C语言之函数栈帧(动图详解)

目录

1.什么是栈帧

2.相关寄存器和汇编指令

        1.相关寄存器

        2.部分汇编指令

3.程序介绍

4.过程分析(汇编角度)

        1.执行main函数

        2.形成Add()函数栈帧

        3.执行Add()函数

        4.Add函数栈帧释放与返回

5.总结


1.什么是栈帧

        C程序在调用函数时,会先在栈上给函数预先开辟一个足够的空间,后续函数中的内容,如非静态局部变量,返回值等都会保存在这段空间中。这段空间就叫栈帧

        当函数调用时,就会形成栈帧;当函数返回时,栈帧也会被释放。所谓释放,是指将某段空间设置为无效,使得可以被覆盖,而并非清空。

2.相关寄存器和汇编指令

        本期我们将通过汇编的角度来了解函数调用前后栈帧的形成与释放过程,首先我们先来认识以下一些相关的寄存器和可能用到的汇编命令:

        1.相关寄存器

寄存器名称 功能
eax 通用寄存器,保存临时数据,常用于返回值
ebx 通用寄存器,保存临时数据
ebp 栈底寄存器
esp 栈顶寄存器
eip 指令寄存器,保存当前指令的下一条指令的地址

        2.部分汇编指令

助记符 说明
mov 数据转移指令
push 数据入栈,同时esp栈顶寄存器也要发生改变
pop 数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub 减法指令
add 加法指令
call 函数调用,1.压入返回地址 2.转入目标函数
jump 通过修改eip,转入目标函数,进行调用
ret 恢复返回地址,压入eip,类似于pop eip指令

3.程序介绍

        下面就是本文研究的样例代码,是在vs2022下进行编译(不同编译器效果可能略有不同):

#include


int Add(int x, int y)    //求出两数和并返回
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 0xA;       //定义两个变量,用16进制
	int b = 0xB;
	int c = 0;
	c = Add(a, b);     //求和函数
	printf("%d", c);   //打印结果
	return 0;
}

 我们将分别从执行main函数内容,形成Add函数栈帧,执行Add函数内容,Add函数栈帧释放四个阶段来对Add()函数进行演示。

4.过程分析(汇编角度)

        1.执行main函数

        我们使用VS,进入调试,打开内存窗口,寄存器窗口,转到反汇编如下:

        C语言之函数栈帧(动图详解)_第1张图片

C语言之函数栈帧(动图详解)_第2张图片

        这里我们需要知道的是,main函数也是函数,它被_tmainCRTStartup函数调用,而_tmainCRTStartup又被mainCRTStartup函数调用,mainCRTStartup函数又是被操作系统所调用的。因此,main函数也会形成栈帧,定义a之前的汇编代码就是用来形成main函数栈帧并进行初始化的,与我们后续要讨论的Add函数的栈帧形成过程相同,这里就不加讨论。我们直接从int a=0xA之后开始分析:

C语言之函数栈帧(动图详解)_第3张图片

         我们F10运行到int a=0xA所对应的汇编指令,此时eip指向下一条要指向的指令地址,也就是int a所对应的指令:

C语言之函数栈帧(动图详解)_第4张图片

         根据ESP,EBP,我们得出main函数的栈帧在栈上的位置如下(地址从上到下递增,下同):

C语言之函数栈帧(动图详解)_第5张图片

        点击F10,执行指令,由于栈是从高地址向低地址增长的,因此在栈底向上8个字节处开辟a的空间并将数据mov进去。此时栈底寄存器不变,ebp-8处内存数据被改为0AH。如下:

C语言之函数栈帧(动图详解)_第6张图片

         同时,eip自动指向下一条要执行的指令,与a同理,将b,c变量入栈如下:

C语言之函数栈帧(动图详解)_第7张图片

        具体过程动图如下: 

C语言之函数栈帧(动图详解)_第8张图片

我们可以发现,变量与变量之间不是连续的 

         接下来就要开始执行下一条语句,调用函数并给c赋值,共有7条指令:

