C语言函数栈帧的创建和销毁(逐步分析)

什么是函数栈帧

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。 那函数是如何调用的?函数的返回值又是如何返回的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

函数栈帧(stack frame)就是函数调用过程中程序的调用栈(call stack)所开辟的空间,这些空间是用来存放: 1. 函数参数和函数返回值 2. 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量) 3. 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

在不同的编译器下,函数调用的过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。

每一个函数调用都要在栈区穿件一个空间


寄存器

C语言函数栈帧的创建和销毁(逐步分析)_第1张图片

寄存器是 CPU 内部用来存放数据的一些小型 存储区域 ,用来暂时存放参与运算的数据和运算结果。 其实寄存器就是一种常用的 时序逻辑电路 ,但这种时序逻辑电路只包含存储电路。

有:eax   ebx    ecx     edx     ebp     esp

ebp与esp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。


举例说明

我们可以深入探究下函数调用在内存空间中到底是怎么运转的

我们可以以下面代码为例来分析

#define _CRT_SECURE_NO_WARNINGS 1
#include 
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函数是不是也有可能被其他函数调用那。

C语言函数栈帧的创建和销毁(逐步分析)_第2张图片

我们可以看出在vs2013中mainCRTStartup调用_tmainCRTStartup,而_tmainCRTStartup调用main,可见main函数也是被调用的。

首先,创建一个_tmainCRTStartup函数栈帧,我们假设栈区下面为高地址,上面为地地址。


esp为栈顶指针,ebp为栈底指针

C语言函数栈帧的创建和销毁(逐步分析)_第3张图片


这样我们就可以进入,我们通过汇编代码可以看出第一步为push。

C语言函数栈帧的创建和销毁(逐步分析)_第4张图片

push为压栈操作,push的目标是ebp,所以压栈ebp,压完元素之后,esp移动到新的栈顶。压栈:给栈顶放一个元素进去 。出栈:从栈顶删除一个元素。

C语言函数栈帧的创建和销毁(逐步分析)_第5张图片


下一步是move,将ebp移动到esp

C语言函数栈帧的创建和销毁(逐步分析)_第6张图片

 C语言函数栈帧的创建和销毁(逐步分析)_第7张图片


下一步是sub,sub是减的意思,意思是将esp减去0E4h(16进制数字),相当于往低地址方向移动

0E4h地址,此刻esp与ebp围成的紫色部分就是main函数的栈帧

C语言函数栈帧的创建和销毁(逐步分析)_第8张图片

C语言函数栈帧的创建和销毁(逐步分析)_第9张图片


然后是三个push分别将ebx esi edi从栈顶压入,最终esp移动到edi的上方

C语言函数栈帧的创建和销毁(逐步分析)_第10张图片

C语言函数栈帧的创建和销毁(逐步分析)_第11张图片


从lea到rep,这几步总的来说是将main函数栈帧里面都初始化“ccccccccccc” 

C语言函数栈帧的创建和销毁(逐步分析)_第12张图片

C语言函数栈帧的创建和销毁(逐步分析)_第13张图片


 以上就是main函数栈帧创建,接下来就是把值放进去,int a=10,dword是双字节的意思,将a的值放在ebp-8这个空间里

C语言函数栈帧的创建和销毁(逐步分析)_第14张图片

C语言函数栈帧的创建和销毁(逐步分析)_第15张图片


接下来就把b, c也像a一样分别放入对应的位置 

C语言函数栈帧的创建和销毁(逐步分析)_第16张图片

C语言函数栈帧的创建和销毁(逐步分析)_第17张图片


接下来就是传参,将ebp-14h也就是b的空间放入eax寄存器里面,再push一下放入栈顶

 C语言函数栈帧的创建和销毁(逐步分析)_第18张图片


再传a,a也一样的道理

C语言函数栈帧的创建和销毁(逐步分析)_第19张图片


接下来就是call指令,call存放的是下一个指令的地址,方便函数返回时直接跳到下一指令

