目录
1、函数栈帧相关概念
2、函数栈帧的创建与销毁
2.1main函数的创建
2.2main函数中变量的创建
2.3 Add函数栈帧的创建
2.4 Add函数栈帧的销毁
在C语言的学习中你可能有很多问题:
局部变量是怎么创建的?为什么局部变量的值是随机的?函数是怎么传参的?传参的顺序是怎样的?形参和实参的关系是什么?函数是如何被调用的?函数调用结束后又是怎么返回的?了解函数栈帧的创建和销毁之后,这些你就都明白了。不同的编译器函数栈帧的创建是略有差异的,具体的细节则取决于编译器的实现。对于初学者而言,越高级的编译器越不容易学习和观察,所以不要使用太高级的编译器,本博客在学习栈帧的创建和销毁时使用的是VS2013。
在了解函数栈帧之前需要先了解的什么是寄存器。寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。比如eax、ebx、ecx、edx、ebp 和esp等等。最值得注意的两个寄存器是ebp (栈底指针)和esp(栈顶指针),这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。
push 压栈:给栈顶放元素进去
pop 出栈:从栈顶删除元素
接着我们来了解函数栈帧的概念。
函数栈帧是编译器实现函数调用的一种数据结构,记录函数调用时产生的相关信息。每一个函数调用,都要在栈区创建一个空间。以main函数为例,我们来看看是如何分配栈空间的:
我们以一段代码为例,看看函数栈帧是如何创建的。
对上方代码进行调试,选中调用堆栈窗口,我们发下其实main函数也是被其他函数调用的。
调用main函数的函数是__tmainCRTStartup函数,而__tmainCRTStartup函数又是在mainCRTStartup函数内部调用的。
为了帮助我们更清楚的看大栈帧的创建过程,我们看看上述代码对应的反汇编代码。
int main()
{
007318B0 push ebp
007318B1 mov ebp,esp
007318B3 sub esp,0E4h
007318B9 push ebx
007318BA push esi
007318BB push edi
007318BC lea edi,[ebp-24h]
007318BF mov ecx,9
007318C4 mov eax,0CCCCCCCCh
007318C9 rep stos dword ptr es:[edi]
007318CB mov ecx,73C003h
007318D0 call 0073131B
int a = 10;
007318D5 mov dword ptr [ebp-8],0Ah
int b = 20;
007318DC mov dword ptr [ebp-14h],14h
int c = 0;
007318E3 mov dword ptr [ebp-20h],0
c = Add(a, b);
007318EA mov eax,dword ptr [ebp-14h]
007318ED push eax
007318EE mov ecx,dword ptr [ebp-8]
007318F1 push ecx
007318F2 call 007310B4
007318F7 add esp,8
007318FA mov dword ptr [ebp-20h],eax
printf("%d\n", c);
007318FD mov eax,dword ptr [ebp-20h]
00731900 push eax
00731901 push 737B30h
00731906 call 007310D2
0073190B add esp,8
return 0;
0073190E xor eax,eax
}
00731910 pop edi
00731911 pop esi
00731912 pop ebx
00731913 add esp,0E4h
00731919 cmp ebp,esp
0073191B call 00731244
00731920 mov esp,ebp
00731922 pop ebp
00731923 ret
007318B0 push ebp //将edp压入栈帧
007318B1 mov ebp,esp //将esp的值赋给edp
007318B3 sub esp,0E4h //esp-0E4h 将esp向上(低地址方向)移动4个字节
007318B9 push ebx //接下来三行是将 ebx esi edi 压入栈顶
007318BA push esi
007318BB push edi
007318BC lea edi,[ebp-24h] //然后将main函数的函数栈帧初始化为0cccccccch
007318BF mov ecx,9
007318C4 mov eax,0CCCCCCCCh
007318C9 rep stos dword ptr es:[edi]
int a = 10;
007318D5 mov dword ptr [ebp-8],0Ah //把0Ah赋值给内存地址为ebp-8中的双字节的空间
int b = 20;
007318DC mov dword ptr [ebp-14h],14h //把14h赋值给内存地址为ebp-14h中的双字节的空间
int c = 0;
007318E3 mov dword ptr [ebp-20h],0 //把0赋值给内存地址为ebp-20h中的双字节的空间
当abc变量创建好了之后,开始调用add函数。分别将eax(20)和ecx(10)压入栈顶。实际上就是在为Add函数传参
c = Add(a, b);
007318EA mov eax,dword ptr [ebp-14h]
007318ED push eax
007318EE mov ecx,dword ptr [ebp-8]
007318F1 push ecx
007318F2 call 007310B4
007318F7 add esp,8
接着使用call指令将call指令的下一条指令的地址压入栈顶,等Add函数调用结束后,就会回到call指令的下一条指令继续执行。
进入Add函数后,前面的几条指令跟进入main之前的几条指令一样,给函数准备栈帧并对其进行初始化。
随后在ebp-8的空间创建临时变量z并初始化为0,再通过ebp+8和ebp+0Ch找到main函数中传递的a,b参数作为形参x,y,相加得到的值赋给eax,再由eax把值赋给z。
00731910 pop edi //将esp所指的地址的值赋给edi,再将esp的值增加4字节
00731911 pop esi //将esp所指的地址的值赋给esi,再将esp的值增加4字节
00731912 pop ebx //将esp所指的地址的值赋给ebx,再将esp的值增加4字节
00731920 mov esp,ebp //将ebp的值赋给esp,并不是将ebp所指向的内存空间的值赋给esp
00731922 pop ebp //将esp所指的地址的值赋给ebp,再将esp的值增加4字节
00731923 ret //执行完该命令后,自动返回call指令的下一行
现在我们可以轻松回答文章开篇提出的问题。
1)局部变量是怎么创建的?
函数开辟栈帧空间初始化之后,为局部变量分配空间。
2)为什么局部变量的值是随机的?
局部变量的值是随机放进去的,在初始化之后才进行了覆盖。
3)函数是怎么传参的?
调用函数时,先push压栈,通过指针偏移量来传参。
4)形参和实参的关系是什么?
形参压栈开辟空间,它与实参在空间上是独立的,它是实参的一份临时拷贝
5)函数是如何被调用的?
开辟栈空间,然后传参,调用
6)函数调用结束后又是怎么返回的
将返回值放在寄存器中,以文中的Add函数为例,当返回函数时,放入局部变量c中