每次调用一个函数,都要为该次调用的函数实例分配栈空间。为单个函数分配的那部分栈空间就叫做栈帧(Stack Frame)。
用一个简单函数Add来理解栈帧,它的功能是求两个数的和。
以下代码运行在Microsoft Visual Studio 2015 X86平台下。
#include<stdio.h>
#include<stdlib.h>
int Add(int i, int j)
{
int ret = 0;
ret = i + j;
return ret;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
system("pause");
return 0;
}
在main函数处添加断点,开始调试。打开函数调用堆栈看到调用堆栈如图所示。
由此可知,main函数也是被其他函数调用。转到反汇编查看main函数的汇编代码
int main()
{
000E16E0 55 push ebp
000E16E1 8B EC mov ebp,esp
000E16E3 81 EC E4 00 00 00 sub esp,0E4h
000E16E9 53 push ebx
000E16EA 56 push esi
000E16EB 57 push edi
000E16EC 8D BD 1C FF FF FF lea edi,[ebp-0E4h]
000E16F2 B9 39 00 00 00 mov ecx,39h
000E16F7 B8 CC CC CC CC mov eax,0CCCCCCCCh
000E16FC F3 AB rep stos dword ptr es:[edi]
int a = 10;
000E16FE C7 45 F8 0A 00 00 00 mov dword ptr [a],0Ah
int b = 20;
000E1705 C7 45 EC 14 00 00 00 mov dword ptr [b],14h
int ret = Add(a, b);
000E170C 8B 45 EC mov eax,dword ptr [b]
000E170F 50 push eax
000E1710 8B 4D F8 mov ecx,dword ptr [a]
000E1713 51 push ecx
000E1714 E8 DC F9 FF FF call _Add (0E10F5h)
000E1719 83 C4 08 add esp,8
000E171C 89 45 E0 mov dword ptr [ret],eax
system("pause");
000E171F 8B F4 mov esi,esp
000E1721 68 30 6B 0E 00 push offset string "pause" (0E6B30h)
000E1726 FF 15 60 91 0E 00 call dword ptr [__imp__system (0E9160h)]
000E172C 83 C4 04 add esp,4
000E172F 3B F4 cmp esi,esp
000E1731 E8 DD F9 FF FF call __RTC_CheckEsp (0E1113h)
return 0;
000E1736 33 C0 xor eax,eax
}
Add函数的汇编代码如下
{
000E1690 55 push ebp
000E1691 8B EC mov ebp,esp
000E1693 81 EC CC 00 00 00 sub esp,0CCh
000E1699 53 push ebx
000E169A 56 push esi
000E169B 57 push edi
000E169C 8D BD 34 FF FF FF lea edi,[ebp-0CCh]
000E16A2 B9 33 00 00 00 mov ecx,33h
000E16A7 B8 CC CC CC CC mov eax,0CCCCCCCCh
000E16AC F3 AB rep stos dword ptr es:[edi]
int ret = 0;
000E16AE C7 45 F8 00 00 00 00 mov dword ptr [ret],0
ret = i + j;
000E16B5 8B 45 08 mov eax,dword ptr [i]
000E16B8 03 45 0C add eax,dword ptr [j]
000E16BB 89 45 F8 mov dword ptr [ret],eax
return ret;
000E16BE 8B 45 F8 mov eax,dword ptr [ret]
}
涉及的特殊寄存器有如下特点(在x86的环境下 ):
ESP 寄存器为 Stack Pointer ,它始终指向栈顶的位置。
EIP 寄存器为返回地址,它是调用函数在执行完 Call 指令后的下一条指令的地址。
EBP 寄存器为 Frame Pointer(亦称 Base Pointer),它被用作在当前的栈帧中寻址所有的函数参数以及局部变量。
根据汇编代码,画出函数的栈帧图如下图
在执行Add函数之前,main函数把寄存器ebp入栈,把一段栈空间初始化为0xcccccccc,然后分别把ebx、esi、edi入栈,把函数的参数存入寄存器eax、ecx并入栈,也需要把Add函数的下一条指令地址存入寄存器eip,然后把eip入栈。做完这些工作才开始正式调用Add函数。
当Add函数执行完毕,执行了这样一段汇编代码
000E16C1 5F pop edi
000E16C2 5E pop esi
000E16C3 5B pop ebx
000E16C4 8B E5 mov esp,ebp
000E16C6 5D pop ebp
000E16C7 C3 ret
这段代码的意思是,Add函数执行完毕寄存器edi、esi、ebx需要出栈,esp指向ebp然后ebp出栈。由于esp始终执行栈顶,所以此时esp应指向寄存器eip也就是Add函数下一条指令的地址。此时Add函数申请的栈空间已经释放,栈帧图如下图。
此时返回main函数,Add函数的栈空间已释放,一次函数调用结束。