目录
背景知识介绍与补充
观察与研究
初始状态
根据反汇编代码进行压栈
建立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函数分配栈帧。
对这样一份代码进行研究
首先push一个ebp,就是将ebp压栈,放一个元素进去
ebp压栈后,esp指向ebp,因为esp维护的是栈顶
通过监视我们也可以看出,esp的值减了4,因为下面是高地址,上面是低地址。此时成功将ebp压栈
然后执行mov ebp,esp 意思是将esp的值赋给ebp
sub esp,0E4h 意思是将esp 的值减去0E4h(这个0E4h代表16进制数字0E4,即十进制数字228)
此时就意味着esp指向上面的某块区域了
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这样的内容
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内部创建的,而是找的之前压进栈的两个数进行计算
接下来开始返回
把ebp-8的值放进eax里去,eax是个寄存器,它不会因为程序退出而销毁,ebp-8的值就是30,即把30放进eax里面去。为啥要把30放进eax里面去,就是因为一会出去Z就销毁了,相当于用一个全局的eax把30保存起来,等回到主函数里,再使用eax。
然后开始三个pop,就是把edi,esi,ebx弹出,弹出一次,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寄存器带回来的。