目录
越高级的编译器,越不容易观察函数栈帧的创建与销毁。且不同编译器下是略有差异的。
寄存器: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函数分配栈帧。
对这样一份代码进行研究
首先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里面的一些列动作
?
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寄存器带回来的。