【C语言】函数栈帧的创建和销毁

目录

1.函数栈帧的含义

概念 

要用到的汇编语言的知识

示例

2.理解栈帧

2.1 main函数栈帧的创建

2.2 局部变量的创建

2.3 函数传参

2.4 调用函数

2.5 函数返回 


        一个.c文件在调用函数的时候(包括main 函数),其内存中的栈区有什么变化?要压栈、出栈哪些寄存器呢?函数的参数是如何进行传递的呢?函数调用结束之后栈区又是如何变化的呢?本文通过使用汇编语言,对这些内容进行了较为详细的剖析。

1.函数栈帧的含义

概念 

        首先,栈的概念想必不需要过多解释,那么什么是栈帧?引用百度百科:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。从这句话中,可以提炼以下几点信息:

· 栈帧是一块因函数运行而临时开辟的空间。
· 每调用一次函数便会创建一个独立栈帧。
· 栈帧中存放的是函数中的必要信息,如局部变量、函数传参、返回值等。
· 当函数运行完毕栈帧将会销毁。

        我们知道,C语言的内存区分成了 静态区、栈区、堆区,函数栈帧无疑是在栈区创建和销毁的,所以其要符合栈“后进先出”的特点。

要用到的汇编语言的知识

        在这里使用汇编语言方面知识的原因是:通过它,我们可以深入底层了解一个程序是如何运行的,在何时——什么东西压栈,什么东西出栈,寄存器(汇编语言中一些用来暂时存储数据的东西)如何变化等等。这些都是C语言无法直观体现的,我们可以通过Visual Stdio 的在调试时的反汇编功能,将C语言代码转换成汇编语言代码,以便更好地观察。(另,C语言也是汇编语言编写的。)所以,简单地说,本文主要是在分析汇编语言的执行过程。

        我们首先要了解几个汇编语言方面的东西,其中ESP和EBP时专门维护函数栈帧的,分别指向栈顶和栈底:

寄存器  用途
EAX 累加寄存器:用于乘除法、函数返回值
EBX 用于存放内存数据指针
ECX 计数器
EDX 用于乘除法、IO指针
ESP 存放栈顶指针(其值是地址)
EBP 存放栈底指针(其值是地址)

汇编指令 用途
mov mov A,B 将数据B移动到A
push 压栈
pop 出栈
call 函数调用
add 加法
sub 减法
rep 重复
lea 加载有效地址

示例

        比如,我们写下一个如下的C语言程序,非常容易,只有main() 函数和一个 Add() 函数,主函数里面调用了 Add() 。

#include

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	return 0;
}

        那么,一开始调用主函数的时候,主函数的函数栈帧就压栈;然后在主函数里面,调用了Add() 函数,此时Add() 的函数栈帧也要压栈。那么现在面临一个问题,是维护Add() 函数,还是维护main() 函数,亦或两者都维护?
        其实,从平时使用Visual Stdio 调试的时候就可以看出来,当主函数内部调用一个函数A,按F11分步调试,会进入函数A的内部,函数A调用结束,会返回主函数。同理,实际上从进入函数A,一直到A 函数调用结束,这个过程都在维护函数A。所以,在main() 函数内部调用 Add() 函数之后,会出现如下图所示的情况,ebp和esp来维护Add() 函数的栈帧当Add() 函数调用结束,它的函数栈帧自然就会出栈,此时ebp、esp又会返回维护main() 函数的栈帧
【C语言】函数栈帧的创建和销毁_第1张图片

2.理解栈帧

2.1 main函数栈帧的创建

        实际上,main() 函数也是由其他函数调用的,其调用链条如下:

        创建main函数的函数栈帧代码如下,汇编语言的注释是用 ; 所以这里 ; 后面的内容是注释,帮助理解代码。

006117A0  push        ebp                 ; ebp 压栈
006117A1  mov         ebp,esp             ; 将 esp里面的值 赋给ebp
006117A3  sub         esp,0E4h            ; esp减去0E4h(十六进制),得到的结果赋给esp
006117A9  push        ebx                 ; ebx 压栈
006117AA  push        esi                 ; esi 压栈
006117AB  push        edi                 ; edi 压栈
006117AC  mov         edi,[ebp-24h]       ; 将ebp往上数,第24h(16进制)个字节开始,向下4个字节的值赋给edi
006117AF  mov         ecx,9               ; 这里到结束的意思是:
006117B4  mov         eax,0CCCCCCCCh      ; 将0CCCCCCCCh 赋值给某块空间,这块空间从附加段中 edi 指向的位置开始
006117B9  rep stos    dword ptr es:[edi]  ; 一共执行九次(0CCCCCCCCh 是四个字节的内容,每次操作四个字节,所以一共操作了36字节)

