函数的栈帧的创建和销毁(c语言)

前期学习的时候,我们可能有很多困惑?

比如:

  • 局部变量是怎们创建的?
  • 为什么局部变量的值是随机的?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用是结束后怎么返回的?

其实,知道和函数栈帧的创建和销毁就都会了,这个过程就是修炼了自己的内功,也能搞懂后期更多的知识。


进入正题

今天讲解的时候,使用的环境是VS2013,不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。


 首先,我们先做一些铺垫,如下:

1.寄存器(包括:eax,ebx,ecx,edx      额为再给大家补充两个ebp, esp)

今天的重点是ebp,esp.因为要了解函数栈帧,必须要了解ebp和esp。

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;

}

假如是这段代码,假如现在是main函数调用,main函数调用的话 ,就要在栈区开辟一块空间,如下假如是栈区开辟的一块空间

 图中,从上往下数,假如第三块代表main函数开辟的一块函数栈帧。

那这块函数是怎么维护的呢?

这块函数由两个寄存器维护的,分别是ebp和esp。他们两个寄存的地址位置指向如图。就是说假如我们进入main函数(如代码上进入main函数的箭头),这时候我们为main函数分配的空间就是如上图第三块空间,这块空间由ebp和esp来维护的,当正在调用那个函数,esp和ebp就是维护哪块空间的那个函数的函数栈帧。比如说这里要马上调用Add函数(如上图箭头),这里esp和ebp就为Add函数维护栈帧了,esp和ebp直接的函数就是为Add函数调用空间。那我们就相对清楚了,esp和ebp这两个指针其实是维护函数栈帧的,当我们调用那个函数,esp,ebp就跑去维护那个函数栈帧,那她们两个的空间就是为这次函数调用所开辟的空间。

通常,我们把底下的ebp叫栈低指针,把上面的esp叫栈顶指针,什么意思呢?栈区的使用习惯是,先使用高地址在使用高地址,从高地址向低地址使用空间,空间被消耗。


其实在VS2013中main函数也是被其他函数调用的。是被__tmainCRTStartup调用的,这个函数又是被mainCRTStartup调用的。就是说mainCRTStartup调用了__tmainCRTStartup,__tmainCRTStartup调用了main函数。

栈区上使用内存的时候的时候,每一次函数调用都要为函数在栈区上分配空间。

如图要调用main函数,看图代码,main函数开始的调用的时候时候 ,程序走进来了,此时为main函数分配了栈帧,继续往向下走,走到c=Add(a,b)的时候,开始调用Add函数,上去调用Add函数的时候,也为Add函数调用了栈帧(如图第一个红色框),此时esp和ebp就去维护Add去了(维护第一个红色框),但是我们要注意,在调用main函数调用之前,,也为__tmainCRTStartup和mainCRTStartup调用了空间(如图,倒数第一个和倒数第二个小红框)。

接下来我们一起探究一下,这个函数具体是怎们调用的。

(按F10,然后右击鼠标,出来的选项,直接转到反汇编,转到反汇编之后,就可以看到c语言的汇编代码了)

那这些代码该如何去理解呢? 

我们上面说在调用main函数之前,调用了一个函数__tmainCRTStartup(调用main函数的函数)。

 这块空间有ebp和esp维护,如上图。这两个寄存器维护这次为函数调用所分配的栈帧。到我们的内局布局是这种情景的时候,接下来我们我们要进入main函数了。

1.进入main函数的第一步叫push,push叫压栈操作。(接下来的操作都是在栈区中进行的)

栈区的使用都是从高地址到低地址使用(内存向上蔓延)

 push  ebp就是把ebp的值放到如图的位置,意思是压栈,给栈里面放个元素进去。这个ebp是为__tmainCRTStartup放的一个值,当压栈的时候,上图中相当于多了一个元素就是上图的ebp,而esp维护的是栈顶,所以他就成上图中的情况,它指向了ebp(此时esp地址变小,这说明地址向上走了一下(可以在调试窗口内存中看到))。

push:压栈指给栈里面放个元素进去

pop:出栈指从栈顶删除一个元素

2.把ebp压进去之后,接下来mov,(mov指把后面的值放到前面去。下图意思是,把esp的值给ebp)

 

 

从监视(esp和ebp相等)可以看出 ,上图中的逻辑是对的。

3.sub(解),下图的意思是,给esp解去0E4h(0E4h是个使用地址数字,监视可以看出它的值是228)

此时他的值变小了,这意味着(esp开始存的是 __tmainCRTStartup上的ebp的值)esp存的不是__tmainCRTStartup上的ebp的值,而是指向上面的某一块区域,此时第二个ebp指向__tmainCRTStartup上的ebp。如下图:

