函数栈桢的创建和销毁
❤️ :热爱编程学习,期待一起交流。 🙏:博主是河牧院大一在读学生,水平有限,如发现错误,期待指点!(2466200050)
🌳:以下是我对main函数栈帧的创建与销毁一些拙见,期待大佬们指教。
前言
了解函数栈桢的目的是提高我们的基础知识功底,了解反汇编和内存,而不是会printf一个hello world,但不知道他是如何实现的,知其所以然。虽然学了这些并不能提高我们的算法能力,但看看到底是怎么运行的,有助于后期的学习,使我们向看代码是内存这个境界更近一步。
正如图上达克效应一样,横坐标代表技术。纵坐标代表自信。我们并没有想象中的那么优秀,要学会从他人眼里看到自己的影子。也许你现在处于愚昧之巅,也许你处在绝望之谷,不过这都是正常的,只要我们认清自己,持续输出,一定会走向开悟之坡。
C语言是由函数构成的
C语言是由函数为单元模块构成的,不信的话,可以想象一下,每一次我们用C语言编写程序的时候,都需要写一个main()。而main()就是一个函数。
就拿最简单的打印一句hello world来说,不就是在main函数里面的的吗。
所以我们了解函数栈帧的创建和销毁,其实就在研究一个代码是如何实现的。
我们在这里要用到汇编代码来演示函数栈帧的创建与销毁。
栈帧概念
C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
首先应该明白,栈帧是存放在内存中的栈区的。栈帧是栈区分配给进程的内存区。
栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
寄存器
ebp:栈底指针
esp:栈顶指针
esp和ebp是用来维护函数的。
寄存器有很多,但这里我们主要用到一下两种寄存器,ebp和esp。
这两个寄存器存放的是地址。用来维护函数栈帧。所以我们可以把它俩个可以叫做指针。指针不就是用来存放地址的嘛。
我们平时用C语言写的每一个程序都包含了函数,而每一个函数的调用,都要在栈区创建一个空间。这个空间区域就叫做函数栈帧。而ebp和esp就是用来维护这片区域的。
hello world是如何实现
这里我们需要明白
main函数是一个程序的入口,main函数有且只能有一个。
我们在进入这个程序的时候,首先需要调用main函数,你没听错,就是调用它
那么是谁调用的main函数呢?它是被一个简称叫做CRT的函数所调用的。(CRT是我自己命名的简称,我只要知道main函数是被它调用的就好了。)
#include<stdio.h> int main() { printf("hello world"); return 0; }
- 看到这段在main函数里面的代码。有人就乐了,不就是一个printf就可以打印出来了吗?
- 是的,没错,的确是一个printf就打印出来了。
- 但是在执行printf这句语句的时候,编译器要做大量的工作。这里的工作就是指函数栈帧的创建
我们转到汇编代码
- 这里我用的是vs2019编译器下转到反汇编查看的。
- 红色的框就是ebp指针和esp指针从维护CRT函数转变到维护main函数的过程。
- 绿色的框就是我们程序代码。
🌳 main函数栈帧的创建(开始调用main函数)
下面是esp和ebp维护CRT函数栈帧的图片
首先进行第一条指令
- push的意思就是压栈,就是将ebp放到栈顶。
- 注意:这是为了将来main函数返回之后,esp寄存器和ebp寄存器还能来回来维护CRT函数的,这里push的ebp就是为以后调用完返回CRT函数埋下的伏笔。
* 压栈的同时esp指针也会随着向上走。CRT函数的栈帧也随之增加
* 接着执行第二条指令
mov是move的缩写,意思是将esp赋值给ebp,实质是将esp里面的地址放到ebp里面。(也就是两个指针指向了同一位置)
注意:此时ebp和esp没有在维护CRT函数了,但他的函数栈帧没有销毁,因为栈空间的使用只能先销毁低地址,再销毁高地址。即图中的从上往下销毁。
- 第三条指令
- sub是减法的英文。意思是将esp这个地址减去C0这个16进制数字。C0其实就是十进制的192.可以在电脑上打开计算器,切换为程序员模式进行计算。(esp-192)
- h是一个标识符,不用管它
- * esp-192就是esp指针从低地址移向了高地址。而此时就呈现出来了一个main函数栈帧的雏形。ebp和esp之间的就是他的雏形。
* 接下来执行第四,五,六条指令。(可以不用管这三条指令)
- 这里是连续三次(push)压栈,为了保存这三个寄存器。压栈的同时,esp指针位置随之上升。main函数的栈帧也随之扩大。
- 到这里main函数的函数栈帧初步创建成功。
🌳 main函数栈帧的初始化
- 这四步骤就是将ebp到esp之间的空间初始化为cc cc cc cc。
- 这些cc cc cc cc是随机值。
* 然后执行这两条命令
- 这里的call是调用的意思。就是调用printf函数来打印hello world。这里我就不赖细究printf函数怎么调用了。触及到博主目前知识水平极限了。
- 保存002e1339这个地址的作用是为了调用完printf函数后返回主调函数,执行下一条指令。
🌳函数栈帧的销毁
printf函数栈帧的销毁
- 调用完printf函数后,printf函数它的栈帧就要销毁了。
- add的意思是让esp加4,即栈顶指针向下移动,来实现栈帧销毁。
- 这个xor指令的意思为异或,两个相同的数异或就为0(实质是设置返回值为0)
main函数的栈帧销毁
- main函数栈帧的销毁遵循后进先出的原则
- 首先连续执行三个pop(弹出),把三个寄存器还原回去。
- 我们让esp加上0C0h,那么esp将会往下移动到ebp的位置。
- 官方语言是:降低栈顶esp的位置,此时局部变量空间被释放。
- 此时main函数的栈帧就销毁了。
- 接着的话esp和ebp指针会重新去维护调用main函数的函数栈帧
总结
每一次的函数调用都要在栈区开辟相应的栈帧空间。并将空间的内容进行初始化。
为什么平时写程序时没有赋值的值都是随机值?因为局部变量就是在函数栈帧里创建的。局部变量的值是我们初始化的cc cc cc cc。cc cc cc cc就是随机值
函数调用是怎么返回的?我们首先push了一个ebp寄存器,这样调用完之后就可以根据ebp这个地址返回了。
如果你觉得我的文章对你有帮助🎉欢迎关注🔎点赞👍收藏⭐️留言📝。