本页目录
目标代码与 ABI
函数调用不是几条指令的事——谁负责保存哪些寄存器、参数怎么传、栈帧怎么布局,全写死在 ABI 里,违反直接崩溃。x86-64 System V 和 Win64 的差异是跨平台编译最常见的坑。
概述
指令选择把 IR 操作映射到目标机器指令。但函数调用不是几条指令的简单映射——涉及谁负责保存哪些寄存器,参数怎么传,栈帧怎么布局,返回地址在哪。这些约定全写在一份 ABI(Application Binary Interface)规范里,操作系统和编译器都要遵守。违反 ABI 程序直接 crash——不是逻辑错误,而是栈被污染,返回地址被覆盖。这篇以 x86-64 System V(Unix/Linux/Mac)为主,对照 Win64,把栈帧的每一块讲清楚。
Calling Convention:寄存器与栈的协议
x86-64 System V 的参数传递:
参数顺序: rdi, rsi, rdx, rcx, r8, r9, [栈 (右压左)]
返回值: rax (64-bit 以内), rdx:rax (128-bit 结构体)
调用者保存: rax, rcx, rdx, rsi, rdi, r8-r11 ← call 后可能被破坏
被调者保存: rbx, rbp, r12-r15 ← 被调函数要用的话必须保存+恢复
一条 fn(a, b, c, d, e, f, g) 的调用在 x86-64 System V 下:
mov rdi, a ; 参数 1
mov rsi, b ; 参数 2
mov rdx, c ; 参数 3
mov rcx, d ; 参数 4
mov r8, e ; 参数 5
mov r9, f ; 参数 6
push g ; 参数 7 → 栈 (在 call 之前推入)
call fn
Win64 不同:前 4 个参数用 rcx, rdx, r8, r9,后全走栈;且调用方必须在栈上为前 4 个参数预留"shadow space"(32 字节)——即使用不上也有这个位置。交叉平台编译时,这是个常踩的坑。
栈帧的解剖
从 rbp(帧指针)出发,正偏移到返回地址和调用方参数;负偏移到自己函数的局部变量。帧指针是可选的——现代编译器常用 frameless:只用 rsp 偏移访问,不 push rbp(节省指令,但调试时难以回溯栈帧)。调试信息(DWARF .debug_frame)存着从 rsp 到历史 rbp 的恢复规则。
Red zone(x86-64 System V)
rsp 以下 128 字节是 red zone——函数可以用这段区域而不用动 rsp。仅当该函数不调用其他函数时有效(leaf function)。Win64 没有 red zone——Windows 上 rsp 以下不保证不被信号处理程序/中断践踏。
Prologue / Epilogue
每个函数在入口(prologue)和出口(epilogue)插入标准序列:
Prologue:
push rbp ; 保存帧指针
mov rbp, rsp ; 设新的帧指针
sub rsp, N ; 为局部变量 + spill + callee-saved 分配栈空间
push rbx (etc.) ; 保存被调者寄存器 (如果本函数用了)
Epilogue:
pop rbx (etc.) ; 恢复被调者寄存器
mov rsp, rbp ; 释放栈空间
pop rbp ; 恢复旧帧指针
ret
编译器知道哪些寄存器本函数使用了——用不到 rbx 就不 push rbx。帧布局(哪个寄存器在栈上的哪个位置)是编译时确定的,prologue/epilogue 的 push/pop 和偏移是常量。
Variadic Function(变参函数)
printf(fmt, a, b, c, ...) 的参数数量和类型在编译时不知道。x86-64 ABI 规定变参函数的参数传递和普通函数一样(前 6 个到寄存器,后走栈),但**al 寄存器记录浮点参数用了几个 XMM 寄存器**——这是让 printf 内部能检索 vararg 的关键。
对编译器的要求:在 vararg 调用点,编译器需要确保所有 XMM 寄存器里可能传的参数已被存到对应的 va_list 位置。va_start/va_arg 的实现完全基于 ABI 的寄存器+栈参数布局。
PIC / PIE:位置无关代码
共享库和现代可执行文件需要 PIC(Position-Independent Code):代码段可以在内存中任意位置加载,不需要重定位改代码本身(代码段是共享的,改了影响所有进程)。代价是全局变量和函数的访问多一层间接:
非 PIC: PIC:
mov rax, [addr_of_x] mov rax, [rip + offset_to_GOT_entry]
call fn call [rip + offset_to_GOT_entry_for_fn]
; GOT = Global Offset Table, 在数据段中
编译器生成 PIC 时,所有"外部符号的地址"通过 GOT 间接——加载时动态链接器填 GOT 表项。rip + offset 是 PC 相对寻址(不需要绝对地址),保证代码段和地址无关。
参考
- System V AMD64 ABI: https://gitlab.com/x86-psABIs/x86-64-ABI — 权威规范
- Microsoft x64 ABI: https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions
- Agner Fog: "Calling Conventions" — 各平台 ABI 的工程化汇总
Keywords: ABI, calling convention, System V, Win64, stack frame, prologue, epilogue, frame pointer, red zone, caller-saved, callee-saved, register passing, shadow space, variadic, va_list, AL register, PIC, PIE, GOT, PLT, PC-relative addressing, stack alignment