当前位置:网站首页>函数栈帧的创建和销毁
函数栈帧的创建和销毁
2022-06-28 05:35:00 【叶超凡】
1.前言
啊哈亲爱的的小伙伴们大家好啊很开兴与大家再次见面,这篇文章其实是函数这篇文章的一个连续哈,我相信大家通过这篇文章应该能够更好的了解函数这方面的内容,因为我们将从汇编的角度来再一次了解函数,那我们废话不多说直接开始吧。
2.几个问题
首先问大家几个问题:
1. 局部变量是怎么创建的?
2. 为什么局部变量被的值是随机值?
3. 函数是如何进行进行传参的?传参的顺序又是怎么样的?
4. 形参和实参是什么关系?
5. 函数的调用又是如何做的?
6. 函数的调用结束后时怎么返回的?
看了这么几个问题是不是有那么一种感觉就是卧槽我学的是假的函数吧?这些问题我咋不知道,那么我们这篇文章我们就通过汇编的角度跟大家更加的详细的学习函数这方面的内容。首先进行说明一下就是本片文章采用的编译器是vs2013进行操作不同的编译器运行的结果会略有不同,请大家注意一下。
3.正文开始
首先我们介绍一个东西叫做寄存器,大家只要知道他的功能是存储数据就可以了,寄存器的种类有很多种分别为eax,ebx,ecx,edx,ebp,esp,其中ebp和esp是最重要的,因为我们每一次函数的调用都要在栈区创建一个空间,而这两个寄存器的功能是存放跟维护空间有关的地址,所以我们这两个寄存器是用来维护我们的函数栈帧的。好我们来看一下一段代码。
#include<stdio.h>
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;
}
根据我们各位小伙伴的水平想必看这种简单的代码应该不成问题吧,这就是我们定义的一个简单的加法函数,我们创建了三个变量a,b ,c其中将a的值初始化为10,将b的值初始化为了20,然后再把加法函数的结果赋值给了c最后再把c的值打印出来,想必我们上面说的这些话大家都能够看懂,那么接下来我要给大家介绍一点新的东西了,我们说过每一个函数的调用都要在栈区内创建一个空间,我们一开始就调用了我们的main函数所以我们一开始就会在这个栈区上申请一块空间给我们的main函数,比如说我们下面的这张图片
因为我们是在栈区开辟的空间,而我们都知道这个空间其实是有地址的,根据内存中的信息可以得我们是从高地址开始往低地址走,又因为这个空间的大小肯定是有限的所以我们这里的main函数就必然存在着一个较高的地址和一个较低的地址,那么我们这里就将较高变的地址放到edp这个寄存器中储存,而我们较低的地址就放到esp这个地址中进行储存,所以我们这里的main函数的函数栈帧就是有edp和esp进行维护的,大家要记住的就是我们调用哪个函数我们的edp和esp就会去维护哪个函数的函数栈帧,比如说我们下面就要调用Add这个函数,那么我们的edp和esp就会去维护Add这个函数的函数栈帧,那么这时edp和esp之间的空间就是为Add函数而创建的函数栈帧,因为我们的地址是从高地址到低地址的使用我们再调用新的函数的时候都是在这个函数栈帧的顶部进行创建空间我们就习惯的称esp为栈顶指针,edp为栈底指针。这里我们可以按Ctrl加F10然后再在调试里面点击窗口再点击调用堆栈我们就进入了这样的页面
这是我们就可以看到我们的这个小窗口上面有这么一句话说
这句话表示的是什么意思呢?他就说其实我们的main函数也是被别人调用的,这是什么意思呢?就是说别人先调用了一个函数,然后在这个函数里面又调用了main函数,那么这个函数是谁呢?我们按几下F10就出现了这样的页面我们看看


我们仔细看一下就可以看到我们在这里调用了main函数
然后我们往上面翻我们就可以看到其实是__tmainCRTStartup() 这个函数里面调用的main函数
但是我们在旁边的调用堆栈里面也可以看的到,下面还有一个mainCRTStartup() 这个函数,那么这个说明了在mainCRTStartup() 这个函数里面调用了__tmainCRTStartup() 这个函数,然后在__tmainCRTStartup() 这个函数里面又调用这个main函数,我们可以继续往上面翻便可以知找到哈
那么看到这里我们再来想一下我们之前画的那个图是不是就有点不对 啊,我们是不是得在main函数下面加上两个空间分给__tmainCRTStartup()和mainCRTStartup()这两个函数,以表示他们的函数栈帧。好我们现在进行反汇编操作我们来看看这个函数的创建的过程具体是什么样的。我们鼠标右击一下就可以看到有转到反汇编这个操作我们点击一下就可以看到如下的情况