C语言之函数栈帧(动图详解)_第9张图片

         首先是调用Add()前(即call指令前)的4条指令,我们可以看出前两条指令的作用是先将变量b的值移动到eax寄存器,然后以压栈push的方式压入栈中,此时栈顶向上移动一个b变量大小:

C语言之函数栈帧(动图详解)_第10张图片

C语言之函数栈帧(动图详解)_第11张图片

         后两条指令也是如此,将a变量的值拷贝入ecx寄存器,然后压入栈中:

C语言之函数栈帧(动图详解)_第12张图片

        具体过程动图如下:

 C语言之函数栈帧(动图详解)_第13张图片

这两个临时变量的形成就是我们所说的形参实例化。我们发现形参实例化是在函数正式调用前就形成了(这里指call之前),形参实例化的顺序是按照参数列表从右向左。同时,我们可以发现,通过压栈形成的变量空间是连续的。

         接下来,我们将执行函数调用指令,由于我们是通过跳转指令修改eip寄存器转入目标函数地址,Add函数调用结束后还需返回main函数执行后续内容,因此我们需要将下一条指令的地址先保存起来,然后进行跳转。因此这个指令分为两步:1.将返回地址压入栈中 2.转入目标函数。

 C语言之函数栈帧(动图详解)_第14张图片

        点击F11进入函数,我们可以发现函数返回后的指令地址被压入栈中(即00EE18F7),然后修改eip进行跳转,转入Add()函数:

C语言之函数栈帧(动图详解)_第15张图片

C语言之函数栈帧(动图详解)_第16张图片

        压栈的具体过程动图如下:

C语言之函数栈帧(动图详解)_第17张图片  

 至此,我们进入了Add()函数内部,接下来就要开始准备形成Add()函数的栈帧。


        2.形成Add()函数栈帧

        前3条指令就是栈帧的形成过程,而后面几条是对栈帧的初始化与赋值,我们重点来解析一下前3条指令:

C语言之函数栈帧(动图详解)_第18张图片         首先是第一条指令,单击F10,将栈底寄存器的内容压入栈中,即把main函数栈底的地址压入栈中:

C语言之函数栈帧(动图详解)_第19张图片

         由于是压栈,栈顶向低字节偏移4个字节,保存main函数栈底的地址,具体动图如下:

C语言之函数栈帧(动图详解)_第20张图片

         然后是第二条指令,单击F10,将栈顶寄存器的内容移动到栈底寄存器,使得栈顶寄存器栈底寄存器指向同一个地址空间:

C语言之函数栈帧(动图详解)_第21张图片

        最后是第三条指令,单击F10,将esp栈顶寄存器的内容减去0CCH,使其向低地址偏移0CC个字节,如下:

C语言之函数栈帧(动图详解)_第22张图片

        第二条指令与第三条指令的具体过程动图如下:

C语言之函数栈帧(动图详解)_第23张图片

 由此我们便得到的一个大小为0CC个字节的空间,这个空间就是Add()函数栈帧。eap指向这个栈帧的栈顶,ebp指向这个栈帧的栈底。


        3.执行Add()函数

        我们略过初始化栈帧部分运行到int z=0所对应的指令处:

C语言之函数栈帧(动图详解)_第24张图片

         点击F10,与前面a,b变量相同,这条指令就是通过mov给z分配空间,将0放入栈底向上8个字节处:

C语言之函数栈帧(动图详解)_第25张图片

        具体过程动图如下:

C语言之函数栈帧(动图详解)_第26张图片

        单击F10,执行z=x+y,这条语句有三条指令,首先把ebp向下偏移8个字节的内容移动到eax寄存器中,然后将ebp向下偏移0CH(12)个字节的内容与eax寄存器中内容相加并存到eax寄存器中,最后将eax寄存器的内容存到ebp向上偏移8个字节处:

C语言之函数栈帧(动图详解)_第27张图片         我们不难发现,由于压栈得到的地址空间是连续的,而我们的栈底向下的几个空间都是通过压栈得到的。因此ebp+8就为临时变量x的地址,ebp+0CH就是临时变量y的地址。由此,我们就把x和y的值进行相加最后存入ebp-8中,也就是变量z中。