第一行 

        我们来开始逐句剖析上方代码,首先执行第一行(图中红色圆圈圈出来的黄色箭头,表示已经执行完其上一行,按F10调试就执行当前行),由于是压栈操作,所以esp的值会有所变化,如下图右边监视窗口,esp的值(十六进制显示的)相较之前改变了,所以变成红色:
【C语言】函数栈帧的创建和销毁_第2张图片

        如下,ebp压栈,同时esp上移:
【C语言】函数栈帧的创建和销毁_第3张图片

第二行

         该行是将esp的值赋给ebp,效果也如下图,右边监视窗口的红色部分所示。
【C语言】函数栈帧的创建和销毁_第4张图片

        如下,将esp的值赋给ebp之后,ebp和esp指向同一块地方:
 【C语言】函数栈帧的创建和销毁_第5张图片

第三行

        该行是将esp的值减去0E4h(十六进制),得到的结果赋给esp,如下图。
【C语言】函数栈帧的创建和销毁_第6张图片

         如图所示,由于图中从下往上是地址高处到地址低处,所以esp值变小,实际上图中是上移。并且,现在ebp和esp维护的空间,就是main函数的函数栈帧:
【C语言】函数栈帧的创建和销毁_第7张图片

第四行

        压栈,压入ebx,改变栈顶指针esp的值。
【C语言】函数栈帧的创建和销毁_第8张图片

        如下,压栈,esp上移:
【C语言】函数栈帧的创建和销毁_第9张图片

第五行

        压入esi,改变esp的值。
【C语言】函数栈帧的创建和销毁_第10张图片

        如下,和上一步类似:
 【C语言】函数栈帧的创建和销毁_第11张图片

第六行

        压入edi,改变esp的值。 
【C语言】函数栈帧的创建和销毁_第12张图片

        和上一步也类似:
【C语言】函数栈帧的创建和销毁_第13张图片

第七行

        将[ebp-24h] 表示的地址赋值给edi。
【C语言】函数栈帧的创建和销毁_第14张图片

        这里就是把 edi 里面的值改变,从函数栈帧看不出什么,看上面的监视图就可以直到确实是改变了。

最后三行

        如之前代码里的注释所说。
【C语言】函数栈帧的创建和销毁_第15张图片

【C语言】函数栈帧的创建和销毁_第16张图片

        如下,两个箭头指示的值是一样的,其代表的是edi所表示的地址,从该地址开始,往后9个dw(double word 双字,一个双字等于四个字节)的内容,都赋值为cccccccc (十六进制)。
【C语言】函数栈帧的创建和销毁_第17张图片

        这三行代码效果如下:
【C语言】函数栈帧的创建和销毁_第18张图片

        整个过程可以用一张动图生动形象地展示:

2.2 局部变量的创建

        接下来,我们在汇编代码中鼠标右击,然后将下图红色箭头所指示的"显示符号名" 的勾去掉。
【C语言】函数栈帧的创建和销毁_第19张图片

        发生改变的是下图中红色圆圈圈出来的,可以看出,原本所有的变量名,都变成了寄存器减去某个十六进制数字。他们实际上是等价的,即变量的地址就等于替换后的地址
【C语言】函数栈帧的创建和销毁_第20张图片

        接下来分析局部变量创建过程。
        首先,创建变量a,代码如下,其含义就是,将0Ah 这个十六进制数字,从ebp的地址低八位处开始放,占四个字节

002C17C5  mov         dword ptr [ebp-8],0Ah  

        如下图,可以通过两个红色箭头看到,右边监视的ebp的值就是左边 地址处的箭头指向的地址,说明这就是ebp的地址,然后减去八位,再根据栈从下往上使用以及Visual Stdio小端存储的特点,就成了内存区里面红色方框框出来的内容。(注意,比如 cc cc cc cc cc占据的是一个字节的空间,四个cc 就占据四个字节,而汇编语言中,地址-1,只跳过一个c,所以ebp-8是跳过8个c,即四个字节)
【C语言】函数栈帧的创建和销毁_第21张图片

        如下,图中一个小格子代表四个字节,不难看出,变量a存储的位置,在栈底指针往上跳过四个字节的地方。
