函数栈帧(值得收藏)
函数栈帧的创建和销毁
前期学习的时候,我们可能有很多困惑?
比如:
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机的?
- 函数是怎么传参的?传参的顺序是怎么样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用结束后是怎么返回的?
今天知道函数栈帧的创建和销毁就都会了,其实就是修炼了自己的内功,也能搞懂后期更多的知识。
今天我们使用的环境是VS2013,不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。
同时在不同的编译器下,函数调用的过程中栈帧的创建时略有差异的,具体细节取决于编译器的实现。
寄存器
首先我们来了解一下寄存器。
这个金字塔是电脑内存的组成部分,分别是硬盘,内存,cache(缓存),寄存器
这幅图从下到上读取速度递增,但存储空间递减,且费用更高。
想必之前应该都有所了解,这里我们只介绍寄存器。
寄存器是CPU的内部组成单元,是CPU运算时取指令和数据的地方,速度很快,寄存器可以用来暂存指令、数据和地址。在CPU中,通常有通用寄存器,如指令寄存器IR;特殊功能寄存器,如程序计数器PC、sp等。
我们常见的寄存器有eax ,ebx, ecx, edx,ebp,esp。
其中后面两个寄存器是用来存放地址的,这两个地址就是用来维护函数栈帧的。
函数栈帧的创建和销毁
前面我们了解到,每一个函数的调用,都要在栈区创建一个空间。
所以我们自然要定义函数,为了方便观察,我们尽量将代码分的足够细。
-
#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 c =
0;
-
-
c =
Add(a, b);
-
-
printf(
"%d\n", c);
-
-
return
0;
-
}
如图,函数是在栈上创建的,如果这是main函数的函数栈帧,那么当程序运行到main函数时,寄存器ebp和esp中存放的地址就是main函数的首尾地址。
同理,当程序运行到哪个函数时ebp和esp就指向那个函数的空间,而这块空间就叫做这个函数的函数栈帧。
根据栈区空间的使用习惯,先使用高地址再使用低地址,所以通常,ebp称为栈底指针,esp称为栈顶指针。
然后我们调试起来,观察函数的调用堆栈。
我们发现,main函数被调用了,那么是被谁调用了呢?
我们继续走,当代码走完会发现是__tmainCRTstartup这个函数内部调用了main函数
然后再将main函数的返回值给mainret。
而同时,__tmainCRTstartup也是被mainCRTstartup这个函数调用的。
所以就可以大概了解这个程序的函数在栈中的空间分配。
那么下面我们就来具体分析到底是怎么做的。
我们按一下F10,然后在调试窗口中找到 转到反汇编 选项
然后就会出现这个界面,这是C语言中对应的汇编代码。
为了能够方便我们观察,我们将这个显示符号名选项关闭。
首先第一步是push
因为执行main函数时__tmainCRTStartup会先调用main函数,所以寄存器ebp和esp首先指向的应该是__tmainCRTStartup函数的空间,如下图所示。
而push的意思是压栈,即这里是将ebp放到栈顶,而随着栈顶的移动,指向栈顶的esp也应该移动到栈顶,在32位下地址大小为4字节,所以这里也是向上移动(减)4字节。
mov
这里就是把esp的值给ebp。
sub
根据代码,是将esp的值减去0E4h(十六进制),即将esp向上移动0E4h。
而这里ebp和esp所包含的空间发生了变化,其实这里的空间就是为main函数开辟的空间。
push
这里push了三次,将ebx,esi,edi 放进栈内。
lea(load effictive address加载有效地址),mov,mov,rep stos
根据代码实则是将ebp的地址减去0E4h的地方即edi,向下39h次(直到ebp),将dword(一个word为2个字节,dword为4字节)个字节全部置为0cccccccch。
下面才是正式开始执行C语言的有效代码
根据代码,是将0Ah放到ebp-8的位置,并且大小为4个字节,实际意思是为a开辟空间。
而如果创建变量时,没有初始化,变量中就放默认值,即为这里的cccccccc,所以打印时就会出现烫烫烫烫烫烫的情况。
然后就是创建b变量和c变量
同理是将14h放到ebp-14h的位置;将0放到ebp-20h的位置。
然后才是调用Add函数
首先根据代码是将ebp-14h(即b)的值放到eax里面。
然后将eax压栈。
同理,将ebp-8(即a)的值放到ecx里面。
然后将ecx压栈。
其实这两个步骤就是传参。
Add函数中的步骤实则和main函数的创建类似,这里就不再重复。
这里是将ebp+8(a)的值放到eax里面,然后将ebp+0Ch(b)的值加到eax中,最后将eax的值放到ebp-8(z)中。
返回时将ebp-8(z)放到eax中,因为函数结束后z会被销毁,而寄存器不会被销毁。
这里的pop是将寄存器出栈,然后将ebp赋给esp
接着继续出栈,把栈顶弹出放到ebp里边。而此时的栈顶就是之前存放的main函数的ebp
pop之后ebp指向main函数的栈底
ret:跳转会调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回值,则放入eax中。
形参的销毁
即将esp向下移动,使esp和ebp之间的空间减少,从而达到销毁形参。
最后将eax的值放到ebp-20h(c)中。