什么是函数栈帧
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。 那函数是如何调用的?函数的返回值又是如何返回的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中程序的调用栈(call stack)所开辟的空间,这些空间是用来存放: 1. 函数参数和函数返回值 2. 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量) 3. 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
在不同的编译器下,函数调用的过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
每一个函数调用都要在栈区穿件一个空间
寄存器
寄存器是 CPU 内部用来存放数据的一些小型 存储区域 ,用来暂时存放参与运算的数据和运算结果。 其实寄存器就是一种常用的 时序逻辑电路 ,但这种时序逻辑电路只包含存储电路。
有:eax ebx ecx edx ebp esp
ebp与esp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。
举例说明
我们可以深入探究下函数调用在内存空间中到底是怎么运转的
我们可以以下面代码为例来分析
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
首先我们先建立main函数的栈帧空间,但我们思考一下,main函数是不是也有可能被其他函数调用那。
我们可以看出在vs2013中mainCRTStartup调用_tmainCRTStartup,而_tmainCRTStartup调用main,可见main函数也是被调用的。
首先,创建一个_tmainCRTStartup函数栈帧,我们假设栈区下面为高地址,上面为地地址。
esp为栈顶指针,ebp为栈底指针
这样我们就可以进入,我们通过汇编代码可以看出第一步为push。
push为压栈操作,push的目标是ebp,所以压栈ebp,压完元素之后,esp移动到新的栈顶。压栈:给栈顶放一个元素进去 。出栈:从栈顶删除一个元素。
下一步是move,将ebp移动到esp
下一步是sub,sub是减的意思,意思是将esp减去0E4h(16进制数字),相当于往低地址方向移动
0E4h地址,此刻esp与ebp围成的紫色部分就是main函数的栈帧
然后是三个push分别将ebx esi edi从栈顶压入,最终esp移动到edi的上方
从lea到rep,这几步总的来说是将main函数栈帧里面都初始化“ccccccccccc”
以上就是main函数栈帧创建,接下来就是把值放进去,int a=10,dword是双字节的意思,将a的值放在ebp-8这个空间里
接下来就把b, c也像a一样分别放入对应的位置
接下来就是传参,将ebp-14h也就是b的空间放入eax寄存器里面,再push一下放入栈顶
再传a,a也一样的道理
接下来就是call指令,call存放的是下一个指令的地址,方便函数返回时直接跳到下一指令
这下算是进入Add函数了创建Add函数栈帧与那main一样 先push ebp将main函数栈底指针地址通过这个元素储存起来方便返回时能找到main函数栈底指针
再move,sub将esp和ebp定义新的位置,再push三个元素ebx,esi,edi,最后再将Add函数栈帧初始化“CCCCCCCCC”
接着给z创建空间,ebp-8的位置
然后就是将传过去的b和a加起来储存在eax寄存器里面
接着将eax里面的值移动到z空间里(ebp-8),此时z空间的值是30,再将这个值放入eax寄存器中,这一步防止函数栈帧销毁时数据流失,所以将值保存在eax中。
调用完就开始返回了,pop意思是跳出 ,把这三个元素先跳出
再将esp返回到ebp的位置
此刻esp指向的是我们先前放进的ebp在main函数底栈时的地址,把当时ebp在main函数底栈位置读取用pop,ebp又指向回了main函数的栈底,而esp继续停留这个位置
接着是ret指令,意思是返回到main函数,返回到call指令,而call指令储存的是下一个指令的地址,所以直接返回main函数call指令下一个指令也就是a传参的空间。此刻esp指向的就是a传参的空间。
然后esp+8,跳出俩传参的空间
再三个pop,将edi,esi,ebx跳出去,到达main函数的空间。
最后将承载着z的值也就是两数和的值的寄存器eax,将值付给ebp-20h也就是c的地址
此时c就为30了
结论
局部变量是怎么创建的
创建好函数栈帧后,我们初始化一部分函数空间,而局部变量就在这个空间里分配一个空间,从而创建了局部变量
为什么局部变量的值是随机值
因为随机值是在我们创建函数栈帧时放进去的,函数空间里都是随机值,所以一定要初始化。
函数是怎么传参的,函数传参的顺序是什么
我们通过push将两个实参压栈,从而栈顶有了两个独立空间,将两个值放进去,创建好调用的函数栈帧后,通过指针的偏移量,实现传参。传参顺序从从右向左
形参和实参是什么关系
形参是实参的临时拷贝
函数调用结束后怎么返回的
我们通过push将当时edp在主函数栈底的地址压栈到一个空间,当我们返回指向这个空间是就能读取到主函数栈底的位置,再读取通过call指令存放下一个指令的地址,就直接返回主函数的栈帧里,返回值是通过寄存器存储,保护数据在调用的函数栈帧销毁时不丢失,再通过寄存器将值放入对应的主函数空间