目录
一、什么是函数栈帧
二、什么是栈
三、相关寄存器
四、相关汇编命令
五、 解析函数栈帧的创建和销毁
一、什么是函数栈帧
在每一次函数调用之前编译器都会提前在内存的栈区为被调用的函数开辟一块空间,这块空间被称为该函数的函数栈帧,这些空间是用来存放:
① 函数参数和函数返回值
② 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
③ 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)
二、什么是栈
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈。压栈操作使得栈增大,而弹出操作使得栈减小。栈区开辟内存时习惯先使用高地址再使用低地址。
三、相关寄存器
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
eip :指令寄存器,保存当前指令的下一条指令的地址
esp:栈顶寄存器
ebp:栈底寄存器
说明:ebp 和 esp 这两个寄存器中存放的是地址,这两个寄存器是用来维护函数栈帧的,ebp 叫栈底指针,用于记录栈底的地址,esp 叫栈顶指针,用于记录栈顶的地址。
四、相关汇编命令
push:压栈,同时esp栈顶指针要向低地址移动4字节
pop:出栈,同时esp栈顶寄存器也要向高地址移动4字节
mov:数据转移指令,将第二个参数的值赋给第一个参数
sub:减法命令
add:加法命令
call:将当前指令的下一条指令的地址压栈,并进入被调函数
jump:通过修改eip,回到主调函数函数
ret:相当于于pop eip
五、 解析函数栈帧的创建和销毁
//以下列代码为例,在VS2013中,讲解函数栈帧的创建和销毁过程
//注意:
//① 在不同的编译器下,函数栈帧的创建和销毁过程会有差异,但大体逻辑相同
//② 在VS2022等比较新的版本中,看不到细节,所以建议用更老的版本观察
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;
}
Ⅰ. 通过打开[调试] -- [窗口] -- [调用堆栈],并调试代码可以发现,在VS2013中main函数其实也是被其他函数调用的,具体:main 函数被__tmainCRTStartup函数调用,__tmainCRTStartup函数是被 mainCRTStartup 函数调用的
Ⅱ. F10调试到main函数开始执行的第一行,右击鼠标点击反汇编,即可观察源代码的汇编代码。
注意:VS编译器每次调试都会为程序重新分配内存,下方截图的反汇编代码是一次调试代码过程中数据,每次调试略有差异。
解析上图:栈区的开辟是从高地址向低地址开辟的,函数内部为局部变量开辟空间也是由高地址向地址开辟的,但数组内部给每个元素开辟空间的顺序是从数组低地址向高地址开辟的。由此有了如图从高地址向低地址依次开辟函数栈帧的顺序排列,
mainCRTStartup --> __tmainCRTStartup --> main --> Add,值得注意的是,栈底指针ebp和栈顶指针esp不能同时维护多个栈帧空间,只能按秩序一个个维护。
函数栈帧的创建和销毁大概过程:① 将主调函数的ebp栈底指针压栈,以便之后pop此处后ebp栈底指针回到主调函数栈底指针原来的位置 ② 为被调用的函数开辟栈帧空间,并初始化栈帧空间所有地址内部的值为cccc...这样一些随机的值③ 给函数内部的局部变量按从高地址到低地址的顺序随机分配内存空间,并初始化局部变量的值 ④ 如果被调用函数需要传参,则会在进入被调用函数之前,先将实参从右至左拷贝一份依次压栈 ⑤ 通过call汇编指令,先将主调函数当前下一行的地址压栈,以便被调函数执行完后返回此处,继续运行下一行的指令,压栈后就会进入到被调用函数的内部,接下来的步骤和上面①②③一样,如果需要用到形参里的值,则会利用指针的偏移量找到形参的位置,再进行相关运算,返回的结果最终会存放在寄存器eax中,接下来就会进行一系列的出栈操作,以便将开辟的空间释放,在此处我们可以观察到,形参的销毁是在函数调用完后才销毁的,然后才将函数调用后返回的值存入主调函数的相关变量中。
通过学习函数栈帧可以回答一些之前学习过程中疑问:
1. 局部变量是如何创建的?
答:首先为被调用函数开辟栈帧空间,再为栈帧空间内部各地址中的值初始化CCCC...,然后才为函数内部的局部变量分配空间(从高地址向低地址利用),并存入初始化的值。
2. 为什么局部变量不初始化的时候会是随机值?
答:在为被调用函数创建栈帧空间的同时,也已经通过相关操作初始化了栈帧空间内部每个地址内部存放的值。如果局部变量写代码时就已经初始化了,原本存放在相关地址内的随机值就可以被覆盖。
3. 函数是怎么传参的,传参顺序是怎么样的?
答:在还没进入被调用函数内部前,就已经通过push把函数调用的参数从右向左拷贝一份了,当进入到被调用函数内部,会通过指针的偏移量找到形参所在的位置利用其内部的值。
4. 形参和实参的关系?
答:形参确实是实参的一份临时拷贝,改变形参不会影响实参。
5. 返回值是如何带回来的?
答:通过寄存器eax带回来的,是由于寄存器存在于CPU中不会被销毁。