← SEARCHABLE ARCHIVE
Assembly · 2026-08-15

x86-64 函数调用与 ABI 实战

System V 与 Windows x64 的参数、保存寄存器、栈对齐与展开

INTERACTIVE READING MODE →

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 AMD64Microsoft x64
整数参数rdi,rsi,rdx,rcx,r8,r9位置 1~4 用 rcx,rdx,r8,r9
FP 参数主要为 xmm0~xmm7位置 1~4 用 xmm0~xmm3
整数返回raxrax
FP 返回xmm0xmm0
caller 区域无 shadow space32 字节 shadow space
red zone合格 user-space leaf 有 128 字节无
主要 nonvolatile GPRrbx,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 位。

栈对齐要标明时点

必须分开:

  1. caller 的 call 前;
  2. 返回地址已推入的 callee 入口;
  3. prologue 后;
  4. 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。

调试顺序

  1. caller 与 callee 是否使用相同 architecture/ABI?
  2. symbol 与 language linkage 是否一致?
  3. call 前各参数的位置、位与类型是否正确?
  4. stack offset 以哪个时点的 rsp 为基准?
  5. alignment 与 Windows shadow space 是否正确?
  6. callee 是否在所有路径恢复 nonvolatile 寄存器?
  7. Windows frame directive 是否吻合真实 prologue?
  8. 返回值是否从正确宽度的 rax 或 xmm0 读取?

总结

call 与 ret 只是硬件骨架。System V 使用六个主要整数参数寄存器并提供 red zone;Microsoft x64 使用四个按位置分配的 register slot、32 字节 shadow space、nonvolatile XMM 与严格 unwind 规则。不要只背表格,要用 caller/callee 反汇编、边界测试与 stack walk 证明契约。

官方资料