我们可以仔细看一下右边的一个框架就可以看到我们写的代码
那么接下来我们就来一个一个的解释这些汇编语言的意思首先我们看到的就是
push edp,首先我们知道的一点就是我们在调用main函数之前得先调用__tmainCRTStartup()这个函数,而只要是函数的调用都会在栈区开辟空间,那么我们在main函数执行前其实我们是先开辟了一个空间用来放置__tmainCRTStartup()这个函数,而我们又知道有esp和edp这两个寄存器他是用来储存地址和维护函数栈帧的,并且esp是栈顶指针,edp是栈底指针,那么我们这里可以先这么画图理解在调用main函数之前我们的栈区是这样(兄弟们不好意思画图的时候把ebp画成立edp抱歉抱歉下面的所以edp都是ebp)
然后我们就接着执行我们的第一个汇编代码push ebp首先我们来解释一下这个push,我们的英文中的解释push就有推的意思,那么我们这里就将push理解为压的意思,简称就是把什么什么东西压进去,所以我们这里就把push理解为压栈的意思,而push后面就是它要压的对象,那么push ebp的意思是什么呢?就是将ebp此时的值压进去但是此时ebp指向的位子还是没有变的,那压的位置又是哪呢?我们知道数据的存储是按照地址又高到低的形式进行储存,因为上面是低地址下面是高地址,所以我们的push ebp就会将他压到栈顶上去,又因为esp是栈顶指针,所以这时候我们的esp就会往上面挪动一格,esp所存储的地址也会对应的发生变化,我们可以画图看看push ebp之后的结果是什么
虽然这个图是我自己本人画的但是不代表这个东西不能进行验证,我们可以对其进行验证我们打开调试然后点击窗孔然后点击监视我们
我们可以看到此时的esp和ebp的值为多少,然后我们再按一下F10让他执行push这个代码我们再来看ebp和esp的值又变成了多少,按照我们的理解就是esp的值会变小因为esp的会想低地址前进一格,而一格又是四个字节所以我们来看一下运行的结果为
结果跟我们想的一模一样,而这时候有小伙伴们可能又要疑惑了难道真的把ebp给压进去了吗?会不会是一格空的内存啊其实里面什么都没有,那么我们这里我们再打开窗口点击调试里面的内存
然后我们就出现了这样的页面,大家记得把上面的列改成4,然后我们在地址的那一块直接输入esp我们来看看结果如何?因为我之前输入过一次所以我上面的那张图片上面的地址就是esp的地址,我们来看看这个地址里面装的内容是什么?20 fc 6f 00我们把这个数据与ebp的地址进行相对比,我们便可以发现这两个其实是一样的就是顺序反了,这个其实是不要紧的,那么我们这里就可以下一个结论就是esp是一个寄存器他里面装的是一个地址,而这个地址所指向的内容与ebp这个寄存器里面装的地址是一样的,这里大家注意一下哈是ebp里面装的地址,并不是里面装的地址这个地址所指向的内容哈。看到这里想必大家应该能够理解push这个词的作用那么我们接着往下看就到这个指令:
这个指令的意思就是将后面的值赋值给前面的值,那么这里的意思就是将esp的值赋值给ebp,所以这时候ebp这里面装的值就跟ebp一样了,那在图像上的表示就是ebp没有再指向栈底而是指向了和esp一样的位置,我们可以画个图来理解理解
当然我们还可以看看我们监视当中执行完这一步之后的结果是什么
大家可以对比一下我们之前的那个结果然后你就会发现ebp的值确实发生了改变并且改变之后的值和esp一模一样,好看到了这里我们就继续往下看:
那么这里的sub的意识就是减的意思,esp是要减的目标而0E4h就是我们要减去的值,然后这个0E4h的的值是多少呢?我们可以在监视里面输入进去这个值我们就可以看到
这个值等于228,那么这里将esp的值减去228就会使得esp的值变小,而我们又知道我们这里是从高地址往低地址的方向跑,所以这里esp又会向上面跑一段的距离那么画图的话就是这样的
我们又知道esp和ebp他是用来维护函数栈帧的,所以我们这里esp和ebp之间的空间就是为main函数申请的空间,好那么我们接着往下看下面的操作是:
三个push,那么根据我们上面讲的便可以得知这里就会在栈顶上面再压栈三个空间进去里面分别装的是ebx,esi,edi,这里具体是干嘛大家其实不用知道哈,大家就知道有这么一个操作就可以了,而且与此同时我们esp的值也会相应的发生变化,那么我们的图像就可以画成这样:


