从汇编层看64位程序运行——参数传递的底层实现


在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个参数起,后面的参数都通过栈传递。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值