C语言函数栈帧的创建和销毁(逐步分析)_第20张图片C语言函数栈帧的创建和销毁(逐步分析)_第21张图片 


这下算是进入Add函数了创建Add函数栈帧与那main一样 先push ebp将main函数栈底指针地址通过这个元素储存起来方便返回时能找到main函数栈底指针

C语言函数栈帧的创建和销毁(逐步分析)_第22张图片

C语言函数栈帧的创建和销毁(逐步分析)_第23张图片


再move,sub将esp和ebp定义新的位置,再push三个元素ebx,esi,edi,最后再将Add函数栈帧初始化“CCCCCCCCC”

C语言函数栈帧的创建和销毁(逐步分析)_第24张图片

C语言函数栈帧的创建和销毁(逐步分析)_第25张图片


接着给z创建空间,ebp-8的位置

C语言函数栈帧的创建和销毁(逐步分析)_第26张图片

C语言函数栈帧的创建和销毁(逐步分析)_第27张图片


然后就是将传过去的b和a加起来储存在eax寄存器里面

C语言函数栈帧的创建和销毁(逐步分析)_第28张图片

 C语言函数栈帧的创建和销毁(逐步分析)_第29张图片


接着将eax里面的值移动到z空间里(ebp-8),此时z空间的值是30,再将这个值放入eax寄存器中,这一步防止函数栈帧销毁时数据流失,所以将值保存在eax中

C语言函数栈帧的创建和销毁(逐步分析)_第30张图片


调用完就开始返回了,pop意思是跳出 ,把这三个元素先跳出

C语言函数栈帧的创建和销毁(逐步分析)_第31张图片


再将esp返回到ebp的位置

C语言函数栈帧的创建和销毁(逐步分析)_第32张图片


此刻esp指向的是我们先前放进的ebp在main函数底栈时的地址,把当时ebp在main函数底栈位置读取用pop,ebp又指向回了main函数的栈底,而esp继续停留这个位置

C语言函数栈帧的创建和销毁(逐步分析)_第33张图片

C语言函数栈帧的创建和销毁(逐步分析)_第34张图片


接着是ret指令,意思是返回到main函数,返回到call指令,而call指令储存的是下一个指令的地址,所以直接返回main函数call指令下一个指令也就是a传参的空间。此刻esp指向的就是a传参的空间。

 C语言函数栈帧的创建和销毁(逐步分析)_第35张图片


 然后esp+8,跳出俩传参的空间

C语言函数栈帧的创建和销毁(逐步分析)_第36张图片


再三个pop,将edi,esi,ebx跳出去,到达main函数的空间。

C语言函数栈帧的创建和销毁(逐步分析)_第37张图片


最后将承载着z的值也就是两数和的值的寄存器eax,将值付给ebp-20h也就是c的地址

 此时c就为30了


 结论

局部变量是怎么创建的

创建好函数栈帧后,我们初始化一部分函数空间,而局部变量就在这个空间里分配一个空间,从而创建了局部变量

为什么局部变量的值是随机值

因为随机值是在我们创建函数栈帧时放进去的,函数空间里都是随机值,所以一定要初始化。

函数是怎么传参的,函数传参的顺序是什么

我们通过push将两个实参压栈,从而栈顶有了两个独立空间,将两个值放进去,创建好调用的函数栈帧后,通过指针的偏移量,实现传参。传参顺序从从右向左

形参和实参是什么关系

形参是实参的临时拷贝

函数调用结束后怎么返回的

我们通过push将当时edp在主函数栈底的地址压栈到一个空间,当我们返回指向这个空间是就能读取到主函数栈底的位置,再读取通过call指令存放下一个指令的地址,就直接返回主函数的栈帧里,返回值是通过寄存器存储,保护数据在调用的函数栈帧销毁时不丢失,再通过寄存器将值放入对应的主函数空间

C语言函数栈帧的创建和销毁(逐步分析)_第38张图片

你可能感兴趣的:(c语言,开发语言)