C语言函数栈帧的创建和销毁
看完本文你能了解什么?
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机值?
- 函数是怎么传参的?传参的顺序是怎么样的?
- 实参和形参是什么关系?
- 函数调用是怎么做的?
- 函数调用结束后怎么返回的?
寄存器
我们常用的寄存器有eax,ebx,ecx,edx。不过我们这节主要用到的是ebp和esp这两个寄存器。
函数栈帧
ebp和esp这两个寄存器中存放的是地址。这两个地址是用来维护函数栈帧的。
每一个函数调用,都要在栈区创建一个空间。

假如说要调用main函数就要在栈区创建一个空间

这个空间是为main函数开辟的,那么我们就称这个空间是为main函数开辟的一块函数栈帧。
这个空间是ebp和esp这两个寄存器来维护的。

ebp作为寄存器存储的是如图所指向的地址。(寄存器是一块区域,是一个存储空间)
esp作为寄存器存储的是如图所指向的地址。
ebp和esp中间的这块空间是由这两个寄存器来维护的。
一般我们把ebp叫做栈底指针,把esp叫做栈顶指针。
为啥叫这个呢?
实际上我们可以把这个看成一个栈,从高地址向低地址是不是那个地址在被使用消耗啊,如果想使用地址只能往上使用,就像栈一样,只能往栈顶放数据。
在vs2013中,main函数也是被其他函数调用的。
main函数被__tmainCRTStartup函数调用,而__tmainCRTStartup函数被mainCRTStartup函数调用。
int Add(int x, int y) {int z = 0;z = x + y;return z;
}int main() {int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%c\n", c);return 0;
}
我们调用main函数,会在栈区分配空间。然后往下走,走到c = Add(a, b);又去调用Add函数去了,给Add函数在栈区分配空间。
这个时候ebp和esp这两个指针就去维护Add函数去了。

那么按照道理,__tmainCRTStartup和mainCRTStartup函数在被调用的时候也会有一个对应的函数栈帧。

右击鼠标转到反汇编

在调用main函数前,会创建一个__tmainCRTStartup函数栈帧

进入main函数第一步就是push,也就是压栈。

压栈操作后,就像这样:

__tmainCRTStartup函数栈帧上面出现了一个ebp元素。同时esp指针也指向ebp元素的上面了。
下一步是mov,把esp的值给ebp。

这个时候esp和ebp指向了同一个地方

然后下一步是sub,给esp减去0E4h。

因为下面是高地址,上面是低地址,所以减就意味着esp在图上往上走。esp和ebp之间的那个新的空间就是main函数的栈帧空间。

然后进行3次push,给顶上压上3个元素。

然后esp也依次指向ebx上面,esi上面,edi上面。停在edi上面。

然后进入lea,这里的[]里面的一坨东西看着很费劲

我们把显示符号名勾上就会变成这样

把ebp-0E4h放到edi里面去,后面两个mov也是把39h放到ecx里面,把0CCCCCCCCh放到eax里面去。
接下来一步比较重要

这一步是把刚刚edi往下的39h这么多个dword(一个word两个字节,dword是double word四个字节)个数据改成0CCCCCCCCh。
压栈(push):给栈顶放一个元素。
出栈(pop):从栈顶删除一个元素。
然后进入mov,把0Ah(也就是10)给到ebp-8的位置上。

我们假设一个紫色框代表4个字节


我们把a=10放进了ebp-8里面,原来的CCCCCCCC就被10替换了。
这也可以说明我们没有初始化的时候内存里面放的就是CCCCCCCC ,之前我们有一节里面打印字符数组没有加/0,导致的打印烫烫烫烫烫烫烫烫就是这个原因。
然后进入mov,把14h放到ebp-14h(相当于ebp-20)上。


c的创建也类似,然后我们创建好abc之后,我们调用Add函数。

我们进入mov,把ebp-14h给eax。这个ebp-14h不就是b吗?也就是相当于把20给到eax里面去了。
然后push,eax压栈。eax里面放的是20,实际上把20给放进去了。

