一些有用的基础知识点
寄存器:%rip , %rbp ,%rsp,
指令:push pop call ret
栈结构:栈的结构,栈帧
寄存器 程序计数器 (X86-64中用%rip表示) 用于表示将要执行的下一条指令的内存地址。 堆栈指针 (X86-64中用%rsp表示) 用于指向栈顶。 栈帧指针 (X86-64中用%rbp表示) 用于表示当前栈帧的开始位置。
指令 放到文章后面解释
栈结构 栈数据结构提供后进先出的管理原则,程序可以用栈来管理过程中所需要的存储空间,栈和寄存器存放着传递控制和数据,分配内存所需要的空间。x86-64的栈向低地址方向增长,%rsp指向栈顶元素,pushq和popq指令分别将数据推入或者弹出栈
什么是栈帧
函数在执行时候,需要传输一些参数数据,参数数据在寄存器不够用的时候会压入栈中,整个函数A在栈中占用的所有内存空间就是栈帧
运行时栈
现在说下当函数间发生调用栈中是怎么运作的,为什么过程的调用适合栈这种后进先出的结构。寄存器的概念和指令的介绍在这篇文章里就不说了,具体可以看《深入理解计算机系统(第三版)》第三章的内容吧
下面是一个简单的C程序,调用一个add()
方法
1
2
3
4
5
6
7
8
9
10
11
//demo.c
int static add(int a, int b)
{
return a+b;
}
int main()
{
int x = 5;
int y = 10;
int u = add(x, y);
}
gcc -g -c demo.c
objdump -d demo.o
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//main方法,省略部分代码
0000000000000000 _main:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
...
1c: e8 00 00 00 00 callq 0 <_main+0x21>
...
2d: c3 retq
2e: 66 90 nop
//add 函数
0000000000000030 _add:
30: 55 pushq %rbp
31: 48 89 e5 movq %rsp, %rbp
34: 89 7d fc movl %edi, -4(%rbp)
37: 89 75 f8 movl %esi, -8(%rbp)
3a: 8b 45 fc movl -4(%rbp), %eax
3d: 03 45 f8 addl -8(%rbp), %eax
40: 5d popq %rbp
41: c3 retq
函数调用
首先看代码,就是在main方法中调用了add()
这个函数,传入两个变量。然后到汇编中对应第6行callq
这个指令,它的作用是把下一条指令的地址压入栈中,并且跳到被调用函数的第一行。
详细说明:
- 把第7行的地址压入栈,这么做是为了方便后面从子函数返回时可以继续执行下去。注意返回的地址是在main函数所属的栈帧中的。
- 至于为什么知道下一条指令的地址,那因为pc寄存器指向的就是下一条指令的内存地址。
进入了子函数,在汇编中第12行-16行,第12行将%rbp压入栈中,并且紧接着第13行将%rbp的位置设置为当前%rsp的值,也就是新的栈帧开始位置,
详细说明:
- 由于%rbp是堆栈指针它是指向栈帧开始的位置,所以在第12行指令执行前这个%rbp一直指向的是main函数栈帧中的开始位置。也就是说当子方法
add()
刚被调用往它堆栈中压入的是父函数(当前是main)栈帧中的%rbp地址。这么做是为了后面退出而设计的。- 第13行的 赋值的行为也就可以理解为上一步把父函数的起始位置记录下,紧接着开辟了自己栈帧的新起始位置。
函数调用的几个关节点已经说完了,稍微总结下父函数调用子函数的基本过程就是:
- 父函数调用前先将返回(下一步)地址压栈.
- 跳转到到子函数中。
- 子函数中先将父函数栈帧的起始位置%rbp压栈。
- 将%rbp位置重新设置为子函数栈帧的起始位置。
函数返回
add()
函数执行完后要返回了,因为在前面函数调用是已经保存了返回地址和父类栈帧起始位置。在汇编代码第18,19行。在18行中弹出堆栈指针,当前%rbp又从新回到了父函数栈帧的起始地址,紧接着19行的retq将%rsp返回子到函数调用前的后面一条指令。
详细说明:
- 在汇编代码的14-17行是在通用寄存器中赋值运算,%rbp中的地址并没有发生变化,所以在第18行可以直接popq出栈.
- 一般在一个被调用的函数参数超过6个的话,剩下的参数会被压入运行栈中。
资料引用
- 深入浅出计算机组成原理
- 《深入理解计算机系统(第三版)》第三章