x86-64 函式呼叫與 ABI 實戰
System V 與 Windows x64 的參數、保存暫存器、堆疊對齊與展開
x86-64 函式呼叫與 ABI 實戰
本部落格文章以教學為目的。 本文比較常見的 System V AMD64 與 Microsoft x64 ABI;kernel、syscall、vectorcall 等必須另外查閱目標環境文件。
CPU 的 call 只儲存回傳位址並轉移控制。參數和回傳值放哪裡、誰保存暫存器、堆疊如何對齊、框架如何 unwind,都是 ABI 的契約。
ISA、calling convention、ABI
- ISA 定義指令與暫存器的硬體行為。
- calling convention 定義參數、回傳值、保存狀態與堆疊責任。
- ABI 還包含型別配置、object format、symbol、relocation、unwind。
extern "C" 控制 C++ language linkage 與 name mangling,不會把 Windows 函式改成 System V 規則。
兩種常見 ABI 比較
| 項目 | System V AMD64 | Microsoft x64 |
|---|---|---|
| 整數參數 | rdi,rsi,rdx,rcx,r8,r9 | 位置 1~4 用 rcx,rdx,r8,r9 |
| FP 參數 | 主要為 xmm0~xmm7 | 位置 1~4 用 xmm0~xmm3 |
| 整數回傳 | rax | rax |
| FP 回傳 | xmm0 | xmm0 |
| caller 區域 | 無 shadow space | 32 位元組 shadow space |
| red zone | 合格 user-space leaf 有 128 位元組 | 無 |
| 主要 nonvolatile GPR | rbx,rbp,r12~r15,rsp | 另含 rdi,rsi |
| nonvolatile XMM | 無 | xmm6~xmm15 低 128 位元 |
aggregate、vector、variadic、hidden return pointer 需要更詳細分類。
caller-saved 與 callee-saved
volatile 暫存器可在 call 後改變,必要時由 caller 保存。callee 若使用 nonvolatile 暫存器,必須在回傳前恢復。
push rbx
mov rbx, rcx
; use rbx
pop rbx
ret
保存不代表一定用 push;未使用就不用保存,也可用 mov 寫入 stack slot。重點是函式邊界可觀察到的狀態。
System V 與 Windows 呼叫
System V:
mov edi, 10
mov esi, 20
mov edx, 30
call combine
; rax = result
Windows x64:
sub rsp, 40
mov ecx, 10
mov edx, 20
mov r8d, 30
call combine
add rsp, 40
32 位元組是 caller 準備的 shadow space,額外 8 位元組是考慮 call 推入回傳位址後的常見對齊方式。參數少於四個時仍要預留 shadow space。
一般 System V LP64 的 C long 是 64 位元,Windows LLP64 則是 32 位元;「x64」不表示每個語言型別都是 64 位元。
堆疊對齊要標明時點
必須分開:
- caller 的
call前; - 回傳位址已推入的 callee 入口;
- prologue 後;
- nested
call前。
把某時點的 modulo 規則套到另一時點,就會產生常見的 8 位元組混亂。Windows x64 要求 prologue/epilogue 外的 rsp 保持 16 位元組對齊。
red zone 與 unwind
System V user-space leaf 可在符合條件時使用 rsp 下方 128 位元組 red zone。nested call、Windows、kernel 或停用 red zone 的建置不能套用此假設。
Windows non-leaf 函式必須讓真實 prologue 與 unwind metadata 一致。
Work PROC FRAME
push rbx
.pushreg rbx
sub rsp, 32
.allocstack 32
.endprolog
; body
add rsp, 32
pop rbx
ret
Work ENDP
directive 不會代替指令執行,只描述 unwind。兩者不一致時,一般 ret 可能成功,但例外處理、profiler 與 stack walk 仍會失敗。
stack 與浮點參數
Windows 第五個之後的位置使用 stack,但 offset 會隨「callee 入口」或「prologue 後」而變。不能脫離時點把 [rsp+40] 背成第五個參數。
void f(int a, double b, int c, float d);
Windows 約為 a -> rcx、b -> xmm1、c -> r8、d -> xmm3。它按參數位置分配,不是只計算 FP 參數後把 b 放 xmm0。
variadic、結構與隱藏參數
printf 等函式有額外規則:
- System V 用
al傳達與 vector register 參數相關的資訊。 - Microsoft x64 會把 variadic FP 值複製到對應 integer register。
- C default argument promotion 會把
float提升成double。
System V 以 eightbyte 分類 aggregate,可能拆到 integer/SSE 暫存器;Windows 對特定大小以值傳遞,其他情況可能傳 caller temporary 的指標。手寫 ASM 邊界應優先使用固定寬度 scalar、簡單 extern "C" free function,並 assert sizeof、alignof、offsetof,同時檢查 caller 與 callee 反組譯。
大型結構回傳可能加入 hidden return pointer,使所有明示參數看起來往後移一格。
tail call
wrapper:
; 安排 target 所需參數
jmp target
若 ABI 條件允許,編譯器可把最後的 call 加 ret 改成 jmp,讓 target 直接回到原 caller。
除錯順序
- caller 與 callee 是否使用相同 architecture/ABI?
- symbol 與 language linkage 是否一致?
call前各參數的位置、位元與型別是否正確?- stack offset 以哪個時點的
rsp為基準? - alignment 與 Windows shadow space 是否正確?
- callee 是否在所有路徑恢復 nonvolatile 暫存器?
- Windows frame directive 是否吻合真實 prologue?
- 回傳值是否從正確寬度的
rax或xmm0讀取?
總結
call 與 ret 只是硬體骨架。System V 使用六個主要整數參數暫存器並提供 red zone;Microsoft x64 使用四個依位置分配的 register slot、32 位元組 shadow space、nonvolatile XMM 與嚴格 unwind 規則。不要只背表格,要用 caller/callee 反組譯、邊界測試與 stack walk 證明契約。