函数栈帧的创建及销毁(在VS 2013环境下)

1,寄存器:eax,ebx,ecx,edx,ebp,esp

其中esp,ebp这两个寄存器中存放的是地址,是用来维护函数栈帧的

每一个函数调用,都要在栈区创建一个空间。

下面用一个简单的程序来进一步地解释这个概念。

 如图,这一个为了main函数运行而开辟的空间就叫做main函数的函数栈帧。

这块空间的维护是由ebp和esp来实现的。

ebp,esp作为寄存器是存储空间,存储的是两个地址,分别指向两个位置。

正在调用哪个函数,ebp和esp维护的就是哪个函数的函数栈帧。例如Add函数在运行的时候,ebp和esp维护的就是它的函数栈帧。

一般我们把ebp叫做栈底指针,esp叫做栈顶指针。

接下来进一步地了解,按F10进入调试,打开调用堆栈。

这里可以看见main函数被调用了,但令人困惑的是main函数被谁调用了。这个时候我们让代码往下走,当代码执行完了的时候,我们可以看到下图

 

 我们可以看到是_tmainCRTStartup()这个函数内部调用了main函数,这里要理解main函数也是被别的函数调用的。

图中右边窗口第二行可以看到_tmainCRTStartup()这个函数是被mainCRTSartup()调用的,这个调用的逻辑是比较复杂的。

简而言之,就是main函数在vs 2013中也是被别的函数调用的。调用关系如下图。

 

 我们为什么要先理清这些呢?接下来我们以最开始那个简单的代码来进一步地展开叙述。

首先main函数开始运行,接下来走到了c=Add(a,b)这一条语句,进入到Add函数,这个红色的框假定是为Add运行而分配的空间。这个时候esp和ebp要去维护Add的函数栈帧。

 值得一提的是,在main函数调用之前还有两个函数,这里将其画出来。

那么接下来看其中的细节

点击F10,然后右击鼠标,转到反汇编

 然后就能看见C语言所对应的汇编代码,如下图。

 

 接下来要讲的是如何理解这个汇编代码。

因为我们一会儿要观察地址,为了便于理解,我们将右上角的“显示符号名”这个选项×掉。

 我们知道,在调用main函数之前便已经调用了_tmainCRTStartup()这个函数。

根据我们的理解,main函数在这之前是被别的函数调用的,而在这里我们进入到main函数,那么调用main函数的那个函数的栈帧在此之前就创建好了。

假设这就是为了调用main函数的函数所创建的空间,那么这一次函数调用时候ebp和esp就会分别指向这两个位置。

 

 当我们内存布局是这样的场景时候,我们要进入main函数了,在前面的汇编代码图中可以看到第一步是push,也就是压栈操作。

我们画图的时候认定下面是高地址,上面是低地址。我们知道栈空间的使用是从高地址向低地址的,在这里就是从下往上使用。

第一步push ebp,也就是往栈里放了一个元素进去。

当第一步执行完了之后,栈顶指针esp里的地址就不在上图的位置了,就指向如下图所示的位置。 

这个过程是可以观察的到的,我们打开监视窗口。

在程序尚未开始运行的时候,esp和ebp里面放的元素如下图。

我们知道地址从高往低,那么当这个过程执行的时候,esp所对应的地址应该向低地址转变。

 

 运行完之后,果然如此。

若想更加清晰地观察,我们打开内存窗口。

 

可以看见,在esp对应的地址里放的值就是ebp的的地址008ffbf4,这就是压栈的操作。

继续往下,是mov指令,这是将后面的值赋到前面去。就是将esp的值给ebp。

这时,ebp就不再指向底部了,而是指向了esp所指向的位置。如下图

 

 在检测窗口中可以清楚地看到这一点。

 接下来是sub操作,就是给esp减去一个0E4h(这是一个八进制的数字)

可以观察到它的值。如下图。改为十进制的话就是228.

 进行完这一步之后我们可以看见esp的值变了。

这意味着esp的值变小,指向了低地址。

 

 这个时候可以清楚地看到,esp和ebp有了新的维护空间,这就是为了main函数申请的栈帧。

程序继续运行,到了三个push的指令。

这三次push其实是给栈顶压入了三个元素。每一次push时esp都会变化,我们目前不需要知道这里的这三个元素指的是什么,只需要知道有这一步。

 执行完这三步之后,代码来到了lea指令。

 

lea其实叫做load effective address,就是加载有效地址,把后面这个有效地址加载到edi中。

我们将显示符号名勾上,以便于较好地观测这个地址。

 ebp-0E4h就是下面紫色箭头所指向的位置。

 代码再往下走,mov是把39h放到ecx里面去,把0CCCCCCCCh放到eax里去。

这三步做完之后,能产生效果的是rep stos这句话,这句话的意思是把刚才从edi开始向下的39h次,这么多个dword的数据全部都改成0CCCCCCCCh。

一个word是两个字节,dword是四个字节。

简而言之,就是要把从edi开始,39h个空间全部都改成eax的内容。

 到了这之后,便结束了前期的准备工作。

准备进入c语言的代码。

我们把显示符号名去掉,观察地址。

 这里注意我们赋给了a变量一个“10”的值,这恰好可以解释初始化的意义,若是不初始化变量的话,每个编译器随机赋予的值是不一样的,这也是为什么初学c语言时我们有时候忘记赋值会打印出来类似于“烫烫烫”这样的值。

言归正传,下图是三条赋值语句在内存中的体现。

做一个小的总结:先为函数调用创建栈帧,然后再在它的函数栈帧中将变量放进去。

a,b,c变量创建好了之后,我们要开始进入Add函数。

 

 然后通过mov操作,把【ebp-14h】里的值赋给eax,再进行压栈操作,把eax放到栈顶。

接下来的两步操作与此类似,此处不再赘述。

这四步操作运行结束后,a和b的传参也就完成了。

 接下来按F11来执行call语句,这个语句会跳转到一个内部的函数,且将下一条语句的地址找到,下一条语句就是正式进入到Add函数之中。

 接下来就是为Add函数创建栈帧,其过程与为main函数创建栈帧基本类似。

 

 接下来是赋值语句,其内部运行逻辑如下红框标记起来的位置。

 现在执行完这些语句之后,z的值变成了30,这个值会被放到寄存器eax中,在函数运行结束后寄存器里的值并不会被销毁。那么等到回到主函数之后,就可以拿到这个值了。

在进行了三次pop操作将edi,esi,ebx弹出之后,进行一个mov操作让ebp和esp回到最开始的位置,我们在当时就存储了一个main函数的初始位置,再进行一个pop操作就可以让ebp指针回到原本的位置。

那么如何让main函数回到call指令的下一句开始执行呢?

这里注意我们当时就在栈顶存了一个地址。在ret操作后就会来到这里。

 

之后执行一个add操作,esp+8就是让两个形参所占据的空间被释放。

 然后将eax的值放到ebp-20h里面去,也就是把30赋值给c,Add函数的返回值成功带回。

以上就是函数栈帧的创建与销毁大致的全部过程。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值