大纲
- 小于等于6个参数
- 一个参数
- 总结
- 两个参数
- 总结
- 三个参数
- 总结
- 四个参数
- 总结
- 五个参数
- 总结
- 六个参数
- 总结
- 大于6个参数
- 七个参数
- 总结
在32位系统中,参数的传递主要依靠栈来进行。那么64位系统上,是否依旧符合这个规则呢?答案是“不是”。64位系统使用了寄存器和栈结合的方案。当参数比较少的时候,使用寄存器传递参数;当参数比较多时,前几个参数仍然采用寄存器传递,但是后几个参数会采用栈传递。
本文我们将探测从1到10个参数传递的汇编实现。
我们在main函数中准备10个栈上变量
int main() {
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int e = 50;
int f = 60;
int g = 70;
int h = 80;
int i = 90;
int j = 100;
每个int型占4个字节,所以一共需要40个字节,即0x28。但是编译器为了让内存对齐,多分配了8个字节。于是我们看到编译器直接通过下面的语句表达栈上变量空间是0x30(即栈增长了0x30)。
0x000000000000135a <+8>: sub $0x30,%rsp
离rbp最远的是a变量的地址,即-0x28(%rbp)。它表达的是该地址是%rbp-0x28。使用减法的原因是,栈的增长方向是向地址空间低的方向。具体这块知识见《从汇编层看64位程序运行——程序中的栈(Stack)结构及其产生的历史原因》。
小于等于6个参数
如果参数的个数小于等于6个,则采用寄存器传递。
一个参数
void foo1(int a) {
a = a + 5;
}
调用foo1处的汇编如下
它会将栈中a变量的值放到eax寄存器中,然后将eax寄存器的值放到edi寄存器中。edi就充当了参数传递的“使者”。
我们在foo1的汇编代码处可以看到,它将edi寄存器中的值保存到它的栈帧的地址空间中(rbp-0x04),然后才可开始做计算。
可能有人注意到,为什么调用处要先将变量值保存到eax,然后再保存到edi中,而不是直接保存到edi中呢?在这个案例中,的确是没有必要的。但是后面涉及栈传递参数时,这种设计就很有必要了。
总结
一个参数时直接使用edi寄存器传递参数。
两个参数
void foo2(int a, int b)
a(-0x28(%rbp))被先保存到eax寄存器中,然后eax寄存器的值保存到edi寄存器中;
b(-0x24(%rbp))被先保存到edx寄存器中,然后edx寄存器的值保存到esi寄存器中;
这意味着a的值被保存到edi,b的值被保存到esi中,然后借用这两个寄存器进行参数传递。
总结
两个参数时,参数分别通过edi、esi寄存器传递。
三个参数
void foo3(int a, int b, int c)
第一个参数a和之前的规则一样,先保存到eax,然后再保存到edi中。
但是这次由于edx寄存器要参与参数传递,即foo3函数要使用edx寄存器。于是第二个参数值先被保存到ecx寄存器中,然后再传递给esi寄存器。
总结
三个参数时,参数分别通过edi、esi和edx寄存器传递。
四个参数
void foo4(int a, int b, int c, int d)
第一个参数a和之前的规则一样,先保存到eax,然后再保存到edi中。
而这次由于ecx也要参与参数传递,于是b(-0x24(%rbp))被直接保存到esi中、c(-0x20(%rbp))被直接保存到edx中、d(-0x1c(%rbp))被直接保存到ecx中。这次参数的传递没有经过太多中间寄存器,相对高效。
总结
四个参数时,参数分别通过edi、esi、edx和ecx寄存器传递。
五个参数
void foo5(int a, int b, int c, int d, int e)
这次针对第五个参数e,它会先被保存到edi寄存器中,然后edi寄存器的值会保存到r8d寄存器。这就意味着e被保存到r8d寄存器中。由于edi寄存器最终要传递第一个参数a,于是在调用foo5前,会将临时存储a的eax寄存器的值设置到edi寄存器中。
我们发现edi寄存器被频繁使用,而它又被当做帮助第一个参数传递的寄存器,于是编译器会先见第一个参数保存到其他寄存器中,然后在调用函数之前在将临时存储第一个参数的寄存器的值保存到edi中。
总结
五个参数时,参数分别通过edi、esi、edx、ecx和r8d寄存器传递。
六个参数
void foo6(int a, int b, int c, int d, int e, int f)
具体的方式和之前类似,只是r8d和edi都作为临时寄存器保存栈上数据,然后在调用foo6函数前,将它们还原成它们理应要去代表的参数。
总结
六个参数时,参数分别通过edi、esi、edx、ecx、r8d和r9d寄存器传递。
大于6个参数
如果参数的个数大于6个,则前6个仍然采用寄存器传递,后面的使用栈传递。
七个参数
void foo7(int a, int b, int c, int d, int e, int f, int g)
前6个参数使用edi、esi、edx、ecx、r8d和r9d寄存器传递到子函数。
第7个参数g(-0x10(%rbp))被先保存到edi(32位,属于64位rdi),然后被push到栈中。
在foo7函数中,栈上的变量会直接参与计算,而不用再拷贝到foo7的栈帧中。
8,9,10个参数和7个参数是类似的,过程就不写了。
总结
- 参数不超过6个时,参数按需使用edi、esi、edx、ecx、r8d和r9d寄存器传递到子函数。
- 参数超过6个参数时,前6个参数使用edi、esi、edx、ecx、r8d和r9d寄存器传递到子函数;从第7个参数起,后面的参数都通过栈传递。