x64程序调用协定

作者:Chdon 发布时间: 2026-02-23 阅读量:3 评论数:0

在 x64 架构下,Windows 环境(通过 MSVC 编译器)主要使用 __fastcall 调用协定。与 x86 时代多种调用协定(stdcall, cdecl, fastcall 等)并存的情况不同,x64 在 Win64 平台上几乎只有这一种标准。

它的核心设计目标是:减少内存访问,尽量利用寄存器传递参数。


1. 核心规则:寄存器与栈的分工

在 x64 调用协定中,前 4 个参数通过寄存器传递,剩下的参数则通过栈传递。

参数传递映射表

参数位置整数 / 指针 / 引用浮点数 (float / double)
第 1 个参数RCXXMM0
第 2 个参数RDXXMM1
第 3 个参数R8XMM2
第 4 个参数R9XMM3
第 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);

汇编层面的动作:

  1. 5 压入栈中(RSP 指向 5)。
  2. 4 放入 R9
  3. 3 放入 R8
  4. 2 放入 RDX
  5. 1 放入 RCX
  6. SUB RSP, 20h (预留 32 字节的影子空间)。
  7. CALL MyFunc

评论