时间:2022-11-17 10:15:06 | 栏目:C代码 | 点击:次
大家在学习的时候一定有以下困惑: 局部变量是怎么创建的?为什么局部变量的值是随机值?函数是怎么传参?传参的顺序是怎样的?形参与实参是什么关系?函数调用是怎么做到的?函数调用完成不是销毁了吗,如何带回的返回值?
以上这些都可以通过了解函数栈帧的创建与销毁来理解。接下来我就带大家来了解函数栈帧的创建与销毁。
本次使用的编辑器是VS2013,因为越先进的编辑器,越不容易观察到底层。好了,我们回到正题。
寄存器里有eax,ebx,ecx,edx,ebp,esp等,我们今天要重点了解的是ebp与esp这两个寄存器。
这两个寄存器中存放的是地址,整两个地址使用来维护函数栈帧的。每一个函数调用,都要在栈区创建一个空间,这块空间叫做函数栈帧,用esp与ebp来维护。
#include<stdio.h> int Add(int x, int y) { int z = 0; z = x + y; return z; } int main() { int a = 10; int b = 20; int ret = Add(a, b); printf("ret=%d\n", ret); return 0; }
这个代码只是一个很简单的实现了加法函数,不用多说。
接下来开始调试程序来看底层代码如何实现,首先打开调用堆栈
从这里可以清楚地知道,main函数之前还有两个函数在调用,所以大家平时认为的main函数就是最初的开始是错误的哦。
在栈的使用中,是从高地址开始的。其中ebp指向栈底,esp指向栈顶。
main函数被调用后,程序会为main函数分配栈空间。接下来调试并右击鼠标转到反汇编。可以看到
这些代码的意思是
将ebp的值压栈,然后将esp赋给ebp,给esp减去0E4h的大小,之后分别将ebx,esi,edi压栈
压栈的意思是将元素放到栈顶。在上面调用堆栈可以看到main函数由__tmainCRTStartup调用。图解如下:
值得一提的是,在栈顶放元素是esp会自动指向新放元素的上方。
上图中我们很容易的看出在调用main函数是为main函数开辟的栈空间即栈帧,并且将esp的值给ebp,ebp和esp指向同一块空间,然后esp变小指向上面的区域,接下来将ebx,esi,edi压栈。
接下来这四句,lea是加载有效地址,从上图中,我们知道ebp指向的地址,那么edi存放的就是ebp-0E4h的地址也就是③esp处的地址,最后一句rep stos dword ptr es:[edi] 意思是从edi里面重复拷贝ecx次eax的内容。一次拷贝四个字节,dword是四个字节的意思。
接下来这三个汇编代码的意思就是将值放入相应的空间。至于为什么是为什么是ebp-8和ebp-20,这个和编译器有关,就不过多叙述。
由汇编代码可知,接下来就开始创建形式参数,将要传递的参数值存入eax与ecx这两个寄存器中并压入栈顶,所以创建的形式参数并不在add函数的函数栈帧哦,并且我们之前常说的形参是实参的一份临时拷贝无疑是非常正确的。
接下来开始add函数调用。call是调用的意思,后面是add函数的地址用来找到调用函数。这里值得一提的是call指令下一条指令的地址被存储来方便执行完add函数可以跳回来。
Add函数创建栈帧的过程其实和main函数一样的,先将ebp压栈方便找到指向main函数栈底的空间,再将esp的地址存放到ebp里面,此时,esp和ebp指向同一位置,再将esp上移0CCh个位置,然后就是ebx,esi,edi压栈,这里不多作说明,由下图可清晰理解:
接下来初始化z与执行加法也没什么要说的,z=x=y是找到之前创建的形参来进行加法。并将结果移动到z里。
返回z的操作是将z的值存放到eax这个寄存器中,所以函数销毁变量与返回的值这个操作并不冲突。
最后销毁add函数栈帧,pop就是弹出元素然后把元素放入后面的寄存器,每次pop都会销毁一个栈顶元素然后esp自动++往下挪移。三下pop之后要回收空间了,操作是把ebp的值传给esp这样esp指向的空间是main函数的栈顶,然后popebp这个操作会将之前栈底存放的之前存放的main函数的栈底指针传给ebp,这样ebp就指向了main函数的栈底。如下图:
接下来销毁main函数的操作也与之前一样,就不细说了。