LOADING

加载过慢请开启缓存 浏览器默认开启

x86-64寄存器 & 调用约定

2026/8/27 C++ Cpp x86-64 汇编
本文总阅读量

最近看反汇编时经常遇到RAXEAXXMM0这一堆名字,同一个函数在Windows和Linux下面,参数使用的寄存器居然还不一样,实在有点混乱,故写一篇文章整理一下。

先来看一个函数

我们先写一个很简单的C++函数:

long long Add(long long a, long long b) {
    return a + b;
}

在Windows x64环境下,它可能被编译成:

lea rax, [rcx + rdx]
ret

只有两条指令,看起来却一下冒出了三个寄存器。

实际上这里发生的事情非常简单:

  • 第一个参数a放在RCX
  • 第二个参数b放在RDX
  • 返回值放在RAX
  • lea在这里完成加法
  • ret返回调用者

为什么参数一定放在这些位置?这不是函数自己决定的,而是Windows x64的函数调用规则。我们先从寄存器本身说起,后面再回到这个例子。


寄存器是什么

可以把寄存器理解成CPU内部数量很少、速度很快的临时工作台

程序中的数据大部分放在内存里。CPU要计算a+b时,通常会先把数据放入寄存器,执行运算,再把结果写回内存或者继续交给下一条指令。

在64位x86架构下,最常见的是下面16个通用寄存器:

RAX RBX RCX RDX
RSI RDI RBP RSP
R8  R9  R10 R11
R12 R13 R14 R15

之所以叫“通用”,是因为大部分整数计算、地址计算和临时数据都可以放在里面。不过其中一些寄存器仍然有约定俗成或者硬件规定的用途。

寄存器经常见到的用途
RAX函数返回值、乘除法中的累加器
RCXRDXWindows函数的前两个参数,也常作临时变量
RSIRDILinux等系统上的前两个参数,也用于字符串指令
RSP指向当前栈顶,不能随便改
RBP有些函数用它定位栈帧,优化后也可能当普通寄存器使用
R8R1564位模式新增的通用寄存器

表里的用途不是说RAX只能放返回值,而是编译器和函数调用规则经常这样使用它。


RAXEAXAXAL是什么关系

同一个寄存器可以按不同宽度访问。以RAX为例:

RAX:完整64位
└─ EAX:低32位
   └─ AX:低16位
      ├─ AL:最低8位
      └─ AH:第8到15位

代码里经常会看到:

mov rax, 1  ; 写入完整64位
mov eax, 1  ; 只写低32位
mov ax, 1   ; 只写低16位
mov al, 1   ; 只写最低8位

这里有一个很重要的不对称规则:

写入32位子寄存器时,高32位会自动清零;写入16位或8位时,高位会保留。

例如:

mov rax, 0xffffffffffffffff
mov eax, 1

第二条指令执行后,RAX不是0xffffffff00000001,而是:

0x0000000000000001

编译器经常利用这个特性,用写EAX的方式同时完成赋值和高位清零。

其他通用寄存器也有类似的命名:

64位32位16位8位
RBXEBXBXBL
RCXECXCXCL
RDXEDXDXDL
R8R8DR8WR8B

所以看到ECX时,不要把它理解成另一个独立寄存器,它只是RCX的低32位。


几个有固定职责的寄存器

RIP:下一条指令在哪里

RIP保存接下来要执行的指令位置。普通代码不能像通用寄存器那样直接给它赋值,而是通过jmpcallret等控制流指令改变它。

64位程序也经常用RIP相对寻址访问全局数据:

mov eax, [rip + global_value]

这样代码加载到不同地址后仍然能够正确找到全局变量,是地址随机化和位置无关代码的重要基础。

RSP:当前栈顶在哪里

RSP指向当前栈顶。pushpopcallret都会直接或间接修改它。

push rbx  ; RSP减8,把RBX保存到栈上
pop rbx   ; 从栈中恢复RBX,RSP加8

如果手写汇编时把RSP改错,函数返回地址和局部变量的位置都会一起出问题,因此它虽然属于通用寄存器,实际使用时一点也不“通用”。

RFLAGS:上一次运算发生了什么

RFLAGS里保存零、进位、溢出、符号等状态。cmpaddsub等指令会更新其中的标志位,后面的条件跳转再读取这些结果。

例如:

cmp ecx, edx
je  equal

cmp内部相当于计算ECX-EDX但不保存结果,只更新标志位。如果结果为0,je就跳转到equal


浮点数为什么使用另一组寄存器

通用寄存器主要处理整数和地址。浮点数以及一次处理多份数据的向量运算,通常放在XMM寄存器中:

XMM0 ~ XMM15:每个128位

后来扩展出的YMMZMM并不是完全独立的新寄存器,而是把同一组寄存器向上扩宽:

ZMM0:512位
└─ YMM0:低256位
   └─ XMM0:低128位

一个XMM寄存器可以放一个浮点数,也可以同时放4个32位浮点数。后者就是常说的“单条指令同时处理多份数据”。

