C语言:那些不为人所知的函数栈帧的创建和销毁(底层知识)_帧在c语言中的三种

}


 


**目录**


[1.基础理解](#1.%E5%9F%BA%E7%A1%80%E7%90%86%E8%A7%A3%C2%A0 "1.基础理解 ")


[1.1 寄存器](#1.1%20%E5%AF%84%E5%AD%98%E5%99%A8 "1.1 寄存器")


[1.2 函数](#1.2%20%E5%87%BD%E6%95%B0 "1.2 函数")


[1.3 栈帧](#1.3%20%E6%A0%88%E5%B8%A7%C2%A0 "1.3 栈帧 ")


[2.main函数栈帧的创建](#%C2%A02.main%E5%87%BD%E6%95%B0%E6%A0%88%E5%B8%A7%E7%9A%84%E5%88%9B%E5%BB%BA "2.main函数栈帧的创建")


[3.main函数栈帧的初始化](#3.main%E5%87%BD%E6%95%B0%E6%A0%88%E5%B8%A7%E7%9A%84%E5%88%9D%E5%A7%8B%E5%8C%96 "3.main函数栈帧的初始化")


[4.函数的调用](#4.%E5%87%BD%E6%95%B0%E7%9A%84%E8%B0%83%E7%94%A8 "4.函数的调用")


[5.函数的销毁](#5.%E5%87%BD%E6%95%B0%E7%9A%84%E9%94%80%E6%AF%81 "5.函数的销毁")


[6.问题解答](#6.%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94 "6.问题解答")


[3.函数是怎么传参的?传参的顺序是怎样的?形参和实参是什么关系?](#3.%E5%87%BD%E6%95%B0%E6%98%AF%E6%80%8E%E4%B9%88%E4%BC%A0%E5%8F%82%E7%9A%84%3F%E4%BC%A0%E5%8F%82%E7%9A%84%E9%A1%BA%E5%BA%8F%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%3F%E5%BD%A2%E5%8F%82%E5%92%8C%E5%AE%9E%E5%8F%82%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB%3F "3.函数是怎么传参的?传参的顺序是怎样的?形参和实参是什么关系?")


[4.函数调用是怎么做的?](#4.%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E6%98%AF%E6%80%8E%E4%B9%88%E5%81%9A%E7%9A%84%3F "4.函数调用是怎么做的?")


[5.函数调用是结束后怎么返回的?](#5.%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E6%98%AF%E7%BB%93%E6%9D%9F%E5%90%8E%E6%80%8E%E4%B9%88%E8%BF%94%E5%9B%9E%E7%9A%84%3F "5.函数调用是结束后怎么返回的?")




---


## 1.基础理解


#### 1.1 寄存器


电脑中主要的本地存储方式有寄存器、内存、硬盘。硬盘最大,读取速度最慢。而寄存器的空间很小,但是速度是最快的,并且寄存器是集成在cpu上面的。它们可用来暂存指令、数据和地址。



> 
> 通俗一点解释就是:
> 
> 
> 寄存器就是你的口袋。身上只有那么几个,只装最常用或者马上要用的东西。
> 
> 
> 内存就是你的背包。有时候拿点什么放到口袋里,有时候从口袋里拿出点东西放在背包里。
> 
> 
> 硬盘就是你家里的抽屉。可以放很多东西,但存取不方便。
> 
> 
> 


 本文我们研究的寄存器主要是:**ebp esp**


#### 1.2 函数


C语言是由函数为单元模块构成的,每一次我们用C语言编写程序的时候,都需要写一个主函数main()。我们了解函数栈帧的创建和销毁,其实就在研究一个代码是如何实现的。


我们需要用到的是 **汇编代码 。**


#### 1.3 栈帧


C语言中,每个栈帧对应着一个**未运行完的函数**。栈帧中保存了该函数的返回地址和局部变量。栈帧也叫**过程活动记录**,是编译器用来实现过程/函数调用的一种数据结构。首先应该明白,栈帧是存放在内存中的**栈区**的。栈帧是**栈区**分配给进程的**内存区**。


![](https://img-blog.csdnimg.cn/adbc56521dc24b74b7014daafe3d06c8.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 


##  2.main函数栈帧的创建


![](https://img-blog.csdnimg.cn/fc1d097f01e0484bb9bb15039b9a4a5f.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


当我们创建了一个main函数,ebp和esp寄存器里面存的就是这个函数的首位地址,用来维护这一块空间。


我们在进入这个程序的时候,首先需要**调用main函数**,你没听错,就是调用它。那么是谁调用的main函数呢?它是被一个简称叫做CRT的函数所调用的。


以VS2019为例,我们转到反汇编


![](https://img-blog.csdnimg.cn/69459e2d6d6d4cb4b7499a768cfc1b9c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 可以看到这个seh函数调用了SRT函数(这里说法可能有误有错误请及时指出)![](https://img-blog.csdnimg.cn/2614e067ba374011b42749dd87d96d3b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 然后SRT函数再调用了main函数(这里说法可能有误有错误请及时指出)![](https://img-blog.csdnimg.cn/e964790297c04c338fdc901d4f034a17.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 


用图表示出来就是esp和ebp在维护函数CRT的空间。我们接下来再看了解主函数是如何创建函数栈帧的。


![](https://img-blog.csdnimg.cn/b9212ee8ad82423287e77b92813a8220.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


![](https://img-blog.csdnimg.cn/1b444a05f5a94917b86ab42e5cd7d722.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_17,color_FFFFFF,t_70,g_se,x_16)


**这里的push代表的是压栈**,就是将ebp放到栈顶。并且esp的指针也要向低地址移动。此时变成了:![](https://img-blog.csdnimg.cn/d8c1a5bebad04a45b27992eb82d73ff0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)



 接着我们看下一条指令:


![](https://img-blog.csdnimg.cn/240536ecf80e42a1bc039ad79bce43e9.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_18,color_FFFFFF,t_70,g_se,x_16)


mov是move的缩写,意思是将esp赋值给ebp,实质是将esp里面的地址放到ebp里面。(也就是两个指针指向了同一位置)


![](https://img-blog.csdnimg.cn/a27c3755ab304257af54750b81434876.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)这里提一句:此时ebp和esp没有在维护CRT函数了,但他的函数栈帧没有销毁,因为栈空间的使用只能先销毁低地址,再销毁高地址。即图中的从上往下销毁。


 我们接下来看第三条指令:


![](https://img-blog.csdnimg.cn/73aebf3df6884a2a96592c369ee9ae1b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_19,color_FFFFFF,t_70,g_se,x_16)


 sub是减法的缩写,意思是将esp这个地址减去E4这个16进制数字。E4转化为十进制就是228,那么就是说将esp从高地址向低地址移动228以后存放起来。变成了如下图:


![](https://img-blog.csdnimg.cn/a3108f2eec69495ca603218e7717d1ad.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 


接下来看第4,5,6条指令:


![](https://img-blog.csdnimg.cn/294e430de6b0405fafc19e892dc210b1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_18,color_FFFFFF,t_70,g_se,x_16)


 这就是把ebx、esi、edi放到栈顶。esp也会对应着变化。至此一个main函数的栈帧就创建成功了


![](https://img-blog.csdnimg.cn/547bec939bbb4d389c0c38eae1199737.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 


## 3.main函数栈帧的初始化


我们往后面看四个指令,lea指的是load efficitve address,即加载有效地址,把后面的地址给edi。


mov就是move的意思,看到第三条和第四条指令,就是把ebp到edi之间的空间全部初始化成为


cc cc cc cc,这些 cc cc cc cc是随机值,被初始化的空间大小为24(16进制)。


dword即double word,就是4个字节。![](https://img-blog.csdnimg.cn/3b60734e4a2a43409365adc212f56cc7.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 ![](https://img-blog.csdnimg.cn/0f5c8ba91b8943258fbfe655b9448c9a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 ![](https://img-blog.csdnimg.cn/a1ac25c7968841b7a13e1ee5110ae9a1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 


 可以很明显的看到,ebp到edi这一部分被初始化成为了cc cc cc cc(随机值)。


![](https://img-blog.csdnimg.cn/2d30f58104674a3dbd1dc8e7d7cb634a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


main函数栈帧开辟完了,接下来就是执行代码了。



## 4.函数的调用


![](https://img-blog.csdnimg.cn/3efa4f97386643e6b0d01bd061e53ae5.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 接着往下看,这是一个move指令,意思是把这个地址存放到ecx中去,就是将返回的地址存放起来,等会再回访这个地址执行下一条指令。


![](https://img-blog.csdnimg.cn/925ccfbc37e644c3a595e62e6a8fbdef.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)  接下来就是a和b的初始化。把0A(16进制数)存放到ebp-8的地址中间去,把14(16进制数)存放到epb-14的地址中去。这样子a和b的值就存放到内存中去了。


![](https://img-blog.csdnimg.cn/3dbbfb7aa2d8461ebe50f8cf17913096.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 这里a和b存放的位置取决于编译器,相隔多远也取决于编译器。


综上:局部变量a,b,c的创建就是先创建一个函数栈帧,然后在里面找到


一块空间,再把a,b,c放进去。


![](https://img-blog.csdnimg.cn/0c1bf2b87d3c4a6e8e244721aead78f0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


紧接着就要进行传参数的操作了。eax、ecx在此之前是没有出现过的寄存器,存放的是


ebp-14h的值,也就是b的值20,接着push一下。ecx也是同理,就变成了这样子(也可以看到函数传参的时候是先传递后面的参数)![](https://img-blog.csdnimg.cn/865278cec4354371a6dac404ed1b5922.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 至此,我们把调试的f10按钮改成按f11按钮,进入ADD函数的反汇编代码中,我们仍然一步一步的进行解析。我们看到了一个call:


![](https://img-blog.csdnimg.cn/0eb8dcb471a443f8b1c8b52e21266fe4.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 


这个call指令是干什么的呢?call指令call了一个地址,这个地址是什么呢?


其实这个地址是执行完ADD函数以后返回来的一个地址,到时候ADD函数销毁以后直接找到这个地址就能继续完成代码。![](https://img-blog.csdnimg.cn/f00571a0ebe545069eec8d090d69e339.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 再次按下f11,反汇编代码就进入了ADD函数的内部,我们一步一步来看:


![](https://img-blog.csdnimg.cn/28894705a6b94088af28396e8f3e7dab.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 这个ebp就是main函数的ebp,此时变成了这样子:


![](https://img-blog.csdnimg.cn/6d7bad1f91034ba697be1a10d413b2d1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 经过之前的一系列操作以后,又出现了跟main函数相同的内容:把esp的值赋给ebp,esp向上移动0CCh,再push ebx,esi,edi![](https://img-blog.csdnimg.cn/8586aaf8d8944620966329499702140c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


![](https://img-blog.csdnimg.cn/f8785da7180f4a98bbe70fba2616dc0a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 这样子ADD函数就开辟好了空间,之后的操作也是跟main函数里面是一样的 


![](https://img-blog.csdnimg.cn/2dacc0eae2634cb4a9556bf3b204524e.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


![](https://img-blog.csdnimg.cn/38c9be411f7e49df99f9b30ee140a4e3.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 接下来进入加法的部分


![](https://img-blog.csdnimg.cn/ec599a568d714941a9566411b03a58e5.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 这里很关键,ebp+8和ebp+0Ch,这两个地方并不是新开创的空间,而是之前a和b传递的值的拷贝,也就是![](https://img-blog.csdnimg.cn/5671cfa79a5d473f808cfe162da4f66f.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 也就是说传递的参数根本没有进入ADD函数里面去,而只是保存在寄存器里。


把两eax相加以后,再把eax的值放到ebp-8里面(也就是z)。这样子就完成了一次加法。


也可以证明,函数在调用的时候是自右向左传参的(ADD(a,b)先传b)。


同时再次证明了,**形参是实参的一份临时拷贝!**



## 5.函数的销毁


我们继续看反编译的代码:


![](https://img-blog.csdnimg.cn/6d3e914bbb794cb38d3e8905bd091780.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


这就是把30这个值放到eax这个全局的寄存器里面去,,然后接着的是三个pop(pop就是在栈顶弹出这个元素,可以理解为销毁): 


![](https://img-blog.csdnimg.cn/605aec94148a43eb8714da6b2f0c6c4a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_16,color_FFFFFF,t_70,g_se,x_16)


但是还有很大一块ADD开辟的空间还存在,怎么办呢?


![](https://img-blog.csdnimg.cn/43d2ff8cffd94387b0fc56b5391eb337.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_19,color_FFFFFF,t_70,g_se,x_16)


 很简单,只要把ebp赋给esp就行了。这时候ebp和esp指向同一个地址,上面的ADD开辟的空间就被销毁了。计算出来的值已经存到eax中了。


![](https://img-blog.csdnimg.cn/91d628d26ad846eea4a855ac2ff4432b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 紧接着:![](https://img-blog.csdnimg.cn/435e43f8ffd944448a9b1c4620ab0c33.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_18,color_FFFFFF,t_70,g_se,x_16)


 pop了ebp,这个ebp是main函数的ebp。main函数的栈顶是很容易找到的,栈底并不好找。于是可以把ebp存在原来main函数里面,先push再pop后ebp回到得地方还是main函数栈底的位置。这样子ebp一下子就回到了栈底的位置。


![](https://img-blog.csdnimg.cn/45c27fdd3f5943a58d350b9a525d6b64.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 之后出现了ret指令:


![](https://img-blog.csdnimg.cn/a013e069fab4464580a4dd251b312fb8.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_12,color_FFFFFF,t_70,g_se,x_16)


为什么要在栈顶存上一个call指令的下一条指令地址?其实ret指令返回的时候就是返回的call指令的下一条指令地址,那么就直接返回到了这个地址里面。![](https://img-blog.csdnimg.cn/d06575213b454357bdb1a251bb6ada59.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 因为执行完call后,会直接进入到ADD函数中去,等到ADD函数销毁了,程序进行到这一步时,就会自然而然访问地址里面。



 再往后就是eax的值存到了c中去![](https://img-blog.csdnimg.cn/5b224b1f944e47c3840708cfdc988d6e.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


 这样子,c的值就被打印出来了。


最后还要把形参销毁,这里销毁的方式比较简单,直接让esp加上8个字节,那么两个形参就不在esp和ebp的范围之内了。


![](https://img-blog.csdnimg.cn/0f26df1a2a694dccbab59c65848d9ca9.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWHVhMzA1NQ==,size_19,color_FFFFFF,t_70,g_se,x_16)


 


## 6.问题解答


**1.局部变量是怎么创建的?**


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值