函数栈帧的创建与销毁

发布时间:2024年01月14日

目录

背景知识介绍与补充

观察与研究

初始状态

根据反汇编代码进行压栈

建立main函数的栈帧

建立Add函数的栈帧?

?完整栈帧建立图

栈帧的销毁?

局部变量是怎么创建的?

为什么局部变量的值是随机值(不初始化)?

函数是怎么传参的?传参的顺序是什么?

形参和实参是什么关系?

函数调用是怎么做的?

函数调用结束后是怎么返回的??

背景知识介绍与补充

越高级的编译器,越不容易观察函数栈帧的创建与销毁。且不同编译器下是略有差异的。

寄存器:eax,ebx,ecx,edx,ebp,esp;要理解函数栈帧,必须理解esp和ebp这两个寄存器,esp和ebp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

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

假设程序开始调用main函数,main函数这块空间就由esp和ebp进行维护。准确来说正在调用哪个函数,我这个esp和ebp维护的就是哪块空间的函数栈帧。假设我现在调用Add函数,那么esp和ebp为Add函数维护函数栈帧了,esp和ebp之间的空间就是为Add函数开辟的空间。

在vs2013中,main函数也是被其他函数调用的。mainCRTStartup调用__tmainCRTStartup,__tmainCRTStartup又调用了main函数。vs2015及后来的版本则观察不到。

在栈区上,开始调用为main函数分配栈帧,然后调用Add函数,为Add函数分配栈帧。同理在调用main函数的之前,会为mainCRTStartup和__tmainCRTStartup函数分配栈帧。

观察与研究

对这样一份代码进行研究

初始状态

根据反汇编代码进行压栈

建立main函数的栈帧

首先push一个ebp,就是将ebp压栈,放一个元素进去

?ebp压栈后,esp指向ebp,因为esp维护的是栈顶

通过监视我们也可以看出,esp的值减了4,因为下面是高地址,上面是低地址。此时成功将ebp压栈

然后执行mov ebp,esp 意思是将esp的值赋给ebp?

通过监视我们也可以看出,确实是将esp的值赋给了ebp?

sub ? ? ? ? esp,0E4h ?意思是将esp 的值减去0E4h(这个0E4h代表16进制数字0E4,即十进制数字228)?

此时就意味着esp指向上面的某块区域了

现在紫色这块空间,就是为main函数申请的空间,即main函数的栈帧 。

002B1939 ?push ? ? ? ?ebx ?
002B193A ?push ? ? ? ?esi ?
002B193B ?push ? ? ? ?edi? 然后就是三次push,push完后exp往上挪

002B193C ?lea ? ? ? ? edi,[ebp-24h]? 。lea即load effective address 加载有效地址。意思就是把ebp-0E4h放到edi里面去。

002B193F ?mov ? ? ? ? ecx,9 ?
002B1944 ?mov ? ? ? ? eax,0CCCCCCCCh? 然后是两步赋值操作

002B1949 ?rep stos ? ?dword ptr es:[edi] 真正起效果的是这句话,意思是要把刚刚从edi这个位置开始,向下把ecx这么多个空间全部改成0xcccccccc这样的内容

一共变了ecx(9个)空间?

002B194B ?mov ? ? ? ? ecx,2BC006h ?
002B1950 ?call? ? ? ? ? ?002B1320?? ?这两句暂时不用管,不影响理解

接着,将10放到ebp-8,20放到ebp-14h,0放到ebp-20h

?

这同时也就说明了为什么变量需要初始化。不初始化里面放的就是随机值。?

??c = Add(a, b);
002B196A ?mov ? ? ? ? eax,dword ptr [ebp-14h] ?
002B196D ?push ? ? ? ?eax ?
002B196E ?mov ? ? ? ? ecx,dword ptr [ebp-8] ?
002B1971 ?push ? ? ? ?ecx
?

意思是把ebp-14h的值即20放到eax里面,把eax压栈,把ebp-8的值即10放进ecx,把ecx压栈。然后esp向上挪

这两个动作其实就是传参。

接下来执行call

我们发现call指令把002B1977这个地址压到栈里面去了

建立Add函数的栈帧?

然后我们就真正进入了Add里面,开始执行Add里面的一些列动作