我们可以看到在执行这三个push之前我们的esp对应的地址上面的几个地址里面对应的值是什么,好我们再按三次F10我们再来看看此时内存的情况
我们看到在原来esp上面的三个地址的内容全部发生了变化,而且内容也确实是和ebx,esi,和edi一模一样,那么我们接着往下看 下面的操作就是
这里的lea的意思就是load effective address的意思,简单的说就是把一个地址复制给谁,那么这里的edi就是面向的对象,而后面的一堆数字就是具体的地址,当然我们这里的地址是不好观察的,那么我们这里就鼠标右击然后把显示符号名勾上就可以很好的观察了
好我们这里就是把ebp-0E4h的值复制给了edi,那么我们这里是不是对0E4h这个值比较眼熟啊!这不就是我们之前开辟的main函数的顶部指针所指向的地址吗?我们来看在三个push之前的esp的值是0x006FFAEC好我们执行完这一步之后我们再来看看我们的edi的值变成了多少我们打开监视看一看
确实跟我们想的一模一样哈,那我们接着往下看下面的操作
这两个move我们熟悉就是将39h的值赋值给ecx然后将0ccccccch赋值给eax,那么这里的第三个操作又是什么意思呢?他的意思就是将从edi开始向下ecx个dword内容全部都改成0cccccccch,这里的dword就是double world的意思,一个word是两个字节,那么double world就是4个字节的意思,我们可以用计算机算一下就可以发现十六进制的39对应的十进制是57,再乘以4就是228个字节,而十六进制的E4对应的十进制的也是228所以我们就可以知道这一步的操作就是将开辟的main函数里面的所有空间全部内容全部都改成cccccccc我们可以在内存里面看一下:

一直到0x006FFBCC才结束,而结束的位置也恰好是我们edp所指向的位置上一个位置,那么这一步的作用就是将main函数开辟的所有空间的内容全部都修改成cccccccc,那么我们此时的图片就成了这样:
其实看到这里大家应该能够明白为什么我们每次创建变量的时候有要进行初始化了,而且我们之前在说字符串的越界访问的时候会出现越界访问出现烫烫烫烫的情况这种情况就是因为我们编译器在在这一步进行初始化的时候将每个内存全部都初始化为了cccccccc,所以当你要打印这方面的内容的时候就会打印出烫烫烫,所以大家以后在创建变量的时候记得一定要进行初始化操作,那我们接着往下看下面的操作
这个我们就比较熟悉了,move就是将后面的值赋值给到前面的地址所对应的内存里面去,那么这里就是将0Ah(10)放到edp-8这个地址里面,将14h(20)放到edp-14h这个地址里面去,将0放到edp-20h这个地址里面去,我们可以看一下内存当中情况
确实是按照我们所说的这样,我们还可以画图进行理解一下
那我们接着往下看
这里我们就很熟悉了我们将ebp-14h的地址对应的内容赋值给了eax,然后再对eax进行压栈,将ebp-8的地址对应的内容赋值给ecx再对ecx进行压栈,那么我们只要往上看一下就会发现这里的edp-14h的地址对应的内容就是我们的变量b,ebp-8的地址对应的内容就是我们的变量a我们可以在内存里面看一下
确实跟我们说的一模一样,那我们的图形就可以这么画
大家看到了这里有没有想一下这里的两个动作是在干嘛?是不是就是我们之前说的传参嘛,对吧,好我们接下来就来看看下面的这个操作:
这个call的作用就是开始调用函数了,但是在调用函数的时候他还会把自己下一条指令的地址进行压栈我们可以看一下,他的下一条指令的地址是:
我们这时就要按一下F11来看一下内存中的情况
确实将他下一条指令的地址压榨进去了,那么我们这一步的作用是什么呢?为什么要把他下一步的地址压栈进去呢?大家可以这么想一下,我们的call函数是用来调用其他函数,那么我们这时的程序就会跑到其他的地方执行其他的指令去了,但是等函数执行完之后他就应该回来执行它call指令的下面的指令了,那如何找到它下面的指令的位置呢?所以我们当时在调用的时候就顺便把他下一条指令的地址给存储了下来这就是为什么要压栈的原因,那么我们这时的图就可以画成这样:
然后我们再按一下F11我们就真的进入到我们函数的内部我们来看一下
这就是我们进入到函数内部的画面就是成了这样我们接着来分析一下这些指令,我们首先可以看到这些代码
是不是跟我们之前的main函数的函数栈帧创建的初期十分的相似啊,那我们就简单的梳理一下,首先我们看到的就是push ebp那么这里就是将ebp的值进行压栈把ebp的值压到栈的顶部,但是这里大家要记住的是我们此时的ebp还是在底部维持main函数的函数栈帧的,那么我们现在将他的地址的值压倒栈的顶部我们可以看看此时的内存
确实是将ebp的内容压栈到了函数顶部,然后我们接着看他的下一条指令move ebp esp,就将esp的值赋值给了ebp,那么此时我的esp和edp就指向了同一个位置就是栈顶,接着他的下一条指令就是sub esp 0CCh,那么这一步就是让esp又往上走了一段距离,那么此时的esp和edp是不是又开辟出来了一个新的空间,这个就是他们为Add函数开辟的新的函数栈帧的空间,下面三条指令都是push就是将三个寄存器全部都分别把ebx esi edi给压栈到栈顶上去,然后接下来的操作就是
加载有效地址,然后再把这些空间进行初始化我们上面讲过跟这个一模一样的我们就不多赘述,那么执行到这里我们可以看一下我们内存中的情况是否跟我们说的一样:

