1、栈和函数调用的基本概念
栈(FIFO):在数据结构中是一个特殊的容器,遵守先入栈的数据后出栈。在计算机系统中是一个具有以上属性的动态内存区域。栈总是向下增长,压栈操作使栈顶地址减小,弹出操作使栈顶地址增大。
每个进程都会有自己的栈空间,而进程中的各个函数也会维护自己本身的一个栈的区域,这个区域包含了函数调用所需要维护的信息,这个区域常常被称为栈帧或活动记录,堆栈帧一般包含如下几个方面:
1、函数返回地址和参数
2、临时变量:包含函数的非静态局部变量以及编译器自动生成的其他临时变量
3、保存存的上下文:包含在函数调用前后需要保持不变的寄存器
在x86中,常用ebp和esp记录范围,esp记录栈顶位置,ebp指向栈帧里面的一个固定位置,通过ebp加减地址,访问栈帧位置
函数调用过程:
1、把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。
2、把当前指令的下一条指令地址压入栈中。
3、跳转到函数体执行。
2、示例
2.1、通用栈帧结构
上一个栈帧RBP/EBP存储的是调用者(上一个函数的rbp地址)。当前的rbp寄存器存储的是上一个栈帧RBP/EBP的地址(类似二级指针)
在x86系统的CPU中,rsp是栈指针寄存器,这个寄存器中存储着栈顶的地址。 rbp中存储着栈底的地址。 函数栈空间主要是由这两个寄存器来确定的
当程序运行时,栈指针RSP可以移动,栈指针和帧指针rbp一次只能存储一个地址,所以,任何时候,这一对指针指向的是同一个函数的栈帧结构。
而帧指针rbp是不移动的,访问栈中的元素可以用-4(%rbp)或者8(%rbp)访问%rbp指针下面或者上面的元素。
2.2、源代码与反汇编代码
源代码:
#include <stdio.h>
int sum (int a,int b)
{
int c = a + b;
return c;
}
int main()
{
int x = 5,y = 10,z = 0;
z = sum(x,y);
printf("%d\r\n",z);
return 0;
}
反汇编代码:
0000000000000000 <sum>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d ec mov %edi,-0x14(%rbp) # 参数传递
7: 89 75 e8 mov %esi,-0x18(%rbp) # 参数传递
a: 8b 55 ec mov -0x14(%rbp),%edx
d: 8b 45 e8 mov -0x18(%rbp),%eax
10: 01 d0 add %edx,%eax
12: 89 45 fc mov %eax,-0x4(%rbp) # 局部变量
15: 8b 45 fc mov -0x4(%rbp),%eax # 存储结果
18: 5d pop %rbp
19: c3 retq
000000000000001a <main>:
1a: 55 push %rbp # 保存%rbp。rbp,栈底的地址
1b: 48 89 e5 mov %rsp,%rbp # 设置新的栈指针。rsp 栈指针,指向栈顶的地址
1e: 48 83 ec 10 sub $0x10,%rsp # 分配 16字节栈空间。%rsp = %rsp-16
22: c7 45 f4 05 00 00 00 movl $0x5,-0xc(%rbp) # 赋值
29: c7 45 f8 0a 00 00 00 movl $0xa,-0x8(%rbp) # 赋值
30: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) # 赋值
37: 8b 55 f8 mov -0x8(%rbp),%edx
3a: 8b 45 f4 mov -0xc(%rbp),%eax
3d: 89 d6 mov %edx,%esi # 参数传递 ,从右向左
3f: 89 c7 mov %eax,%edi # 参数传递
41: e8 00 00 00 00 callq 46 <main+0x2c> # 调用sum
46: 89 45 fc mov %eax,-0x4(%rbp)
49: 8b 45 fc mov -0x4(%rbp),%eax # 存储计算结果
4c: 89 c6 mov %eax,%esi
4e: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 55 <main+0x3b>
55: b8 00 00 00 00 mov $0x0,%eax
5a: e8 00 00 00 00 callq 5f <main+0x45>
5f: b8 00 00 00 00 mov $0x0,%eax
64: c9 leaveq
65: c3 retq
2.3、分析
2.3.1、main函数分析
1)建立了main函数的栈:
main函数调用前,调用者会为main函数做准备。首先,函数栈上开辟了16字节的空间,存储定义的3个int型变量,建立了main函数的栈(1@:main函数分配,sum函数未分配,sub $0x10,%rsp )
push %rbp # 保存%rbp。rbp,栈底的地址
mov %rsp,%rbp # 设置新的栈指针。rsp 栈指针,指向栈顶的地址
sub $0x10,%rsp # 分配 16字节栈空间。%rsp = %rsp-16
2)会给三个变量进行赋值
接着,先把z压栈,然后y然后x
movl $0x5,-0xc(%rbp) # 赋值 x=5
movl $0xa,-0x8(%rbp) # 赋值 y=10
movl $0x0,-0x4(%rbp) # 赋值 z = 0
3)参数传递:
我们可以看到是函数参数是倒序传入的:先传入第N个参数,再传入第N-1个参数(CDECL约定)
2@:esi 与edi不能直接接受内存地址的数据(猜测,未验证)
mov -0x8(%rbp),%edx # 把y值存到edx
mov -0xc(%rbp),%eax # 把x值存到到eax
mov %edx,%esi # 参数传递 ,从右向左
mov %eax,%edi # 参数传递
4)调用sum函数执行到call指令处,CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。具体来说,call指令执行时,先把下一条指令的地址入栈,再跳转到对应函数执行的起始处。
callq 46 <main+0x2c> # 调用sum
2.3.2、sum函数分析
1)压栈rbp,赋值rbp
push %rbp
mov %rsp,%rbp
重要:此时,原rbp寄存器的入栈,新的rbp寄存器的地址指向栈顶。rbp处于一个很重要的位置。
%rbp+4处为返回地址,%rbp+8处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),%rbp-4处为第一个局部变量,%rbp处为上一层rbp值。
由于rbp中的地址处总是“上一层函数调用时的rbp值”,而在每一层函数调用中,都能通过当时的%rbp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。
2)传递参数上述指令通过rbp加偏移量的方式将main传递给sum的两个参数保存在当前栈帧的合适位置,然后又取出来放入寄存器,看着有点儿多此一举,这是因为在编译时未给gcc指定优化级别,而gcc编译程序时,默认不做任何优化,所以看起来比较啰嗦。
空出来的4个字节,是为了保证8字节对齐
mov %edi,-0x14(%rbp) # 参数传递 b 这是对的
mov %esi,-0x18(%rbp) # 参数传递 a 这是对的
mov -0x14(%rbp),%edx
mov -0x18(%rbp),%eax
3)执行运算,并存储结果
第一条指令负责执行加法运算并将并将结果存入eax中,第二条指令将eax中的值存入局部变量c所在的内存,第三条指令将局部变量c的值读取到eax中
add %edx,%eax
mov %eax,-0x4(%rbp) # 局部变量 c
mov -0x4(%rbp),%eax # 存储结果
4)函数返回
pop %rbp
retq
相当于这三条指令
mov %rbp,%rsp
pop %rbp
pop %rip
1.1)首先把rsp赋值,它的值是存储调用函数rbp的值的地址通过出栈操作,来找回调用函数的rbp
1.2)rbp上面就是调用函数调用被调用函数的下一条指令的执行地址,所以需要赋值给rip(指令寄存器)
整个函数跳转回main,他的rsp,rbp都会变回原来的main函数的栈指针
2.3.3、sum函数调用后
1)函数结果存储在eax寄存器里,把函数结果赋值给main函数的局部变量的z中
mov %eax,-0x4(%rbp) # 把sum函数的返回值赋给变量z z = sum(x,y);
2)上述指令首先为printf准备参数,然后调用printf,具体过程和调用sum的过程相似,让CPU直接执行到main倒数第二条leave指令处。
mov -0x4(%rbp),%eax #传参
mov %eax,%esi #传参
lea 0x0(%rip),%rdi
mov $0x0,%eax
callq 5f <main+0x45>
3)将main的返回值赋值给eax
mov $0x0,%eax
4)恢复main函数调用前的状态
leave指令首先将rbp的值复制给rsp,rsp就指向rbp所指的栈单元。之后leave指令将该栈单元的值pop给rbp,如此,rsp和rbp就恢复成刚进入main时的状态
leaveq
retq
这两条指令相当于,与sum函数退出一样
mov %rbp, %rsp
pop %rbp