?

002B20A1 ?mov ? ? ? ? ebp,esp ,把esp的值赋给ebp

002B20A3 ?sub ? ? ? ? esp,0CCh? ?把esp减去0CCh ,这其实是为Add开辟空间。

002B20A9 ?push ? ? ? ?ebx ?
002B20AA ?push ? ? ? ?esi ?
002B20AB ?push ? ? ? ?edi? ?将这三个压栈

然后初始化?

?

002B20CC ?mov ? ? ? ? eax,dword ptr [ebp+8]? ? ebp+8就找到了10,把10放到eax里
002B20CF ?add ? ? ? ? eax,dword ptr [ebp+0Ch]? ebp+12就找到了20,eax加上20得到30
002B20D2? ?mov ? ? ? ? dword ptr [ebp-8],eax ?? 再把eax的值放到ebp-8里面,即放到了Z里面

参数是从右向左传的,且形参根本不是在Add内部创建的,而是找的之前压进栈的两个数进行计算

?所以形参a',b'就是实参a,b的两份拷贝,当我使用形参的时候,改变形参根本不影响实参。

?完整栈帧建立图

栈帧的销毁?

接下来开始返回

把ebp-8的值放进eax里去,eax是个寄存器,它不会因为程序退出而销毁,ebp-8的值就是30,即把30放进eax里面去。为啥要把30放进eax里面去,就是因为一会出去Z就销毁了,相当于用一个全局的eax把30保存起来,等回到主函数里,再使用eax。

然后开始三个pop,就是把edi,esi,ebx弹出,弹出一次,esp就++

?通过监视,也可以得出esp确实是增加了

然后看这个划线的指令,mov esp ebp,把ebp赋值给esp?,此时esp和ebp就都指向同一个位置了

然后pop ebp,ebp指向的这个位置存储的是指针main函数的ebp,所以这时候,ebp就回到了main函数原来ebp的位置

这个时候就回到了main函数里面,main函数的栈帧又由esp和ebp开始维护。

然后执行ret指令,现在esp指向的是call指令的下一条指令的地址。而且当我们回到main函数里面也应该从call指令的下一条指令开始执行,我们如何回到那呢,恰好栈顶就是call下一条指令的地址。ret指令就是弹出了栈顶的元素。从而回到call指令的下一条指令

所以一开始我们存call指令下一条指令的地址就是为了函数调用完后回来,继续从call指令下一条指令这个地方继续执行

然后执行 esp +8 ,让esp执行edi,此时相当于把形参x,y也还给操作系统了。

002B197A ?mov ? ? ? ? dword ptr [ebp-20h],eax 然后把eax里的值放到ebp-20h里,即把eax里面的值30放到c 里面去,c就是30。所以返回值首先放到寄存器里面去,等真正返回main函数里面去的时候在把这个值放到变量C里面去。

接下来就是类似于Add栈帧销毁的main函数的销毁,这里就不再赘述。

到此就是一个完整的函数栈帧创建与销毁的过程。?

局部变量是怎么创建的?

首先为函数分配栈帧空间,栈帧空间里初始化一部分空间后,然后给局部变量在栈帧里分配空间

为什么局部变量的值是随机值(不初始化)?

因为随机值是随机放进去放的,如果初始化了就把随机值给覆盖了。

函数是怎么传参的?传参的顺序是什么?

还没有调用函数的时候, 就已经把参数从右向左开始压栈压了进去,当真正进入函数里面的时候,通过指针的偏移量找回了形参。

形参和实参是什么关系?

相参和实参值是相同的,空间是独立的。相参是实参的拷贝,改变形参不会影响实参。

函数调用是怎么做的?

见上。

函数调用结束后是怎么返回的??

调用之前就把call指令下一条指令的地址记住了入栈了,当调用函数要返回的时候,弹出ebp就可以找到上一个函数的ebp,指针向下走的时候就可以找到esp的顶,然后就回到了栈帧空间。再通过call指令下一条指令的地址,就可以跳转到call指令下一条指令的地址,进行返回。返回值是通过eax寄存器带回来的。

文章来源:https://blog.csdn.net/qq_52433890/article/details/135540396
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。