此时我们大概画出esp指向的空间,从图中我们可以看出,esp和ebp都有了新维护的空间。这时候其实已经掉入了main函数,为main函数预开辟了空间,如下图: 

4.接下来开始使用这块空间。

我们可以看到,又出现了3个push,这三次push指给顶上(main函数的预栈帧空间) 压进去了三个元素,分别是ebx,esi,edi。如下图:

注:这里观察这三个值是否压进去了在内存中观察。 

5.lea(lea叫load effecitve address):加载有效地址

上图指把ebp-24h这个有效地址加载到edi中去,相当于给edi中放了一个有效地址。

6.下面第二,第三个地址中,第二个指把9放到ecx里面去,第三个指,把0CCCCCCCCh放到eax中去。

上图 中前三个做完之后,真正产生效果的是第四个。第四个指,要把刚刚edi这个位置开始,向下的ecx中放的9这么多个dword(一个word两个字节,dword是双字节,4个字节)空间全部都改(初始化)成0CCCCCCCCh内容。所以main函数的栈帧里面全部初始化成0CCCCCCCCh。

此时,为main函数的栈帧正式开辟好了,接下来进行正式有效的代码。

这时候开始执行第一段代码,如上图,这段代码的意思翻译成汇编语言是dword prt [ebp-8],0Ah。

上面就是把0Ah(0A指10)这个数字放到[ebp-8]这个位置。 

如上图main函数的栈帧这个空间里面,这些小框都是大小为4的框。此时,看倒数第一个紫色框下边线,他代表ebp,上边线代表ebp-4,此时这整个框代表ebp-4,往上偏移了4个字节。如上图,我们继续往上看,倒数第二个紫色框,他就是ebp-8,往上偏移了8个字节。依次往上类推 。此时,我们回头继续看这个图:

 

看第一条,这里说,把0Ah的值放到ebp-8(汇编语言)里面,就是放到上面我们说的倒数第二个紫色框里面,就是10。假如这里,我们没有给a赋值,这里默认的值就是CCCCCCC(如下图),现在我们来看一下内存里面是否是我们说的这样。

 接着往下看第二条。

又是mov的指令,把14h放到[ebp-14h]里面,(这个个14h完全没有关系 ),14其实就是20,相当于解了20,往上走,我们看内存。

 通过图我们发现,a和b空了两个空间。我们看前三个图,就很容易发现b的位置。

紧接着继续看第三条。

还是mov指令,把0放到[ebp-20h]里面,20h和14h又差一块距离,这里差多少呢?我们看内存。

通过图我们可以看出,他们之间又差两个整形。 我们看前四个图,就很容易发现c的位置。

所以在函数里面局部变量是怎么创建的呢?首先我们要为这次函数调用创建函数栈帧(如main函数的栈帧),有了函数栈帧,我们要在函数栈帧里面找到一些而空间,把我们a,b,c放进去,这就是局部变量的创建方式。

当把a,b,c创建好之后,接下来可以调用Add函数了。

此时,我们就要想,函数调用就要传参,那他是怎样传参的呢?

  

 

看第一段代码,mov,把ebp-14h放到eax中去,其实就是把b的值放到eax中去。

看第二段代码,push,push eax,压栈eax,而eax里面放的是20,就是压栈20.  如图,第一个浅蓝色框。

如图,此时esp指向了第一个浅蓝色上边框。(图中有误,应该是第一个浅蓝色上边框线)

此时,我们发现内存栈顶把14放进去了。接着往下看: 

第一段代码 再mov,mov ebp-8(就是a)放到ecx里面去,就是相当于把10放到ecx里面去。第二段代码push,压栈ecx,而ecx里面放的是20,就是压栈10.观察内存,如下图:

 此时,esp移向第二个浅蓝色框上边线。

如上这两个动作在传参。

接着往下看:

第一段代码中,call(调用的意思),(此时按F11观察),你会发现,call调用的是第二条代码的地址003518F7。call执行完执行以下语句。继续按F11,这次才是真正进入Add函数,如下图:

 

观察上图我们发现,这些代码和在main函数前面的代码一样,这其实是在为Add函数准备栈帧。

第一条,push ebp,但是我们发现,ebp还在维护如下图的main函数栈帧的栈底。

这里push ebp就是说在如下图00C21450上面再压上个ebp,,这个ebp是为main函数创建的ebp。此时,esp移动到如下图红色框上边框。

 

 此时,后面的步骤和上面main函数中的一样。

此时,创建了Add函数的栈帧,如图:

当上面执行完之后,我们的汇编语言终于要开始计算了。

 

 

 看此图的代码,意思是,创建临时变量z,把0放到[ebp-8]的位置上去。如上上图。