C语言之函数栈帧(动图详解)_第28张图片

C语言之函数栈帧(动图详解)_第29张图片         具体过程动图如下:

C语言之函数栈帧(动图详解)_第30张图片

        最后,执行return z语句,将ebp-8处的内容(即z)放入eax寄存器中:

C语言之函数栈帧(动图详解)_第31张图片

 C语言之函数栈帧(动图详解)_第32张图片

        具体过程动图如下: 

 C语言之函数栈帧(动图详解)_第33张图片

 至此,ADD函数调用完毕,进入最后一步,栈帧释放(销毁)。

        4.Add函数栈帧释放与返回

        栈帧的销毁我们重点来谈后三条语句,前几条语句对应着前面栈帧创建时的初始化操作,进行设置,我们不做深究 。

C语言之函数栈帧(动图详解)_第34张图片

         首先是第一条mov命令,我们单击F10运行,ebp栈底寄存器的值赋给esp栈顶寄存器,此时ebp与esp指向同一个地址空间: 

C语言之函数栈帧(动图详解)_第35张图片

        具体动图如下:

C语言之函数栈帧(动图详解)_第36张图片

实际上这时,Add函数的栈帧所在空间就被置为无效了,栈帧就被释放了 

         接下来就是恢复main函数栈帧的操作了。

        我们单击F10,执行下一条pop指令,将栈顶内容弹出并放入ebp栈底寄存器中,同时esp栈顶寄存器的指向发生改变。由于栈顶处内容为main函数栈底地址,因此pop操作完成后ebp就重新指向main栈帧的栈底。

C语言之函数栈帧(动图详解)_第37张图片

         具体过程动图如下:

C语言之函数栈帧(动图详解)_第38张图片         最后,执行ret指令,ret作用是恢复返回地址,压入eip,类似于pop eip指令,即把栈顶元素(保存的指令返回地址)弹出到eip指令寄存器中,改变下一条执行的指令。我们单击F10,发现返回到了main函数,此时eip的内容就是我们之前保存的下一条main函数指令地址,esp栈顶寄存器发生改变:

C语言之函数栈帧(动图详解)_第39张图片

         具体过程动图如下:

C语言之函数栈帧(动图详解)_第40张图片

         之后执行main函数中的下一条add指令,将esp栈顶寄存器的值加8并存回esp栈顶寄存器,此时esp向下偏移8个字节,指向原main函数栈顶:

C语言之函数栈帧(动图详解)_第41张图片

         具体过程动图如下:

C语言之函数栈帧(动图详解)_第42张图片

至此,main函数的栈帧恢复完毕, Add函数栈帧被释放,Add函数调用过程结束,进入main函数后续内容。

        最后,执行mov语句将存在eax寄存器的Add函数返回值赋值给变量c:

C语言之函数栈帧(动图详解)_第43张图片

          具体过程动图如下:

C语言之函数栈帧(动图详解)_第44张图片

以上就是Add()函数栈帧的创建和释放(销毁)的全过程。后续的printf()也是函数,与Add()函数也会创建函数栈帧,但是总体的步骤都是一样的,这里就不再说明。 


5.总结

通过以上分析,我们可以得出几点结论

1.函数正式调用(call)前会进行形参实例化,分配存储空间,形参实例化的顺序是从右向左。

2.临时空间的开辟,是在对应函数栈帧的内部通过mov命令的方式开辟的。

3.函数调用完毕,栈帧结构被释放。

4.临时变量具有临时性的本质是:栈帧具有临时性。

5.调动函数是有成本的,体现在时间和空间上,本质是形成和释放栈帧有成本。

6.函数调用,因拷贝而形成的临时变量,变量和变量之间的位置关系是有规律的。

7.函数的栈帧是自己形成的,esp减多少是由编译器决定的。即栈帧的大小是由编译器决定的。编译器有能力知道所有类型对应定义变量的大小。


 以上,就是本期的全部内容。

制作不易,能否点个赞再走呢qwq

你可能感兴趣的:(C语言学习打卡,c语言,开发语言,学习)