然后mov,把ebp-8(也就是a的值10)给了ecx。
接着push,ecx压栈。ecx里面放10。

上面两步其实是传参。
接下来call,就是调用,

执行call指令会把这个00C21450(也就是call指令的下一条指令的地址)给压栈。

为什么要压栈这个呢?因为我们call指令执行了之后会进入被调用函数的内部,进去了怎么出来呢?这就要靠call指令的下一条指令了。回来的时候就会找到这个地址,然后从这个地址往下执行。
然后就进入Add函数里面了。

进入push,把ebp压栈。

然后mov,把esp的值给ebp。

然后sub,给esp-0CCh。



然后继续3次push,3次压栈


然后lea,把ebp+FFFFFF34h加到edi里面去。

然后把33h,放到ecx里面,把0CCCCCCCCh放到eax里面去
然后rep,从edi这个位置开始,向下33h的内容初始化成0CCCCCCCCh。
然后进入mov,把0给ebp-8。相当于z=0。


然后mov,把ebp+8的值放到eax里面。eax=a=10。
接着add,把ebp+0Ch(也就是ebp+12)的值加到eax里面。eax=10+b=10+20=30。
然后mov,把eax的值放到ebp-8里面。也就是说z从0变成了30。


从上面我们可以看到,函数调用的时候没有主动创建x,y这两个形参,而是直接把a,b的参数调用过去了。
传参把a,b的值进行了压栈。

我们函数传参,还没有调用Add函数的时候我们就先把a,b传过去了,压栈了两个参数。
然后进入Add函数内部进行计算的时候,我们就把压栈的两个参数进行运算。把这两个参数加起来放到z里面去。
所以我们常说,形参是实参的一份拷贝。
然后我们继续mov,把ebp-8的值放到eax(eax是个寄存器)里面去。
我们常说这个z,return z;在函数调用完就销毁了,销毁了怎么传值出来的呢?就是靠的寄存器传递的,放到这样一个全局的寄存器里面就安全了。

接下来这个3个pop,就相当于出栈3次。


然后我们z的值都给寄存器了,那么Add函数的函数栈帧按道理来说也因该被释放掉了。
然后进入mov,把ebp赋值给esp。


然后pop,把ebp出栈。这个棕色框就出栈了。然后ebp指针指向的值就是下面的ebp区域了。

ebp出栈了之后,这个ebp指针指向了下面,因为这个出栈的ebp是为main函数创建的。

然后esp指针顺势指向了00C21450。

也就相当于我们回到了main函数的函数栈帧里面。
然后是ret。ret指令会让子程序返回到调用函数的代码处。但问题是它怎么知道回到哪里去呢?

我们注意,现在的栈顶是00C21450,也就是上面说的call指令的下一条指令的地址。ret指令返回的时候其实就是在栈顶返回了call指令的下一条指令的地址。这样才能返回之前的地方。ret后00C21450相当于也pop出去了。


然后add,esp+8。


然后mov,把eax的值给ebp-20h。

也就是把之前Add函数存在eax寄存器里面的返回值给c。


c的值从0变成了30。
答案:
局部变量是怎么创建的?
在函数栈帧里面划定一块区域给局部变量。
为什么局部变量的值是随机值?
因为局部变量不初始化的时候,值是我们随机放进去的。就像这里面的CCCCCCCC。
函数是怎么传参的?传参的顺序是怎么样的?
还没调用函数的时候就push,push,把这两个要用的实参压栈压进去。然后调用的时候通过指针偏量来调用这两个压栈压进去的值。
实参和形参是什么关系?
形参是实参的一份拷贝。但是形参不会影响实参。
函数调用是怎么做的?
上面讲过。
函数调用结束后怎么返回的?
我们在调用函数之前就把call指令的下一条指令的地址压栈压进去了。当我们调用完函数后,弹出ebp,和call指令下一条指令的地址后,esp指针往下走的时候就可以跳转到call指令下一条指令的地址。返回值是通过寄存器来传递回来的。