什么是函数栈帧?
理解了函数栈帧能解决什么样的问题?
函数栈帧的创建和销毁解析!
调试工具: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:本人对函数栈帧的创建和销毁的拙见,请有大佬看到的其中不妥的问题时候,可以纠正我的问题。谢谢!