首先介绍下面会用到的几个寄存器:
rsp : 栈指针寄存器,指向栈顶
rbp : 栈基址寄存器,指向栈底
edi : 函数参数
rsi/esi : 函数参数
eax : 累加器或函数返回值用
int test2(int a, int b) {
int v1 = a + 1;
int v2 = b + 2;
int c = v1 + v2 + 3;
return c + 4;
}
void test1() {
int a = 1;
int b = 2;
int c = a + b + test2(a, b);
}
int main(int argc, const char * argv[]) {
test1();
return 0;
}
首先我们要知道,函数栈里面的内存地址是从高到低的。
下面从main函数开始一句句汇编进行解读:
0、初始时
rsp = 0x00007ffeefbff418
rbp = 0x00007ffeefbff428
1、 pushq %rbp
将rbp的地址压栈,rsp继续指向栈顶,所以我们可以看到
rsp = 0x00007ffeefbff418 - 0x8 = 0x00007ffeefbff410
此时栈顶地址存放的内容就是刚才压栈的rbp的地址,即
*0x00007ffeefbff410 = 0x00007ffeefbff428
(我这里就用*表示取地址的内容,如果有不懂打印的为什么是反的,可以先去了解下大小端)。
2、 movq %rsp, %rbp
将栈顶rsp的值赋值给栈底rbp,即
rsp = rbp = 0x00007ffeefbff410
3、 subq $0x10, %rsp
栈顶往下移16个字节,可以理解成给后面预留的16字节的空间。此时
rsp = 0x00007ffeefbff410 - 0x10 = 0x00007ffeefbff400
4、 movl $0x0, -0x4(%rbp)
5、 movl %edi, -0x8(%rbp)
6、 movq %rsi, -0x10(%rbp)
这三句可以理解成将寄存器edi和rsi之前的值先用第三步预留的内存存储下来,因为下面调用函数里面可能会修改这两个寄存器里面的值。
7、 callq 0x100002f80
call表示调用函数,同时call有一个作用:将call指令的下一条指令地址压栈。所以此时栈顶
rsp = 0x00007ffeefbff400 - 0x8 = 0x00007ffeefbff3f8
rbp = 0x00007ffeefbff410
并且里面存放的内容就是call下一条指令的地址,即
*0x00007ffeefbff3f8 = 0x100002fdb
从这里开始进入test1函数
8、 pushq %rbp
rbp压栈,则
rsp = 0x00007ffeefbff3f8 - 0x8 = 0x00007ffeefbff3f0
*0x00007ffeefbff3f0 = 0x00007ffeefbff410
9、 movq %rsp, %rbp
将栈顶rsp的值赋值给栈底rbp,即
rsp = rbp = 0x00007ffeefbff3f0
10、 subq $0x10, %rsp
栈顶往下移16个字节,可以理解成给test1函数栈空间分配16字节内存。
rsp = 0x00007ffeefbff3f0 - 0x10 = 0x00007ffeefbff3e0
11、 movl $0x1, -0x4(%rbp)
将1放入内存地址0x00007ffeefbff3f0 - 0x4
中,即
*0x00007ffeefbff3ec = 1
正好对应a = 1
,所以可以猜测0x00007ffeefbff3ec
就是a的地址
12、 movl $0x2, -0x8(%rbp)
同上,可知将2放入内存地址0x00007ffeefbff3f0 - 0x8
中,即
*0x00007ffeefbff3e8 = 2
正好对应 b = 2
,可猜测0x00007ffeefbff3e8
就是b的地址
13、 movl -0x4(%rbp), %eax
14、 addl -0x8(%rbp), %eax
15、 movl %eax, -0x10(%rbp)
在文章最前面提过eax寄存器一般作为累加器,所以源码里面的int c = a + b + test2(a, b);
这里就很好理解,前面不是分配了16个字节的内存么,我们只用到了高8个字节的内存分别存储a、b,这里将a、b的值通过累加器加起来,再用低8个字节的内存 -0x10(%rbp)存储a+b的和,即
*0x00007ffeefbff3e0 = 3
16、 movl -0x4(%rbp), %edi
17、 movl -0x8(%rbp), %esi
前面提到过寄存器edi、esi一般用来做函数参数存储。所以这里就是将a的值用edi存储,b的值用esi存储,即
edi = 1
esi = 2
此时rsp = 0x00007ffeefbff3e0
rbp = 0x00007ffeefbff3f0
18、 callq 0x100002f50
同第7步,call的下一条汇编指令地址压栈,所以
rsp = 0x00007ffeefbff3e0 - 0x8 = 0x00007ffeefbff3d8
*0x00007ffeefbff3d8 = 0x100002faa
从这里开始进入test2函数
19、 pushq %rbp
rbp压栈,则
rsp = 0x00007ffeefbff3d8 - 0x8 = 0x00007ffeefbff3d0
*0x00007ffeefbff3d0 = 0x00007ffeefbff3f0
20、 movq %rsp, %rbp
将栈顶rsp的值赋值给栈底rbp,即
rsp = rbp = 0x00007ffeefbff3d0
21、 movl %edi, -0x4(%rbp)
22、 movl %esi, -0x8(%rbp)
把前面用edi、esi存储的参数用test2的栈空间存储,即
*0x00007ffeefbff3cc = 1
*0x00007ffeefbff3c8 = 2
23、 movl -0x4(%rbp), %eax
24、 addl $0x1, %eax
25、 movl %eax, -0xc(%rbp)
将参数0x00007ffeefbff3cc
里面存放的值(1)通过累加器eax,计算之后的值放入地址-0xc(%rbp),即0x00007ffeefbff3c4 = 2
,正好对应源代码int v1 = a + 1;
,所以这里v1的地址就是0x00007ffeefbff3c4
26、 movl -0x8(%rbp), %eax
27、 addl $0x2, %eax
28、 movl %eax, -0x10(%rbp)
将参数0x00007ffeefbff3c8
里面存放的值(2)通过累加器eax,计算之后的值放入地址-0x10(%rbp),即0x00007ffeefbff3c0 = 2
,正好对应源代码int v2 = b + 2;;
,所以这里v2的地址就是0x00007ffeefbff3c0
29、 movl -0xc(%rbp), %eax
30、 addl -0x10(%rbp), %eax
31、 addl $0x3, %eax
32、 movl %eax, -0x14(%rbp)
这里就是通过累加器eax将-0xc(%rbp)和-0x10(%rbp)里面的值相加,再加上3,放入地址-0x14(%rbp)中,正好对应int c = v1 + v2 + 3;
,所以这里c的地址就是-0x14(%rbp)即0x00007ffeefbff3bc
,且*0x00007ffeefbff3bc = 9
33、 movl -0x14(%rbp), %eax
34、 addl $0x4, %eax
这里将前面计算的c的值加4,将结果放入寄存器eax,前面提到eax也做函数返回,所以此时eax = 13
35、 popq %rbp
这句指令表示出栈,同时将出栈的值放入寄存器rbp,所以有
rsp = 0x00007ffeefbff3d0 + 0x8 = 0x00007ffeefbff3d8
rbp = *0x00007ffeefbff3d0 = 0x00007ffeefbff3f0
从这里开始退出test2函数
36、 retq
这句表示退出test2函数,同时出栈,并且断点跳到出栈值的地址,所以可以看到
rsp = 0x00007ffeefbff3d8 + 0x8 = 0x00007ffeefbff3e0
,而之前
*0x00007ffeefbff3d8 = 0x100002faa
,所以此时跳到0x100002faa
。
同时我们可以发现栈顶rsp = 0x00007ffeefbff3e0
栈底rbp = 0x00007ffeefbff3f0
,与进入test2函数之前的值保持一致,说明函数在调用前后会保持栈平衡,即从哪里开始,最后又会回到哪里
。
从这里开始回到test1函数
37、 movl %eax, %ecx
前面提到test2的返回值存放在寄存器eax,这里先将返回值用寄存器ecx存储,即ecx = 13
38、 movl -0x10(%rbp), %eax
39、 addl %ecx, %eax
40、 movl %eax, -0xc(%rbp)
因为之前a+b的值存在地址-0x10(%rbp)中,这里这三句正好对应int c = a + b + test2(a, b);
,其中-0xc(%rbp)就是c的地址,所以有
*0x7ffeefbff3e4 = 16
41、 addq $0x10, %rsp
这句正好对应前面的subq $0x10, %rsp
,此时
rsp = 0x00007ffeefbff3e0 + 0x10 = 0x00007ffeefbff3f0
42、 popq %rbp
这句指令表示出栈,同时将出栈的值放入寄存器rbp,所以有
rsp = 0x00007ffeefbff3f0 + 0x8 = 0x00007ffeefbff3f8
rbp = *0x00007ffeefbff3f0 = 0x00007ffeefbff410
从这里开始退出test1函数
43、 retq
这句表示退出test1函数,同时出栈,并且断点跳到出栈值的地址,所以可以看到
rsp = 0x00007ffeefbff3f8 + 0x8 = 0x00007ffeefbff400
,而之前
*0x00007ffeefbff3f8 = 0x100002fdb
,所以此时跳到0x100002fdb
。
同时我们可以发现栈顶rsp = 0x00007ffeefbff400
栈底rbp = 0x00007ffeefbff410
,与进入test1函数之前的值保持一致,再次验证了前面的观点。
44、 xorl %eax, %eax
因为函数test1无返回值,所以这里eax也没实际用到
45、 addq $0x10, %rsp
这句正好对应前面的subq $0x10, %rsp
,此时
rsp = 0x00007ffeefbff400 + 0x10 = 0x00007ffeefbff410
46、 popq %rbp
这句指令表示出栈,同时将出栈的值放入寄存器rbp,所以有
rsp = 0x00007ffeefbff410 + 0x8 = 0x00007ffeefbff418
rbp = *0x00007ffeefbff410 = 0x00007ffeefbff428
此时也正好对应初始时栈顶和栈底的值。
总结:函数调用会保持栈平衡
补充:函数递归没有退出条件之所以会造成死循环,就是因为rsp会一直往下减,直至减到栈区范围外,这样就会造成栈溢出。