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指令下一条指令的地址。返回值是通过寄存器来传递回来的。