例如Windows x64函数返回floatdouble时,结果通常放在XMM0,而不是RAX


函数调用约定

现在回到最开始的问题:编译器为什么知道参数aRCX、参数bRDX

函数调用前,调用者和被调用者必须先约定好下面这些事情:

  • 参数放在哪些寄存器里
  • 多出来的参数放在栈的什么位置
  • 返回值放在哪里
  • 哪些寄存器可以随便改
  • 哪些寄存器修改前必须保存
  • 栈要按照多少字节对齐

这套规则就叫调用约定。调用约定属于应用程序二进制接口的一部分,英文常缩写为ABI。这个名字看着很大,其实在本文里主要就是上面这些函数如何交接数据的规则。

Windows x64的参数传递

Windows x64下,前四个整数或指针参数依次放在:

RCX → RDX → R8 → R9

返回值通常放在RAX

例如:

long long Sum4(long long a, long long b,
               long long c, long long d) {
    return a + b + c + d;
}

刚进入函数时可以这样理解:

RCX = a
RDX = b
R8  = c
R9  = d

浮点参数使用位置对应的XMM0XMM3,更多参数则放到栈上。

Windows还要求调用者在调用函数前预留32字节空间,这块区域通常叫“影子空间”。即使函数只有一个参数,这32字节也要留出来,方便被调用函数保存前四个参数寄存器或完成调试、栈展开等工作。

一个常见调用现场如下:

sub rsp, 40      ; 32字节影子空间 + 8字节对齐
mov ecx, 1       ; 第一个参数
mov edx, 2       ; 第二个参数
call Add
add rsp, 40

call还会自动把返回地址压到栈上,ret再从栈中取出这个地址并跳回调用者。

Linux等系统上的参数传递

Linux、macOS等系统通常使用另一套64位调用规则,前六个整数或指针参数依次放在:

RDI → RSI → RDX → RCX → R8 → R9

返回值同样通常放在RAX

所以同一个Add(a,b)函数,在这套规则下更可能看到:

lea rax, [rdi + rsi]
ret

代码逻辑没有变化,只是参数交接的位置换了。这也是为什么不能只看见RCX就断言它一定是第一个参数,必须先知道程序使用的是哪套调用约定。

两套规则的主要区别可以简单整理成:

项目Linux/macOS常用规则Windows x64
前几个整数参数RDI, RSI, RDX, RCX, R8, R9RCX, RDX, R8, R9
整数返回值RAXRAX
浮点参数XMM0XMM7位置对应的XMM0XMM3
调用者预留影子空间没有32字节
栈顶下方临时区域有128字节Red Zone没有

表里的Red Zone是指:在不调用其他函数的叶子函数中,Linux等系统允许暂时使用RSP下方128字节而不移动栈顶;Windows没有这条规则。


哪些寄存器要自己保存

假设调用者把一个重要变量放在RBX,然后调用了另一个函数。如果对方随手把RBX改掉,调用者回来后数据就丢了。

为了解决这个问题,调用约定把寄存器分成两类。

调用者负责保存

这类寄存器允许被调用函数自由修改。调用者如果还想保留原值,就要在call之前自己保存。

例如Windows x64中的RAXRCXRDXR8R11都属于这一类。它们常用来传参数和放临时值。

被调用者负责保存

这类寄存器如果要修改,被调用函数必须先保存,返回前再恢复。

Windows和Linux常见规则都要求保持RBXRBPR12R15RSP也必须在函数返回前恢复到调用者预期的位置。

常见代码形态如下:

push rbx       ; 先保存调用者的RBX
mov  rbx, rcx  ; 函数内部使用RBX
...
pop  rbx       ; 返回前恢复
ret

因此阅读反汇编时,看见函数开头的一串push,很可能就是在保存后面要使用的寄存器。


回到最开始的例子

现在再看这两条指令:

lea rax, [rcx + rdx]
ret

就不再神秘了:

  1. 程序使用Windows x64调用约定,所以前两个整数参数在RCXRDX
  2. lea把两个参数相加并写入RAX
  3. 整数返回值约定放在RAX
  4. 函数没有局部变量,也没有调用其他函数,所以不需要建立复杂栈帧。
  5. ret从栈中取出返回地址,回到调用者。

总结

  1. 寄存器是CPU内部用于计算和临时保存数据的小块高速存储。
  2. RAXEAXAXAL是同一个寄存器的不同宽度视图。
  3. 写32位子寄存器会清零高32位,写16位或8位不会。
  4. RIP控制下一条指令位置,RSP指向栈顶,RFLAGS保存运算状态。
  5. 浮点和向量运算通常使用XMMYMMZMM寄存器。
  6. 调用约定决定参数、返回值和寄存器保存规则。
  7. Windows x64前四个整数参数使用RCX、RDX、R8、R9,Linux等系统通常使用RDI、RSI、RDX、RCX、R8、R9
  8. 阅读反汇编时一定要先确认平台和调用约定,否则很容易把参数位置看错。

参考资料

本站不开放评论区,如有讨论内容请移步至本站Github讨论区