【C语言】函数栈帧的创建和销毁_第22张图片

        然后创建局部变量b,通过内存图可以看出,变量b和变量a是间隔八个字节的:
【C语言】函数栈帧的创建和销毁_第23张图片

        如下图:
【C语言】函数栈帧的创建和销毁_第24张图片

2.3 函数传参

        代码如下:

002C17D3  mov         eax,dword ptr [ebp-14h]   ; 将变量b的值赋给eax
002C17D6  push        eax                       ; eax压栈
002C17D7  mov         ecx,dword ptr [ebp-8]     ; 将变量a的值赋给ecx
002C17DA  push        ecx                       ; ecx压栈

        执行前两行代码,确实将变量b的值赋给了eax,然后eax引起的压栈导致了esp改变:
【C语言】函数栈帧的创建和销毁_第25张图片

        如下图,不要忘了eax里面的值和变量b是一样的哦:
【C语言】函数栈帧的创建和销毁_第26张图片

        执行后两行代码:
【C语言】函数栈帧的创建和销毁_第27张图片

        其效果和前两行类似,同时ecx里面存的是变量a的值:
【C语言】函数栈帧的创建和销毁_第28张图片

2.4 调用函数

        上面的内容执行完之后,要执行如下语句,其意思是,执行 002C10B4 地址处的内容:

002C17DB  call        002C10B4  

        然后我们将其滑倒该地,发现是这样的,意思是跳到002C1740地址处:

         又找到该地址,发现如下,所以,通过这两步调用Add() 函数,如下红色部分,和创建main函数的函数栈帧类似,实际上就是创建了Add() 的函数栈帧
【C语言】函数栈帧的创建和销毁_第29张图片

        效果如下,建立了Add函数的函数栈帧:
【C语言】函数栈帧的创建和销毁_第30张图片

        红色个方框后面两行代码不是很重要,是用来检查bug的,如下代码和图片:

002C1757  mov         ecx,2CC003h  
002C175C  call        002C130C  

 

【C语言】函数栈帧的创建和销毁_第31张图片

2.5 函数返回 

        函数返回

002C1761  mov         eax,dword ptr [ebp+8]   
002C1764  add         eax,dword ptr [ebp+0Ch]  

        第一行代码: 将ebp+8 地址处的数据放到eax 。

        第二行代码:将ebp+0Ch 地址处的数据和eax相加,结果存到eax里面。

        如下图中,由于图片从下往上是地址从高到低,所以图片中ebp+8是在ebp下方。实际上就是ecx和eax的值相加,然后存到eax里面。eax里面存储变量b的值,ecx里面存储变量a的值,最后eax的值就是变量a、b之和。并且eax是不会随着Add() 函数的函数栈帧销毁而改变值。
【C语言】函数栈帧的创建和销毁_第32张图片

        通过监视也可以看出,eax的值变成0x0000001e,转换成十进制就是30。
【C语言】函数栈帧的创建和销毁_第33张图片 
        此时已经拿到返回值,存储在eax里面,还要执行以下几行代码:

00AA13F1  pop         edi  
00AA13F2  pop         esi  
00AA13F3  pop         ebx  
00AA13F4  mov         esp,ebp  
00AA13F6  pop         ebp  
00AA13F7  ret  

         就是出栈、赋值等等,结果如下,回到了调用Add() 函数之前的状态:
【C语言】函数栈帧的创建和销毁_第34张图片

        然后执行main() 函数后续代码代码,如下图红色框出:
【C语言】函数栈帧的创建和销毁_第35张图片

        第一行:esp加8,即esp在途中向下移动四个字节。

        第二行,将eax的值赋给ebp-20h 地址处。

        执行完之后,调试图如下,通过对比两个红色方框的内容,左边红色方框的地址,和右边&c 的值一样,说明那就是变量c 存储的地方,其值也是变量c 的值:
【C语言】函数栈帧的创建和销毁_第36张图片

        示意图如下:
【C语言】函数栈帧的创建和销毁_第37张图片

        通过对函数栈帧创建、销毁过程的剖析使我们不仅了解计算机做了什么,还了解了它是如何做的。通过函数栈帧尝试解析递归等问题相信也会更加直观。由于本人水平有限,不足之处还请大家多多指教。

你可能感兴趣的:(加深对计算机的理解,C语言学习之路,开发语言,c语言)