函数栈帧的创建与销毁

目录

背景知识介绍与补充

观察与研究

初始状态

根据反汇编代码进行压栈

建立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寄存器带回来的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值