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 证明契约。