函数栈帧的创建和销毁

什么是函数栈帧?

理解了函数栈帧能解决什么样的问题?

函数栈帧的创建和销毁解析!

调试工具:vs 2013。

什么是函数栈帧?

函数栈帧就是函数调用过程中在程序的调用栈所开辟的空间,这些空间是用来存放:

        ①函数参数和函数返回值    ②临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)   ③保持上下文信息(包括在函数调用前后需要保持不变的寄存器)

理解函数栈帧能解决什么样的问题?

在写代码时,我们总是会去调用函数,创建函数和变量等等,这时我们不禁会去思考:

 ①局部变量是如何创建的?

②为什么局部变量不初始化内容是随机的?

③函数调用时参数是如何传递的?传参的顺序是怎么样的?

④函数的形参和实参分别是怎么样实例化的?

⑤函数的返回值是如何带回来的?

接下来,将好好分析一下关于函数栈帧的知识点。

一、寄存器:eax,ebx,ecx,edx,ebp,esp.而本文中重点提到的是esp和ebp!

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

众所周知,每一函数调用,都需要在栈上创建空间,比如:

 这时候就在栈上开辟了一块空间:首先,要为main函数开辟一个栈帧:

 但是如何维护呢?这就需要用到了esp和ebp,它们分别指向了main函数栈帧的两个位置,以便维护栈帧:

(ebp和esp就算,调用了哪块函数,就去维护哪块函数的栈帧,此时进入的是main函数。) 

通常我们把esp称为栈顶指针,ebp称为栈底指针(为什么上面是栈顶,下面是栈底呢?因为像是使用栈的时候,往栈顶放数据,使用时,是从高地址开始望低地址开始使用。)

首先我们来调试main函数,而main函数是被__tmainCRTStartup这个函数给调用,而__tmainCRTStartup这个函数又是给mainCRTStartup给调用

,注意的是,这两个函数也在栈区上开辟了空间,而在main函数里面调用Add函数,Add函数也在栈区上开辟了空间。

因此,这几块空间的大致分布是:

 这就是往低地址开辟调用函数的栈帧。

根据上面所述,在调用main函数之前,esp和ebp指向的是__tmainCRTStartup这个函数的栈帧,也在维护着这个栈帧。

 布局于此,当我们开始进入main函数了,于是:在调试中我们看见:

 开始进入main函数的时候,第一步就算push(压栈)ebp,把ebp往低地址压入!此时此刻,esp也要跟着变化,变化成了ebp的值:

变化前:

 变化后:

接下来,便是move: ,意思是把esp的值给ebp,这意味着,ebp不再指向下面那个位置,而是指向esp现在所指的位置。

 

随后到了sub这一步:意思是减,也就是将esp减去 0E4H,这个是16进制数。减去之后,就地址就变化了:

这意味着,esp的地址变小了,指向了上面的低地址的某一块区域 了,而此时的esp和ebp不再维护__tmainCRTStartup这个函数的栈帧空间了,而是维护这一块新的空间!

此时此刻这一块新空间就是main函数的栈帧。

紧接着下来,是三个push, ,将ebx,esi,edi压栈。此时的esp也需要改变位置。

 然后是load effecitve address,意思是加载有效地址

 

 ,也就是把[ebp+FFFFFF1Ch]这个地址加载到edi里面去,注意到,[ebp+FFFFFF1Ch]实际上就算ebp-0E4H(在调试器里面显示符号名所得的结果),所以,ebp指向了原来esp指向的那个位置。

 然后从edi开始,向下地把ecx中39h次,每次dword(double world,4个字节)个地址,全部改成0XCCCCCCCCh)。这时候,esp到ebp这块空间的地址,已经全部初始化为0XCCCCCCCCh了,main函数的空间开辟完成。

接下来便可以执行main函数里面的代码了。

 move,就是将0Ah,也就是10这个数据,放进【ebp-8】这个空间里面,这块空间也就是a的地址。值得注意的是,有时候我们写代码的时候,没有给变量初始化,那么此时存进这个变量里面的值就是0XCCCCCCCCh,也就是随机值。这就是为什么有时候我们会打印出随机值、烫烫烫....这些数据出来。

