呼叫慣例是關於參數放置、結果、保留狀態與堆疊配置的二進位契約。Linux x86-64 程式使用 System V AMD64 函式 ABI;32 位元程式使用 System V i386 ABI。Linux 系統呼叫使用分開的進入 ABI,不得視為一般函式呼叫。
AMD64 函式呼叫 #
AMD64 ABI 首先會分類每個參數。整數類別值(包括一般整數與指標)使用 rdi、rsi、rdx、rcx、r8 與 r9。SSE 類別值使用 xmm0 至 xmm7。耗盡一個類別本身不會耗盡另一個類別;無法依分類規則指派的參數會放在堆疊輸入區域。
聚合型別會分成八位元組類別。因此,符合條件的小型結構可能佔用多個整數或向量暫存器,而記憶體類別結構會在堆疊上傳遞。對記憶體類別回傳值,呼叫者會配置儲存空間,並將其位址放在隱藏參數暫存器 rdi;被呼叫者會在 rax 回傳該位址。
整數類別回傳值使用 rax,接著使用 rdx;SSE 類別結果使用 xmm0,接著使用 xmm1。浮點 float 與 double 結果使用 xmm0,而 long double 等 x87 類別則依 ABI 規定使用 x87 堆疊。
rbx、rbp、r12 至 r15 與 rsp 由被呼叫者保存。變更其中一個的被呼叫者必須在 ret 前恢復它。基礎 System V ABI 中的其他通用暫存器與所有向量暫存器由呼叫者保存,因此呼叫者要在呼叫後保留所需的存活值。
在 call 之前,rsp 必須以 16 位元組對齊。推入八位元組回傳位址後,一般被呼叫者進入時的 rsp % 16 == 8。堆疊參數會依其對齊要求配置填補,而不是一次推入一個。ABI 也在目前 rsp 下方保留 128 位元組的 red zone。葉函式使用者空間函式可以不調整堆疊而使用它,因為正常訊號傳遞會保留它。核心程式碼與以 -mno-red-zone 建置的程式碼不得依賴該區域。
可變參數函式 #
可變參數函式的固定參數使用一般分類演算法。呼叫者還會將 %al 設定為 0 至 8 的上限,表示使用的向量參數暫存器數量。此用途只定義低位元組。因此,未指定 %al 的手寫 printf 呼叫即使每個可見參數都正確,也會違反 ABI。
使用 va_start 的可變參數被呼叫者會在自己的框架中提供暫存器儲存區,並儲存可能需要的參數暫存器。AMD64 va_list 追蹤:
- 該儲存區中的
gp_offset與fp_offset; - 以記憶體傳遞之參數的
overflow_arg_area;以及 - 已儲存參數暫存器的基底
reg_save_area。
va_arg 在推進相關位置時套用型別分類與對齊。由於套用預設參數提升,呼叫者會在需要時以 double 傳遞 float,並以 int 或 unsigned int 傳遞窄整數型別。
i386 函式呼叫 #
i386 System V ABI 會將一般參數依從右至左的順序放在堆疊輸入區域。進入時,0(%esp) 包含回傳位址,4(%esp) 包含第一個參數。在標準 C 慣例下,呼叫者會在回傳後移除參數。
ebx、esi、edi、ebp 與 esp 由被呼叫者保存;eax、ecx 與 edx 由呼叫者保存。整數或指標結果使用 eax,雙字整數可以使用 edx:eax;浮點結果使用 st(0)。結構回傳慣例取決於分類,且可以使用隱藏目的地指標。
傳統的框架指標前置碼為:
push ebp
mov ebp, esp
sub esp, local_sizeebp 並非固有的框架指標,因此最佳化程式碼可以省略此序列。歷史上的 i386 程式假設堆疊以四位元組對齊。目前 GNU/Linux i386 ABI 要求呼叫邊界以 16 位元組對齊,編譯器在與舊呼叫者互通時可以重新對齊進入點。手寫組合語言必須遵循其工具鏈選取的 ABI,而不是從舊範例推論對齊。
Linux 系統呼叫 #
x86-64 Linux 系統呼叫 ABI 如下:
| 用途 | 暫存器 |
|---|---|
| 系統呼叫編號 | rax |
| 參數 1–6 | rdi、rsi、rdx、r10、r8、r9 |
| 結果 | rax |
syscall 無條件破壞 | rcx、r11 |
硬體會將使用者回傳位址儲存在 rcx,並將使用者旗標儲存在 r11;因此第四個參數使用 r10,不同於 AMD64 函式呼叫。核心會在 rax 回傳成功值或負錯誤;-1 至 -4095 的值編碼 -errno。libc 封裝器會將這類錯誤轉換為 -1,並設定執行緒區域的 errno。
; x86-64 Linux: write(1, message, message_length)
mov eax, 1
mov edi, 1
lea rsi, [rel message]
mov edx, message_length
syscall使用 int $0x80 的 i386 程序中,eax 保存編號,參數使用 ebx、ecx、edx、esi、edi 與 ebp,而 eax 保存結果。這些暫存器是系統呼叫運算元,即使 ebx、esi、edi 與 ebp 在 i386 函式 ABI 中由被呼叫者保存;包含內嵌進入程式碼的函式仍必須為其呼叫者保存函式 ABI。
函式 ABI 與系統呼叫 ABI #
AMD64 函式呼叫使用 rcx 作為第四個參數,並保存 rbx、rbp 與 r12–r15;x86-64 系統呼叫使用 r10 作為第四個參數,且明確破壞 rcx/r11。函式依其宣告 API 回傳錯誤,而原始核心邊界使用負錯誤碼。系統呼叫指令也會變更權限層級,且本身不會推入回傳位址或切換堆疊;Linux 進入程式碼會執行所需的核心堆疊轉換。
應用程式程式碼應優先使用 libc 封裝器,因為它們實作兩種契約、錯誤轉換、適用時的取消行為與 vDSO 選取。內嵌組合語言必須向編譯器描述暫存器、條件碼與記憶體效果,主要適用於 C 函式庫與執行期。
System V AMD64 psABI、syscall(2) 與 Linux entry_64.S 是這些契約的主要參考資料。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)