确实是跟我们说的一模一样,我们可以看一下图是怎么画的 
那么此时我们的图就成了这样,那么我们接着往下面看
下面的操作就是我们实现相加的过程,首先执行的是mov ebp-8就是将ebp-8的地址指向的内容全部都改成0,然后再执行的内容就是mov eax ebp+8 那么这里的意思就是将地址为ebp+8所对应的内容赋值给eax,接下来的操作就是add eax ebp+0Ch就是将地址为ebp+0Ch所对应的内容加到eax里面去,那么我们这里就有两个问题我们这里的ebp+8和ebp+0Ch到底对应的是什么,我们可以看一下我们上面画的那个图就可以知道此时的edp指向的地址是ebp–main那里,因为下面是高地址,上面是低地址,所以ebp+8就要往下两个格,对应的值就是10,我们把10放到了eax里面,ebp+0Ch就是往下三格对应的值20,然后我们这里就要执行的操作就是将20加到eax里面,那么此时的eax的值就是30,然后我们再执行的操作就是mov ebp-8 eax那么这里的意思就是将eax的放到ebp-8这个地址对应的内容里面去,而我们的ebp-8就是我们z申请的空间,。看到了这里想必大家应该能够明白几个问题就是我们函数的形参是如何创建的,是先调用函数再创建形参,还是先创建形参再来调用函数,那么通过上面的讲解我们发现他是先创建的形参然后再调用的函数,如果函数里面要用到形参的话就会再转过头来去找我们创建的形参,而并不是要用到的时候再创建,不用到就不创建,而且我们还发现这个传参是从左往右传的,先传b再来传a,好我们了解了这么几个问题之后我们再来往下看下面执行的指令,我们函数调用结束了,那么剩下来的操作就是返回一个值我们这里的操作就是mov eax ebp-8,这里的意思就是将ebp-8的地址里面的内容又放到了寄存器eax里面,我们这步操作的目的就是防止之后内容的清除而导致的数据的丢失,因为你的这些什么形参销毁后我的寄存器他还是存在的,那我们接着看下面的操作:
这里pop的意思就是将这里的edi弹出返回到寄存器edi里面,那么下面的两个都是这样的意思讲esi弹出放到寄存器esi里面,将ebx弹出放到寄存器ebx里面,那么这时随着这些内容的弹出我们esp的值也随着变大那么经过这三个pop之后我们的图就成了这样
,因为我们的函数已经调用完了,我们得到了我们想要的结果那么我们此时的函数申请的空间也就没有存在的必要了,所以我们现在就要对这些空间进行释放,那我们又是如何释放的呢就是靠的我们这个操作move esp ebp,将ebp的值赋值给esp那么此时esp就和ebp指向了同一个位置,而我们有知道esp和ebp是用来维持函数栈帧的,一旦他们两个合并到一起去了,那么他们之前维持的那个空间将不复存在,我们可以看一下这个图就变成了这样
那么接下来的操作就是pop ebp我们把ebp的值弹出还给了ebp,大家想一下这个ebp的值是什么?是原来ebp在main函数对应的栈底的地址,要是把这个地址弹回给了ebp那么我们现在ebp就会回到原来main函数的栈底,然后我们的esp的指向又会再向下移动一格,那么看到这里我们发现我们的esp和ebp又开始维护main函数的函数栈帧,我们又回到main函数的函数栈帧了,那我们接着往下看就可以发现下面还有个操作就是ret,大家可以看一下我们的这个图就可以发现我们这个esp现在值的值是我们之前执行的那个call的下面的那个地址,既然我们的函数已经执行完了我们是不是就得回到原来我们调用函数的下面继续执行下面的代码,那么怎么回去呢,就是听过指令ret和我们这里存储的这个地址,执行完这个ret之后就会把这个地址弹出我们的程序的执行就会回到call指令的下面我们esp的值就会再向下一格
大家这时候就应该能理解当初为什么要存储这个地址了,就是为了我们能够走的出去还能够回的来。那我们看下面的指令add esp 8 大家可以想一下我们的函数都调用完了,那么我们的这些形参还有用吗?肯定就没有用了嘛,那么我们就要将他消除掉如何消除呢?就是我们这里的指令add esp 8 这样我们的esp就又会向下两格我们的图形就成了这样
看到了这里大家应该就能明白一个问题就是我们的形参的销毁是在函数调用结束前还是结束后啊?想必大家都能够给出答案是调用结束后,好我们这里就能只剩下最后一个指令了mov [ebp-20h] eax 因为我们函数调用之后得把结果给我们,那么这里就是将eax寄存器里面的值赋值给ebp-20h里面而这里的ebp-20h就是我们之前创建的变量c的地址,看到这里我们这里的内容就结束了,想必大家应该也能学到一些知识吧,那么这篇文章到这里也就结束谢谢大家阅读。
边栏推荐
- How does the power outlet transmit electricity? Simple problems that have plagued my little friend for so many years
- msa. h: There is no such file or directory
- Wedding studio portal applet based on wechat applet
- 刘海屏手机在部分页面通过[[UIApplication sharedApplication] delegate].window.safeAreaInsets.bottom得到底部安全区高度为0问题
- Create NFS based storageclass on kubernetes
- Disable right-click, keyboard open console events
- [Verilog quick start of Niuke online question brushing series] ~ one out of four multiplexer
- What does mysql---where 1=1 mean
- 数据中台:一篇带你深入浅出了解数据中台
- Qtcanpool knowledge 07:ribbon
猜你喜欢

