目录
先了解一些东西
寄存器
我们在开始学习函数栈帧的创建与销毁之前,要先了解一下这几个寄存器:
这里面eax, ebx, ecx是通用寄存器,保存临时数据;
eip是指令寄存器,保存当前指令的下一条指令的地址。
这里面最重要的是esp,ebp这两个寄存器。 我们要了解函数栈帧,就必须了解esp,ebp这两个寄存器。esp,ebp两个寄存器里面存放的都是地址。这里面esp是栈顶寄存器,保存的是函数栈帧的栈顶地址。ebp是栈底寄存器,保存的是函数栈帧的栈底地址。esp与ebp共同维护着函数的栈帧空间。
相关的汇编指令
函数栈帧
如图代码
我们应该知道,每一次函数的调用,都要为本函数开辟新的空间,这就是函数的栈帧空间。并且该空间被栈顶栈底寄存器所维护。注意,栈底栈顶寄存器内部只保存一个地址。所以只能维护一个空间。比如如果在调用main函数,那栈底栈顶寄存器就是在维护main函数的栈帧空间。但是如果main函数正在调用另一个函数Add, 那栈底栈顶寄存器就正在维护Add函数的栈帧空间。等到Add结束调用,栈底栈顶寄存器重新维护main函数栈帧空间。
下图为本函数Add没有被调用前的esp,ebp维护栈帧情况
下图为main函数调用Add时的esp,ebp维护栈帧图
了解main函数如何被调用
接下来我们来看main函数是怎么被调用的。首先这需要用到函数堆栈 ,先进行调试,点击f10,然后不要动。打开调用栈堆,右击显示外部代码。然后可以看到以下信息:
然后,由前面的基础知识我们知道,invoke_main函数会有自己的栈帧空间,而main函数同样有自己的栈帧空间,Add函数也有自己的栈帧空间。
那么我们就可以想到,在main函数被调用之前应该是这样的
现在我们进行继续程序调试。右击反汇编:
这是c语言的汇编代码,我们接下来进行逐一解读。
现在来看第一条代码push ebp,我们在开始的部分已经介绍了一些基本的汇编指令(忘记的小伙伴请返回进行查看),push的意思就是进行压入数据。而在第一条这里,压入的是什么数据?压入的是ebp的数据。而ebp里面保存的是什么?我们说ebp是栈底寄存器,保存的是栈底的地址。所以,这第一条指令的意思就是,将栈底的地址压入栈帧空间。同时esp地址发生变化。那么到底是不是这样呢。我们来看一下vs2022编译器的监视:
首先要注意十六进制打开,这样好观察地址。
然后f10走一下。我们会发现,esp指向的地址,确实发生了变化,这个变化是四个字节。并且是变小。正好对应我们压入的数据ebp的地址所占用的内存大小。
然后一条指令“move ebp, esp”;意思是将esp的值赋值给ebp。注意,将esp的值赋值给ebp。那么ebp和esp都将指向一处空间。
我们接下来再看下一步:“sub esp, 0E4h”。意思是让esp减去0E4h。此时esp再次发生变化。 esp往上走,指向上面的一个位置。现在ebp和esp所维护的这块空间就是为main函数预开辟好的空间。这一块空间同样可以通过vs2022的内存监视来查看。
三张内存图片,可以观察到就是ebp到esp的内存空间。而这么一大块空间就是为main函数预开辟的空间。接下来还有操作;
接下来就是三个连续的压栈:“push ebx”; “push esi”;“push edi”;意思是将ebx,esi,edi三个的值依次压入栈帧空间,同时esp向上移动。可以观察到,每压栈一个,esp的地址就向上移动四个字节。
再然后的四条语句就很重要了。首先是“lea edi,[ebp - 24h]”这里要知道lea的意思。lea 就是load effective address。意思是加载有效的地址。而 “lea edi,[ebp - 24h]”就是将[ebp - 24h]这块地址加载放进进入edi之中。 然后看“mov ecx,9”。意思是将9这个数字赋值给ecx。“mov eax,0xCCCCCCCC”是将0xcccccccc这个数赋值给eax。最后一步是最重要的
rep stos dword ptr es:[edi] 的意思是将从edi所指向地址向上的ecx个,也就是39h个四字节(dword就是double word双字,一个word是两个字节,也就是四个字节)的内存全部赋值成0xCCCCCCCC
这里应该是9 * 4 = 36个字节的空间被赋值为CC,这里应该是这样的:
到这里,main函数的栈帧空间基本创建完成;
main函数的核心代码
进入main函数后,我们来看main函数的核心代码:
局部变量的创建
首先将显示符号名关掉。以便我们进行观察;
然后我们会看到这样的代码。
为了方便观察。我接下来直接上图:
以上,就是main函数内部局部变量的创建和初始化。由上面我们可以得出结论。局部变量其实就是在函数栈帧中创建的。
函数的调用
Add函数的栈帧创建与main函数类似。不过有了创建形参和语句地址传参的过程。就是在ebp地址压入栈帧之前先进行形参的创建。然后是语句地址的压栈。下面为形参的创建与语句地址的压栈过程:
此时我们的栈帧空间是这样的:
接下来进入Add函数 ,现在就是正式开辟Add函数的栈帧空间。
上图就是Add开辟好的栈帧空间;然后Add内部开辟局部变量后:
return返回时需要将返回的值存入寄存器之中,通过寄存器返回想要带回的结果。
栈帧的销毁
当函数想要返回的时候,栈帧也要开始准备销毁了。这时候需要遵循先入的后出,后入的先出。
下面为主要流程:
恢复main函数栈帧维护后仍就需要进行多条指令:
这就是一套大概的栈帧销毁流程 。当main函数被调用完之后也是同样的道理。先弹出三个寄存器的值,再将ebp的值赋值给esp,回收main函数栈帧空间,然后再将invoke_栈底的地址弹出还给ebp。再弹出call的下一条指令。然后返回,如果是正常返回。就将eax中的返回值0返回给操作系统。