很喜欢一部动漫,《星游记》,有这样一句话:“能让我们进步的事情,都是由恐惧开始的,把最恐怖的地方,变成最美丽的景色,这才是男人的梦想。”
在学习函数栈帧前,我们不妨问自己这样几个问题!
1.局部变量是怎么创建的?
2.为什么不初始化的局部变量默认值是随机值?
3.函数是怎么传参的?传参的顺序又是如何?
4.形参和实参是什么关系?
5.函数调用是怎么做的?
6.函数调用结束后怎么返回?那返回值呢?
我演示所使用的编译器是VS2013,低版本的编译器所看到的内容会全面一些,注意:随着编译器的不同,函数栈帧的创建和销毁会略有差异!!!
进入正题前,让我们了解一下待会所用的内容!
图片可以鼠标左键点开右击选择放大哦!!!
一:注意:函数栈帧顾名思义是在栈区操作的,栈区的特点是向上开辟内存(下面是高地址,上面是低地址)。
二:寄存器的了解
1. eax、ebx、ecx、edx
2.ebp、esp(用来存放地址)
这里着重理解第2类:提到函数栈帧就离不开esp、ebp 俩个寄存器,二者地址之间的内存空间就是用来维护函数栈帧的(其中esp寄存器叫做栈顶指针,ebp寄存器叫做栈底指针),每调用一个函数就要在栈区创建一个栈帧,说白了就是给调用的函数在栈区开辟一块内存空间罢了。
3.在汇编中会看到俩个常见指令:push表示的是压栈:从栈顶压入一个元素。pop表示的是出栈:从栈顶删除一个元素。对应的栈顶指针也会做出相应的偏移。
进入正题!
上面二-2中提到,每调用一个函数就要在栈区开辟一块内存空间,那main()函数是由谁来调用的呢!请看下图
![](https://i-blog.csdnimg.cn/blog_migrate/6fcc361134a15d18b933097fb69f5714.png)
上图很好的解释了主函数main()是被谁调用而来的,注意:待会画栈区图的时候我会直接从__tmainCRTStartup开始。
本次我们的测试案例代码如下
#include<stdio.h>
int add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = add(a, b);
printf("%d", c);
return 0;
}
在调试界面中将上述代码右击鼠标转到反汇编,接着继续点击右键注意显示符号名不要勾选这样就可以看到十六进制的数值,方便我们接下来的走读(F11)。
int add(int x, int y)
{
004513C0 push ebp
004513C1 mov ebp,esp
004513C3 sub esp,0CCh
004513C9 push ebx
004513CA push esi
004513CB push edi
004513CC lea edi,[ebp+FFFFFF34h]
004513D2 mov ecx,33h
004513D7 mov eax,0CCCCCCCCh
004513DC rep stos dword ptr es:[edi]
//-----------上面的指令是为函数开辟函数栈帧-------------
int z = x + y;
004513DE mov eax,dword ptr [ebp+8]
004513E1 add eax,dword ptr [ebp+0Ch]
004513E4 mov dword ptr [ebp-8],eax
return z;
004513E7 mov eax,dword ptr [ebp-8]
}
004513EA pop edi
004513EB pop esi
004513EC pop ebx
004513ED mov esp,ebp
004513EF pop ebp
004513F0 ret
//---------------------------------------------------
int main()
{
00451400 push ebp
00451401 mov ebp,esp
00451403 sub esp,0E4h
00451409 push ebx
0045140A push esi
0045140B push edi
0045140C lea edi,[ebp+FFFFFF1Ch]
00451412 mov ecx,39h
00451417 mov eax,0CCCCCCCCh
0045141C rep stos dword ptr es:[edi]
//-----------上面的指令是为函数开辟函数栈帧-------------
int a = 10;
0045141E mov dword ptr [ebp-8],0Ah
int b = 20;
00451425 mov dword ptr [ebp-14h],14h
int c = 0;
0045142C mov dword ptr [ebp-20h],0
c = add(a, b);
00451433 mov eax,dword ptr [ebp-14h]
00451436 push eax
00451437 mov ecx,dword ptr [ebp-8]
0045143A push ecx
0045143B call 004511DB //这里的call指令是重点
00451440 add esp,8
00451443 mov dword ptr [ebp-20h],eax
下面将展示以上代码在栈区创建和销毁的每一步细节,大家跟着我的思路来哈~!
![](https://i-blog.csdnimg.cn/blog_migrate/e31cf73c8583ade1b3b11e3e48da0b9f.png)
在图2基础上执行了3条汇编指令后的栈区图。执行指令后esp和edp的地址变化在图里都有展示!
![](https://i-blog.csdnimg.cn/blog_migrate/bcfb8ffd986fdf4b4919865ab4e85db3.png)
在图3的基础上又执行了7条汇编指令后的栈区图。
![](https://i-blog.csdnimg.cn/blog_migrate/9ff07c130e124897d26aef0f93d93c4f.png)
这里值得注意的是,随着汇编指令的执行main函数的函数栈帧进行了初始化,内容是0cccccccc,这里给不初始化局部变量的值为随机值(烫烫烫)埋下了伏笔,欲知后事如何!请见图5哈~
![](https://i-blog.csdnimg.cn/blog_migrate/b757bae4accdf84069e897b98bd55a70.png)
图5中随着汇编指令的进行我们也在main函数的函数栈帧中创建了a,b,c三个变量并进行初始化(养成好的变成习惯),如果我们不进行初始化那这三个变量的值都是随机值0cccccccc。这里插入一个例子,如果运行程序就会打印烫烫烫(第一次见到以为电脑发烫了哈哈哈哈哈~!)
#include<stdio.h>
int main()
{
char arr[] = {'a','b','c'};
printf("%s",arr);
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/f692fde168d8f913d67b965eb17d8ab4.png)
继续往下走!重点来了!
![](https://i-blog.csdnimg.cn/blog_migrate/7bd87f2c5ee996759e8528e89ad01c32.png)
图7是call指令的详解,紧接着我们进入add函数的汇编代码,由于add函数和main函数开辟栈帧的方式是类似的,所以add函数的函数栈帧将用图8一步到位。
![](https://i-blog.csdnimg.cn/blog_migrate/eb9b83150ae61fa8a3ef295dd419caf6.png)
下面接着在add的函数栈帧中进行变量的创建(有意思的终于来了!)
![](https://i-blog.csdnimg.cn/blog_migrate/5184b1c2d542670d16ece8049f6c6676.png)
图九内容很丰富,就不做解释了!
![](https://i-blog.csdnimg.cn/blog_migrate/537e0f95f5f4c2ffa9881901e2c1d55c.png)
这里再次强调图10中的ret指令,这里解释我分为俩个步骤:1.返回到call指令的下一条指令的地址处,并且pop掉esp所指向的位置(正是图中紫色的内存框被pop)还又图11就完结了,加油!!!
![](https://i-blog.csdnimg.cn/blog_migrate/afa8ef9845f0a36f8ac2eb7fdf6eac48.png)