电子邮件营销的优势在哪里?为什么shopline独立站卖家如此重视?

MySQL export database dictionary to excel file

? How to write the position to output true

How to learn programmable logic controller (PLC)?

独立站卖家都在用的五大电子邮件营销技巧,你知道吗?

Share a powerful tool for factor Mining: genetic programming

mysql导出数据库字典成excel文件

分享|智慧环保-生态文明信息化解决方案(附PDF)

如何做好水库大坝安全监测工作

MySQL 45讲 | 05 深入浅出索引(下)
随机推荐
数据仓库:DWS层设计原则
Create NFS based storageclass on kubernetes
What does mysql---where 1=1 mean
联想混合云Lenovo xCloud,新企业IT服务门户
Share a powerful tool for factor Mining: genetic programming
Flink 窗口机制 (两次等待, 最后兜底)
Can wechat applets import the data in the cloud development database into makers with one click in the map component
拉萨手风琴
Animation de ligne
Jdbc的使用
How to learn programmable logic controller (PLC)?
jq图片放大器
numpy.reshape, numpy.transpose的理解
数据仓库:金融/银行业主题层划分方案
Steve Jobs' speech at Stanford University -- follow your heart
msa. h: There is no such file or directory
Intensive learning notes
密码学笔记
什么是WebRTC?
[JVM] - memory partition in JVM