應用程式二進位介面(ABI)
/ A-B-I /
兩段素未謀面的已編譯程式碼——你的程式與一個程式庫,或你的程式與核心——要能協同運作,仍必須在許許多多底層細節上達成共識:哪個 CPU 暫存器存放第一個引數、誰負責保存哪些暫存器、一個 int 有多大、一個結構在記憶體中如何排列、堆疊如何安排。應用程式二進位介面(ABI)正是這份在已編譯二進位層級上達成的契約。如果 API 是原始碼層級的契約(你所呼叫函式的名稱與形狀),那麼 ABI 就是下一層、在機器層級的契約,讓編譯出來的二進位檔真正能互通。
具體來說,ABI 釘死了編譯器必須做得一模一樣、程式碼才能彼此連結與一起執行的那些事。呼叫慣例規定引數放在哪(在 x86-64 System V 上:前六個整數引數放在 rdi、rsi、rdx、rcx、r8、r9,回傳值放在 rax),以及被呼叫的函式必須保留哪些暫存器。它固定了資料型別的大小與對齊、結構的記憶體配置、堆疊框架格式,以及如何進行系統呼叫。由於已編譯的程式把這些選擇都編碼進去了,它們在原始碼早已不見之後仍能存活:多年前建置的二進位檔今天仍能執行,正是因為它所依據的 ABI 一直保持穩定。
它最重要的意義是一個穩定性承諾。Linux 以保持核心對使用者 ABI 穩定而聞名——數十年前編譯的程式今天仍能發出相同的系統呼叫——這正是為什麼舊的二進位檔能跨越核心升級持續運作。相對地,核心內的模組介面則刻意不穩定,因此驅動程式往往必須為每個核心版本重新編譯。一個常見的混淆是把 API 和 ABI 搞混:你可以保持 API 不變(相同的函式名稱),卻破壞了 ABI(更改某個結構的大小),於是即使原始碼仍能編譯,舊的二進位檔卻會當掉。
建置同一程式不同部分的兩個編譯器,必須透過 ABI 達成共識:第一個整數引數會在 rdi 裡到達。若其中一個遵守、另一個卻改用堆疊傳遞,被呼叫的函式就會在本該是引數的地方讀到垃圾——即使兩者都乾淨地編譯通過、原始碼也相符。ABI 正是阻止這種不一致的東西。
API 約定名稱;ABI 約定暫存器、大小與配置,好讓二進位檔互通。
ABI 是依 CPU 與作業系統而定的:為 x86-64 Linux 建置的二進位檔,無法在 ARM64 Linux 或 Windows 上執行,因為即使原始碼完全相同,各家的 ABI 也不同。這是軟體要分發成許多個別建置版本的原因之一。