在 x64 架构下,Windows 环境(通过 MSVC 编译器)主要使用 __fastcall 调用协定。与 x86 时代多种调用协定(stdcall, cdecl, fastcall 等)并存的情况不同,x64 在 Win64 平台上几乎只有这一种标准。
它的核心设计目标是:减少内存访问,尽量利用寄存器传递参数。
1. 核心规则:寄存器与栈的分工
在 x64 调用协定中,前 4 个参数通过寄存器传递,剩下的参数则通过栈传递。
参数传递映射表
| 参数位置 | 整数 / 指针 / 引用 | 浮点数 (float / double) |
|---|---|---|
| 第 1 个参数 | RCX | XMM0 |
| 第 2 个参数 | RDX | XMM1 |
| 第 3 个参数 | R8 | XMM2 |
| 第 4 个参数 | R9 | XMM3 |
| 第 5 个及以后 | 栈 (Stack) | 栈 (Stack) |
2. 关键特性
影子空间 (Shadow Space / Home Space)
这是 x64 调用协定中最容易让初学者困惑的地方。
即使前 4 个参数是通过寄存器传递的,调用者(Caller)仍必须在栈上预留 32 字节 的空间。
-
用途:被调用函数(Callee)如果需要(比如为了调试或寄存器压力太大),可以将 RCX, RDX, R8, R9 的值临时存入这块空间。
-
维护:由调用者负责分配和清理这块空间。
栈平衡与对齐
-
调用者清理:由调用者负责清理栈空间(类似于 x86 的 cdecl)。
-
16 字节对齐:在执行
CALL指令之前,栈指针RSP必须保持 16 字节对齐(除了返回地址压栈导致的偏移)。这主要是为了优化 SSE/AVX 等指令集的性能。
返回值
-
整数/指针:存储在 RAX 中。
-
浮点数:存储在 XMM0 中。
-
大型结构体:通常由调用者在栈上分配空间,并将指向该空间的指针作为“隐藏的第一参数”传递。
3. 寄存器保护策略 (Preservation)
为了保证函数调用前后状态不混乱,寄存器被分为两类:
-
易失性寄存器 (Volatile / Caller-saved):
-
RAX,RCX,RDX,R8,R9,R10,R11以及XMM0-XMM5。 -
规则:被调用函数可以直接覆盖它们。如果调用者在调用后还需要这些值,必须自行备份。
-
-
非易失性寄存器 (Non-volatile / Callee-saved):
-
RBX,RBP,RDI,RSI,R12,R13,R14,R15以及XMM6-XMM15。 -
规则:被调用函数如果要使用它们,必须先入栈保存,并在返回前恢复(
PUSH/POP)。
-
4. 举个例子
假设有如下 C 函数调用:
void MyFunc(int a, int b, int c, int d, int e);
MyFunc(1, 2, 3, 4, 5);
汇编层面的动作:
- 将
5压入栈中(RSP 指向 5)。 - 将
4放入 R9。 - 将
3放入 R8。 - 将
2放入 RDX。 - 将
1放入 RCX。 SUB RSP, 20h(预留 32 字节的影子空间)。CALL MyFunc。