同理,b和c的变量跟a的变量一样,存放在某块空间中,即为b或c的地址。

 

 到了Add函数:

 首先是将b的数据给了eax,然后push  eax,esp往上移。再把a的值给ecx,push ecx,esp的值往上移。这个操作,就是所谓的传参!

 接下来的指令是call,就是调用的意思。

执行 call指令的时候,会将它下一条指令的地址压栈!也就是Add函数的地址

 这个时候,就能进入到Add函数里面去了!进入了Add函数,指令的指向顺序跟进入main函数的一样,就是要给Add函数创建栈帧!

 此时第一条指令,push ebp,而此时的ebp还是在维护之前的那个地方

 push ebp,就将ebp压到了栈顶,这个ebp是main函数的ebp

move ebp esp,把esp 给 ebp,也就是将ebp 要移动到现在esp当前的位置,再将esp减去0CCh,esp移到到空间上的某块位置,为add函数分配函数栈帧!

 接下来又是3次push,这里不再描述。

给add函数空间的地址值进行初始化:0XCCCCCCCCh 。

开始执行计算任务:

根据上图可知: 给z创建空间地址ebp-8,初始化为0。进接下来是move,把ebp+8的值放到eax中去,ebp+8在哪?

 然后把ebp+0Ch的值加到eax里面去,eax就变成了30,接着再把eax的值放到ebp-8这个地址里面去,也就是z这个地址。

值得注意的是,这里的ebp+8  和  ebp+12 都是a 和b 的地址的一份临时拷贝,并不会改变a和b。因为地址不一样,传的是值,这里就已经证明了这一点了。

接下来是返回z:

这里有个小问题,就是Add函数已经完成任务了,里面的东西应该已经销毁了,怎么还能把Z的值返回到main函数里面去了呢?

其实很简单,看下面的指令:把ebp-8的值放到eax里面去,eax可是个寄存器啊,因此是不会因为程序退出而销毁的!

 然后三个pop,就是弹出的意思,每一次弹出,esp就会加加,往下走

既然add函数的使命已经完成了,那么add函数的这块空间就要回收!

指令move,把ebp 给esp! 

然后POP ebp, 

当初存在这里,就是为了现在把ebp弹到main里面去,这个ebp就咔的一声,回到了 最初的那个位置

 

 这时候esp也走人了。至此,全部的指针,都回到了main函数里面去了。带着结果回到家了!

esp和ebp又开始正式地维护main函数的栈帧。

但此时还有一条指令:

 这条指令在add函数返回的时候,就来到了call指令的下一条指令的地址,所以当时存放call指令的下一条指令的时候,就是为了返回的时候,还能回来,然后执行call以下的指令。

执行add指令,让ebp加8,此时形参x和y已经销毁了,被回收了!

 紧接着,把eax的值放到ebp-20h中去,也就是c的地址

最后,打印,然后结束程序,销毁main函数的栈帧。

因此: 

①局部变量是如何创建的?

给函数创建栈帧,再在空间里面分配变量的空间。

②为什么局部变量不初始化内容是随机的?

因为随机值是在函数在创建栈帧时,初始化成0XCCCCCCCCh时的结果。

③函数调用时参数是如何传递的?传参的顺序是怎么样的?

在还没正式调用函数的时候,已经将参数push到栈顶上,通过寄存器ebp的偏移量来找到形参。

④函数的形参和实参分别是怎么样实例化的?

在开辟的空间上,只是值相同,但是地址不相同,形参是实参的临时拷贝。

⑤函数的返回值是如何带回来的?

在调用函数之前,就把call指令的下一条指令记住了,已经压栈了。然后调用这个函数的上一个函数的ebp,将其存进去。当返回的时候,弹出这个函数的ebp的时候,就能找到之前存的原始的那个ebp,然后往下走的时候,就能走到调用这个函数的那个函数的栈顶。也就是说,是通过寄存器带回来的!

PS:本人对函数栈帧的创建和销毁的拙见,请有大佬看到的其中不妥的问题时候,可以纠正我的问题。谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山雾隐藏的黄昏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值