13.4 ABI 与调用约定细节
应用二进制接口(Application Binary Interface,ABI)规定独立编译代码如何互操作。调用约定(calling convention)是其中关于函数调用的部分:参数与结果放在哪里、哪些寄存器必须幸存、栈如何对齐,以及控制如何返回。ABI 还覆盖数据布局、符号命名、异常处理与目标文件约定。
源语言类型一致还不够;caller 与 callee 必须对每个值的机器级分类完全一致。
参数分类与放置
多数 64 位 ABI 先用若干 general-purpose registers 传整数/指针,用另一组寄存器传浮点/向量,超出后再放栈上。具体寄存器不同:
- System V x86-64 的整数参数从
rdi, rsi, rdx, rcx, r8, r9开始;
- Windows x64 从
rcx, rdx, r8, r9开始,并预留 32 字节 shadow space;
- AArch64 常用
x0–x7传整数,用v0–v7传浮点/向量。
聚合类型还需分类。小结构体可能拆进多个寄存器;大型返回值可能通过 caller 分配存储并传隐藏指针。方法可传隐藏 receiver,闭包可传 environment pointer。这些隐藏操作数虽不在源码参数表中,却会消耗 ABI 位置。
栈对齐与变参调用
每个 call boundary 上,stack pointer 必须满足 ABI 对齐规则,现代 64 位平台常见 16 字节。多 push 一个值或预留奇怪大小 frame 都可能破坏对齐,必须用 padding 补偿。
变参函数需要额外协议,因为 callee 无法从固定签名得知所有参数类别。ABI 可能要求镜像浮点参数、传 vector-register 参数计数,或建立 va_start 可遍历的 register-save area。若把普通固定签名 lowering 套在 variadic call 上,callee 可能根本看不到参数。
Caller-saved 与 callee-saved
Caller-saved register 可以被 call 覆盖;caller 若有值跨调用活跃,就必须在调用前保存、移动或 spill。Callee-saved register 则由任何使用它的 callee 负责恢复,通常在 prologue/epilogue 中完成。
这种划分是在分配成本。短命值和 leaf function 偏好 caller-saved,因为无需 prologue 保存。跨许多 call 活跃的值可能更适合 callee-saved,因为函数只支付一次保存/恢复,而不是每次 call 都支付。
Tail call、异常与互操作
只有参数放置、返回约定、栈清理、安全插桩与 unwind 规则兼容时,tail call 才能复用当前 frame;否则后端必须发射普通 call 再 return。
异常处理还包含平台特定 landing pad、personality function 和 unwind table。Foreign function interface 也要处理 name mangling、结构体布局、字节序、所有权,以及由哪一方捕获异常。除非两个运行时明确支持同一机制,否则不能让异常穿过 ABI 边界。
ABI 测试应把生成代码与小型手写 assembly stub 对接,覆盖每个参数寄存器边界、混合整数/浮点、小型/大型结构体、栈对齐、varargs、callback、caller/callee saves、异常和 tail call。一条指令的错误可能只在优化代码或另一编译器调用时暴露。