接下来要开始执行如上代码,但是我们发现,我们还没有看到x和y,其实他在如下图的位置,我们继续往下探究就会发现。

 

 看第二条代码,mov,把[ebp+8]的值放到eax里面去,此时ebp在如下图位置。给他加8,向下走,走到如上图位置。

 第三条代码意思是,add,把[ebp+0ch](0ch就是12,给ebp加12,就是20)放到eax里面去,然后加起来就是30。加起来之后,mov,把eax里面的值放到[ebp-8]里面去。

从上面我们可以发现,函数在调用计算的时候,形参并不是创建的,而是在调用指令的时候直接传过去的。当真的来到函数内部去调用这两个数相加的时候,形参并不是在这个函数内部创建的,而是回去找了我们刚刚在调用的时候临传参传过去的这个空间,此时如下图这两个就是x和y。

函数传参的方式是怎样的呢? 

其实在还没有调用c=Add(a,b),中a,b的时候,参数就已经传过去了,先传b,再传a.然后在如下图在函数栈帧里面压了两个参数,压进去之后,当我们真正进入函数内部的进行x,y相加的时候,我们又找回了之前压进去如下图这两个数,然后计算,计算好的值在放到z里面去。所以说,形参是实参的一份临时拷贝。

 

接下来,要返回了,那返回是怎样返回的呢?

 

mov,把[ebp-8]的值放到eax里面,为什么放到eax里面去呢?因为如果不放在eax里面去,这个z就会销毁。而eax是个寄存器,寄存器不会程序退出就销毁程序。而eax是如下图中的值,里面放的是30.(相当于把z放到里面保存了起来)然后回到主函数里面,把eax的值继续拿出来用即可。

 

此时,到下面这段代码。

 

 pop是弹出的意思,这里指,(如下图中)把栈顶的edi弹出放到edi这个寄存器里面去(本来是edi,再放到edi里面去)一次弹出,esp就会加加一下,往下走(到esi的上框),在弹出,放到esi中去,esp再加加一下,往下走,再弹出,esp再加加一下,往下走,esp到达如下蓝色箭头指的位置。

 当这次调用完之后,此时的结果都已经放到eax往回带了,此时,Add函数的栈帧没必要存在了,这是,将这块空间回收,纳黛蒂是怎样回收的呢?接着往下看。

看第一段代码,mov,把ebp赋给esp,此时esp不指向如上上图的位置了,应该指向如下图ebp指向的位置。

 

 紧接着看下一句指令。

pop  ebp,指弹出一个指令放到ebp里面去,弹出的是如下图红色框,而他存的是main函数的ebp的地址,这个地址存在这,就是当我们函数返回之后随着Add函数栈帧的销毁,我们能很容易找到main函数的栈顶,但是找不到main函数的栈底,所以把栈底存在这里,当esp走到这里的时候,pop一下,把结果弹出,弹到ebp里面去,这个时候,就意味着ebp走了,回到如下图位置。

 现在,我们回到main函数里面来了,此时,main函数栈帧是怎样一块空间,又由esp和edp维护了。

然后,进行最后一条指令,ret,请注意,ret这条指令有一个问题。说我们这个函数还没有回去呢,其实还没有回去呢,继续看图:

 

栈顶 上除了我们刚刚的main函数之外,还有call指令的下一条地址。刚刚pop出去之后只是让我们找到了mian函数的ebp和esp,找到了栈帧空间,但是当我们回到了main函数之后,我们应该从call指令的下一条指令开始执行,那怎么回到call指令的下一条指令呢?如上图,放着call指令的下一条地址(ret就是pop之后跳到了这个地址),所以我们为什么开始再call指令下存这个地址,就是为了回来之后从call指令之后开始往下走。pop之后,esp指向如上图第二个浅蓝色上边框位置。

 此时,指向如图倒数第二行add  esp 8(意思是esp+8),此时代码指向如下图紫色框第一个edi的上边框,指向这的时候,相当于把形参,x,y还给操作系统了,此时想必大家已经了解了形参空间是怎样销毁的,什么时候销毁的。

 

 当回到这里的时候,把图中ecx-10和eax-20这两个形参变量的释放了,在这往上走的空间不属于我们了,因为esp和ebp维护的空间才是我们当前使用的。继续向下看,mov,把eax的值放到[ebp-20h]中,如下图:

20h在第一个红色边框,红色箭头指的位置,就是c,而eax就是和30,c就等于30.

此时我们来说一下,返回值是怎么带回来的,返回值开始放在寄存器里面 ,到我们回到这个函数里面,再把寄存器里面的值放到局部变量里面,这样返回值就带回来了。

讲到这里,大家是否觉得非常神奇。

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值