RDI作为第一个参数的“潜规则”
目录
RDI作为第一个参数的“潜规则”
这个问题问到了底层设计的核心。为什么要专门规定 RDI 是第一个参数、RSI 是第二个参数?为什么不随机选两个寄存器(比如 R8 和 R9)?
这背后的原因主要可以归结为两点:硬件遗产的延续 和 极致的性能优化。
1. 硬件遗产:为了“零成本”调用系统指令
在 C 语言中,很多基础函数的操作逻辑是 “目标 来源”。最典型的例子就是 memcpy(dest, src, size)。
-
RDI 的 D 代表 Destination(目的)。
-
RSI 的 S 代表 Source(源)。
设计的巧妙之处:
x86 架构内置了一些非常强大的“字符串指令”(如 movsb),这些指令是硬编码(Hard-wired)在 CPU 电路里的,它们强制要求使用 RSI 作为源,RDI 作为目标。
如果调用约定(ABI)规定第一个参数进 RDI,第二个进 RSI,那么当你写 memcpy(dest, src, n) 时:
-
dest已经在 RDI 里了。 -
src已经在 RSI 里了。 -
CPU 不需要再花时间写
mov指令去交换寄存器的值,直接执行一条硬件指令就能开始搬运。
结论: 这种设计是为了让 C 语言的高频函数(如字符串处理、内存拷贝)能直接对接 CPU 的硬件加速指令。
2. 性能革命:从“内存时代”进化到“寄存器时代”
在老旧的 32 位(x86)时代,函数参数是放在**栈(Stack)**里的。
- 缺点:栈在内存里,CPU 访问内存的速度远慢于寄存器。每次调用函数都要写内存、读内存,非常浪费。
到了 64 位(x86-64)时代,寄存器变多了。工程师们制定了一套规则(System V ABI),规定前 6 个参数必须通过寄存器传递。
为什么要排这个顺序?
这就像一种“标准协议”。如果没有这个规定:
-
A 程序员写的函数认为第一个参数在 RAX。
-
B 程序员写的代码把参数传到了 RBX。
-
结果:程序直接崩溃。
统一规定 RDI, RSI… 的目的,是让编译器、操作系统和不同语言编写的代码能够互相无缝通信。这就像大家约定好在公路右侧行驶一样。
3. 实际意义:对 Pwn 和逆向的影响
作为学习二进制安全(Pwn)的同学,理解这个原因对你至关重要:
-
寻找 Gadgets:在构造 ROP 链时,你经常需要控制第一个参数(比如
system("/bin/sh"))。因为你知道规则是 RDI,所以你会拼命寻找pop rdi; ret这样的指令片段。 -
快速定位漏洞:你在反汇编工具(如 IDA)里看到代码把一个奇怪的地址传给了 RDI,然后调用了一个函数,你立刻就能反应过来:“哦,他在设置这个函数的第一个参数”。
总结对照表
| 维度 | 旧时代 (32位/x86) | 新时代 (64位/x86-64) | 带来的好处 |
|---|---|---|---|
| 存放位置 | 内存栈 (Stack) | 寄存器 (RDI, RSI…) | 速度快(寄存器访问是纳秒级) |
| 指令数量 | 需要多次 push/pop | 简单的 mov 即可 | 代码体积小,执行效率高 |
| 逻辑一致性 | 随意 | 严格遵循 Dest/Source 逻辑 | 硬件级优化,减少数据搬运成本 |
你想看看当参数超过 6 个(寄存器不够用)时,CPU 是如何“被迫”动用内存栈来处理的吗?