汇编 - 理解函数调用栈

首先介绍下面会用到的几个寄存器:
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函数开始一句句汇编进行解读:

image.png

0、初始时
rsp = 0x00007ffeefbff418
rbp = 0x00007ffeefbff428

image.png

1、 pushq %rbp
将rbp的地址压栈,rsp继续指向栈顶,所以我们可以看到
rsp = 0x00007ffeefbff418 - 0x8 = 0x00007ffeefbff410
此时栈顶地址存放的内容就是刚才压栈的rbp的地址,即
*0x00007ffeefbff410 = 0x00007ffeefbff428(我这里就用*表示取地址的内容,如果有不懂打印的为什么是反的,可以先去了解下大小端)。

2、 movq %rsp, %rbp
将栈顶rsp的值赋值给栈底rbp,即
rsp = rbp = 0x00007ffeefbff410

image.png

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之前的值先用第三步预留的内存存储下来,因为下面调用函数里面可能会修改这两个寄存器里面的值。

image.png
image.png

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

image.png

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的地址

image.png

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

image.png

16、 movl -0x4(%rbp), %edi
17、 movl -0x8(%rbp), %esi
前面提到过寄存器edi、esi一般用来做函数参数存储。所以这里就是将a的值用edi存储,b的值用esi存储,即
edi = 1
esi = 2
此时rsp = 0x00007ffeefbff3e0
rbp = 0x00007ffeefbff3f0

image.png
image.png

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

image.png

21、 movl %edi, -0x4(%rbp)
22、 movl %esi, -0x8(%rbp)
把前面用edi、esi存储的参数用test2的栈空间存储,即
*0x00007ffeefbff3cc = 1
*0x00007ffeefbff3c8 = 2

image.png

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

image.png

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

image.png

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

image.png

33、 movl -0x14(%rbp), %eax
34、 addl $0x4, %eax
这里将前面计算的c的值加4,将结果放入寄存器eax,前面提到eax也做函数返回,所以此时eax = 13

image.png

35、 popq %rbp
这句指令表示出栈,同时将出栈的值放入寄存器rbp,所以有
rsp = 0x00007ffeefbff3d0 + 0x8 = 0x00007ffeefbff3d8
rbp = *0x00007ffeefbff3d0 = 0x00007ffeefbff3f0

image.png
从这里开始退出test2函数

36、 retq
这句表示退出test2函数,同时出栈,并且断点跳到出栈值的地址,所以可以看到
rsp = 0x00007ffeefbff3d8 + 0x8 = 0x00007ffeefbff3e0,而之前
*0x00007ffeefbff3d8 = 0x100002faa,所以此时跳到0x100002faa
同时我们可以发现栈顶rsp = 0x00007ffeefbff3e0 栈底rbp = 0x00007ffeefbff3f0,与进入test2函数之前的值保持一致,说明函数在调用前后会保持栈平衡,即从哪里开始,最后又会回到哪里

从这里开始回到test1函数
image.png

37、 movl %eax, %ecx
前面提到test2的返回值存放在寄存器eax,这里先将返回值用寄存器ecx存储,即ecx = 13

image.png

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

image.png

41、 addq $0x10, %rsp
这句正好对应前面的subq $0x10, %rsp,此时
rsp = 0x00007ffeefbff3e0 + 0x10 = 0x00007ffeefbff3f0

image.png

42、 popq %rbp
这句指令表示出栈,同时将出栈的值放入寄存器rbp,所以有
rsp = 0x00007ffeefbff3f0 + 0x8 = 0x00007ffeefbff3f8
rbp = *0x00007ffeefbff3f0 = 0x00007ffeefbff410

image.png
从这里开始退出test1函数

43、 retq
这句表示退出test1函数,同时出栈,并且断点跳到出栈值的地址,所以可以看到
rsp = 0x00007ffeefbff3f8 + 0x8 = 0x00007ffeefbff400,而之前
*0x00007ffeefbff3f8 = 0x100002fdb,所以此时跳到0x100002fdb
同时我们可以发现栈顶rsp = 0x00007ffeefbff400 栈底rbp = 0x00007ffeefbff410,与进入test1函数之前的值保持一致,再次验证了前面的观点。

44、 xorl %eax, %eax
因为函数test1无返回值,所以这里eax也没实际用到

image.png

45、 addq $0x10, %rsp
这句正好对应前面的subq $0x10, %rsp,此时
rsp = 0x00007ffeefbff400 + 0x10 = 0x00007ffeefbff410

image.png

46、 popq %rbp
这句指令表示出栈,同时将出栈的值放入寄存器rbp,所以有
rsp = 0x00007ffeefbff410 + 0x8 = 0x00007ffeefbff418
rbp = *0x00007ffeefbff410 = 0x00007ffeefbff428
此时也正好对应初始时栈顶和栈底的值。

总结:函数调用会保持栈平衡
补充:函数递归没有退出条件之所以会造成死循环,就是因为rsp会一直往下减,直至减到栈区范围外,这样就会造成栈溢出。

你可能感兴趣的:(汇编 - 理解函数调用栈)