文章目录
本章主题:
什么是函数栈帧?
理解函数栈帧能解决什么问题?
函数栈帧的创建和销毁解析
1. 什么是函数栈帧
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。那函数是如何调用的?函数的返回值又是如何带回的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
- 函数参数和函数返回值
- 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
- 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
2. 理解函数栈帧能解决什么问题呢?
理解函数栈帧有什么用呢?
只要理解了函数栈帧的创建和销毁,以下问题就能够很好的额理解了:
- 局部变量是如何创建的?
- 为什么局部变量不初始化内容是随机的?
- 函数调用时参数时如何传递的?传参的顺序是怎样的?
- 函数的形参和实参分别是怎样实例化的?
- 函数的返回值是如何带回的?
让我们一起走进函数栈帧的创建和销毁的过程中。
3. 函数栈帧的创建和销毁解析
3.1 什么是栈?
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
在我们常见的i386或者x86-64下,栈顶由成为 esp
的寄存器进行定位。
3.2 认识相关寄存器和汇编指令
相关寄存器
相关寄存器 | 作用(简介) |
---|---|
eax | 通用寄存器,保留临时数据,常用于返回值 |
ebx | 通用寄存器,保留临时数据 |
ebp | 栈底寄存器 |
esp | 栈顶寄存器 |
eip | 指令寄存器,保存当前指令的下一条指令的地址 |
相关汇编命令
相关汇编命令 | 作用(简介) |
---|---|
mov | 数据转移指令 |
push | 数据入栈,同时esp栈顶寄存器也要发生改变 |
pop | 数据弹出至指定位置,同时esp栈顶寄存器也要发生改变 |
sub | 减法命令 |
add | 加法命令 |
call | 函数调用,1. 压入返回地址 2. 转入目标函数 |
jump | 通过修改eip,转入目标函数,进行调用 |
ret | 恢复返回地址,压入eip,类似pop eip命令 |
3.3 解析函数栈帧的创建和销毁
3.3.1 预备知识
首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁。
-
每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
-
这块空间的维护是使用了2个寄存器:
esp
和ebp
,ebp
记录的是栈底的地址,esp
记录的是栈顶的地址。
如图所示:
- 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2019为例。
3.3.2 函数的调用堆栈
演示代码:
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 3;
int b = 5;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
这段代码,如果我们在VS2019编译器上调试,调试进入Add函数后,我们就可以观察到函数的调用堆栈(右击勾选【显示外部代码】),如下图:
函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main
函数调用之前,是由 invoke_main
函数来调用main
函数。
在 invoke_main
函数之前的函数调用我们就暂时不考虑了。
那我们可以