以该代码为例,对函数栈帧的创建和销毁进行讲解 :
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 20;
int b = 10;
int c = 0;
c = Add(a,b);
printf("c = %d\n",c);
return 0;
}
main函数
将函数进入调试状态,并对调用堆栈进行观察。
我们发现,即使是main函数程序的入口,也是被调用的函数。关于main函数,其实是被__tmainCRTStartup函数所调用,而该函数也是被mainCRTStartup函数所调用。
分析
vs2013中存在很多寄存器,类似于eax,ebx,ecx等,以及ebp和esp,这两个寄存器存放地址,用来维护函数栈帧。
接下来,让我们先找到该c语言程序所对应的汇编代码。
int Add(int x, int y)
{
00C213C0 push ebp
00C213C1 mov ebp,esp
00C213C3 sub esp,0CCh
00C213C9 push ebx
00C213CA push esi
00C213CB push edi
00C213CC lea edi,[ebp+FFFFFF34h]
00C213D2 mov ecx,33h
00C213D7 mov eax,0CCCCCCCh
00C213DC rep stos dword ptr es:[edi]
int z = 0;
00C213DE mov dword ptr [ebp-8],0
z = x + y;
00C213E5 mov eax,dword ptr [ebp+8]
00C213E8 add eax,dword ptr [ebp+0Ch]
00C213EB mov dword ptr [ebp-8],eax
return z;
00C213EE mov eax,dword ptr [ebp-8]
}
00C213F1 pop edi
00C213F2 pop esi
00C213F3 pop ebx
00C213F4 mov esp,ebp
00C213F6 pop ebp
00C213F7 ret
int main()
{
00C21410 push ebp
00C21411 mov ebp,esp
00C21413 sub esp,0E4h
00C21419 push ebx
00C2141A push esi
00C2141B push edi
00C2141C lea edi,[ebp-0E4h]
00C21422 mov ecx,39h
00C21427 mov eax,0CCCCCCCh
00C2142C rep stos dword ptr es:[edi]
int a = 20;
00C2142E mov dword ptr [ebp-8],14h
int b = 10;
00C21435 mov dword ptr [ebp-14h],0Ah
int c = 0;
00C2143C mov dword ptr [ebp-20h],0
c = Add(a,b)
00C21443 mov eax,dword ptr [ebp-14h]
00C21446 push eax
00C21447 mov ecx,dword ptr [ebp-8]
00C2144A push ecx
00C2144B call 00C210E1
00C21450 add esp,8
00C21453 mov dword ptr [ebp-20h],eax
}
在main函数调用之前,我们会先对调用main函数的__tmainCRTStartup函数申请空间。
寄存器ebp(栈底指针)和esp(栈顶指针)分别指向栈底和栈顶。
同时,根据栈区空间的使用习惯,我们将从高地址向低地址使用空间。
接下来进入到汇编代码的分析:
1.
00C21410 push ebp
进行压栈(push)操作,在当前栈区的可使用位置压入ebp。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ebp。
2.
00C21411 mov ebp,esp
需要修改ebp的指向,将ebp的指向修改成与esp相同。
3.
00C21413 sub esp,0E4h
将esp的地址减去0E4h,相当于esp向上移动,与ebp申请维护一块新的内存空间。
4.
00C21419 push ebx
进行压栈(push)操作,在当前栈区的可使用位置压入ebx。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ebx。
5.
00C2141A push esi
进行压栈(push)操作,在当前栈区的可使用位置压入esi。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧esi。
6.
00C2141B push edi
进行压栈(push)操作,在当前栈区的可使用位置压入edi。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧edi。
7.
00C2141C lea edi,[ebp-0E4h]//显示了符号名
lea表示load effective address 加载有效地址。找到ebp-0E4h的地址,并且标记。0E4h刚好是esp刚才申请的内存空间大小。
8.
00C21422 mov ecx,39h
00C21427 mov eax,0CCCCCCCh
00C2142C rep stos dowrd ptr es:[edi]
把从edi开始向下的39h次(ecx)这么多个dword数据,全部改成0CCCCCCCh。
9.
int a = 20;
00C2142E mov dword ptr [ebp-8],14h
在ebp-8的位置,开辟一块空间存放14h,也就是将a赋值20。
10.
int b = 10;
00C21435 mov dword ptr [ebp-14h],0Ah
在ebp-14h的位置,开辟一块空间存放0Ah,也就是将b赋值10。
11.
int c = 0;
00C2143C mov dword ptr [ebp-20h],0
在ebp-20h的位置,开辟一块空间存放0,也就是将c赋值0。
12.
c = Add(a,b)
00C21443 mov eax,dword ptr [ebp-14h]
将地址ebp-14h处的值(10)赋值给寄存器eax。
13.
00C21446 push eax
进行压栈(push)操作,在当前栈区的可使用位置压入eax。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧eax。
14.
00C21447 mov ecx,dword ptr [ebp-8]
将ebp-8的值(20)赋值给寄存器ecx。
15.
00C2144A push ecx
进行压栈(push)操作,在当前栈区的可使用位置压入ecx。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ecx。
16.
00C2144B call 00C210E1
00C21450 add esp,8
call调用函数,进入Add函数,同时需要在退出函数后继续进行下一条指令,所以会在执行call时压入call指令的下一条指令的地址。
17.
00C213C0 push ebp
进行压栈(push)操作,在当前栈区的可使用位置压入ebp。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ebp。该ebp被销毁可返回main前的ebp。
18.
00C213C1 mov ebp,esp
需要修改ebp的指向,将ebp的指向修改成与esp相同。
19.
00C213C3 sub esp,0CCh
将esp的地址减去0CCh,相当于esp向上移动,与ebp申请一块新的内存空间。
20.
00C213C9 push ebx
00C213CA push esi
00C213CB push edi
进行压栈(push)操作,在当前栈区的可使用位置分别压入ebx,esi,edi。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ebx,esi,edi,最后指向edi。
21.
00C213CC lea edi,[ebp+FFFFFF34h]
00C213D2 mov ecx,33h
00C213D7 mov eax,0CCCCCCCh
00C213DC rep stos dword ptr es:[edi]
与第8步相同,通过加载有效地址找到ebp+FFFFFF34h的地址,把从edi开始向下的33h次(ecx)这么多个dword数据,全部改成0CCCCCCCh。
22.
int z = 0;
00C213DE mov dword ptr [ebp-8],0
为0开辟一块空间在ebp-8的位置。
23.
z = x + y;
00C213E5 mov eax,dword ptr [ebp+8]
将ebp+8位置的数据赋值到寄存器eax中。
24.
00C213E8 add eax,dword ptr [ebp+0Ch]
00C213EB mov dword ptr [ebp-8],eax
将ebp+0Ch中的数据与eax中的数据相加,得到eax = 30,再将eax中的数据存放到ebp-8中,也就是将z赋值为30。
25.
return z;
00C213EE mov eax,dword ptr [ebp-8]
将ebp-8中的值存放到寄存器eax中。
26.
00C213F1 pop edi
进行出栈(pop)操作,在当前栈区的最低地址可使用位置处将edi出栈。同时,因为edi栈帧出栈,esp作为栈顶指针需要指向edi的上一个栈帧。
27.
00C213F2 pop esi
进行出栈(pop)操作,在当前栈区的最低地址可使用位置处将esi出栈。同时,因为esi栈帧出栈,esp作为栈顶指针需要指向esi的上一个栈帧。
28.
00C213F3 pop ebx
进行出栈(pop)操作,在当前栈区的最低地址可使用位置处将ebx出栈。同时,因为ebx栈帧出栈,esp作为栈顶指针需要指向ebx的上一个栈帧。
29.
00C213F4 mov esp,ebp
将esp的位置指向改变成ebp的位置指向,已知ebp此时正指向Add函数栈帧的起始位置,将esp位置修改为ebp,使得Add函数失去了esp的维护。将Add函数栈帧的内存回收给操作系统。
30.
00C213F6 pop ebp
进行出栈(pop)操作,在当前栈区的最低地址可使用位置处将ebp出栈。同时,因为ebp栈帧出栈,esp作为栈顶指针需要指向ebp的上一个栈帧。因为ebp出栈,所以ebp的指向将直接变回指向main函数的起始位置指向。
31.
00C213F7 ret
进行返回操作,执行call调用函数的下一条指令,因为此时esp正指向call指令下一条指令的地址,所以可以直接进行下一步操作。函数执行重新回到main函数之中。
32.
00C21450 add esp,8
进行加法操作,将8个字节大小的地址加到esp之中,esp地址变大,箭头向下移动8个字节大小的位置。
33.
00C21453 mov dword ptr [ebp-20h],eax
将寄存器eax中的值赋值给ebp-20地址处,已知ebp-20出是变量c的所在区域,也就是将eax中的30赋值给c,完成了c = Add(a,b)的操作。
思考
局部变量的初始化
当局部变量没有进行初始化操作,就对其进行打印时,往往会出现的乱码打印是为什么?
解:
在运行过程中,当我们对main函数进行调用的时候,main函数的函数栈帧开辟过程中,已经在main函数之中添加了很多随机值0CCCCCCCh,所以当局部变量在main函数中没有进行初始化操作时,会对随机值0CCCCCCCh进行打印。
形参只是实参的一份临时拷贝
为什么说形式参数只是实际参数的一份临时拷贝?
解:
该程序的形式参数在程序运行过程中只是起到了传递值的效果,通过介质(寄存器eax,ecx)完成对应地址的加法操作,在该Add函数结束,eax和ecx进行出栈操作时,无法对实际参数产生影响,所以说形参只是实参的一份临时拷贝。
总结
函数栈帧的创建和销毁存在很多的细节,例如main函数的调用、call调用语句的地址存储等,将整个程序都紧密联系在了一起。同时esp和ebp寄存器指向的变动来起到维护函数栈帧的操作也十分灵活,能快速开辟空间,也可以快速将空间回收给操作系统。本次程序运行是在vs2013的环境下实现的,汇编代码表达足够清晰,便于我自身的理解,也对我在未来的程序编写中起到了很大的益处。