我们来深入研究一下函数的调用过程。
先看下面这个简单的C语言源程序:
#include <stdio.h>
#include <stdlib.h>
int add(int A,int B)
{
int z = A + B;
return z;
}
int main()
{
int a = 0xAAAAAAAA;
int b = 0xBBBBBBBB;
int ret = add(a,b);
printf("ret = %d\n",ret);
system ("pause");
return 0;
}
在debug状态下,我们查看view/call stack(快捷键alt+7),可以发现main函数是在__tmainCRTStart函数中调用的,而__tmainCRTStartup函数是在mainCRTStartup被调用的。
函数的调用过程要为函数开辟栈空间,用于本次函数调用过程保存现场(为了调用结束后返回调用的位置)以及临时变量保存。这块栈空间我们称为函数栈帧。
要分析这个过程必须要查看相应的汇编代码,我们先来了解几个会涉及到的寄存器。
通用寄存器:EAX,EBX,ECX,EDX。
EIP(PC):程序计数器,用于保存当前正在执行的指令的下一条指令的地址。
ESP:栈指针,用于指向栈的栈顶(下一个压入栈的活动记录的顶部)。
EBP:栈底指针(帧指针),指向当前活动记录的底部。
下面是main函数的汇编代码:
12: int a = 0xAAAAAAAA;
00401078 mov dword ptr [ebp-4],0AAAAAAAAh //这一行汇编代码用于为变量a开辟空间。
13: int b = 0xBBBBBBBB;
0040107F mov dword ptr [ebp-8],0BBBBBBBBh //为变量b开辟空间
14: int ret = add(a,b); //注意下面几行代码的顺序,说明调用函数时形成临时变量的顺序是从右往左的
00401086 mov eax,dword ptr [ebp-8] //将变量b的值放入通用寄存器eax中
00401089 push eax //将存有变量b的值的通用寄存器eax的值入栈
0040108A mov ecx,dword ptr [ebp-4] //将变量a的值放入通用寄存器ecx中
0040108D push ecx //将a的值入栈
0040108E call @ILT+0(_add) (00401005) //调用add函数,将当前的指令的下一条指令的地址(00401093)保存,并跳转(修改eip)
00401093 add esp,8
00401096 mov dword ptr [ebp-0Ch],eax
下图是此时栈的状态:
接下来是跳转至add函数,我们来看汇编代码:
@ILT+0(_add):
00401005 jmp add (00401020) //跳转至add函数,修改了eip
4: int add(int A,int B)
5: {
00401020 push ebp //将ebp入栈
00401021 mov ebp,esp //将ebp指向esp,这两行代码的目的是形成新的栈帧
00401023 sub esp,44h //以下几行代码可理解为为add函数在栈中开辟空间
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
6: int z = A + B;
00401038 mov eax,dword ptr [ebp+8] //将变量a的值放入eax中
0040103B add eax,dword ptr [ebp+0Ch] //完成加法操作
0040103E mov dword ptr [ebp-4],eax //将结果放入[ebp-4]中
7: return z;
00401041 mov eax,dword ptr [ebp-4] //返回结果放入eax中
8: }
00401044 pop edi
00401045 pop esi
00401046 pop ebx
00401047 mov esp,ebp //释放add函数的栈帧,与删除的原理相似
00401049 pop ebp //返回值地址出栈,恢复ebp
0040104A ret //修改eip
栈帧图如下:
00401093 add esp,8 //释放为调用add函数开辟的存放临时变量的空间
00401096 mov dword ptr [ebp-0Ch],eax //add函数返回的值放入[ebp-0ch]
15: printf("ret = %d\n",ret);
00401099 mov edx,dword ptr [ebp-0Ch]
0040109C push edx
0040109D push offset string "ret = %d\n" (00424024)
004010A2 call printf (00401200)
004010A7 add esp,8
16: system ("pause");
004010AA push offset string "pause" (0042401c)
004010AF call system (004010f0)
004010B4 add esp,4
17: return 0;
004010B7 xor eax,eax
18: }
返回之后的栈帧图:
总结一下
1、调用函数所做的工作:将当前的指令的下一条指令的地址保存,保存的目的是为了调用结束后修改PC值返回,然后跳转至目标地址处。实现跳转是由修改EIP(PC)的值完成的。
2、返回值的地址也是放在栈里的。
3、形参实例化时从右至左的。
4、任何一个临时变量都保存在当前的函数的栈帧内。调用结束后,修改esp和ebp完成空间释放,但栈帧实际还存在,只是告诉编译器这部分栈空间可以被覆盖掉。文件删除也是这个原理。
5、return所做的工作是将当前的函数的返回值地址出栈,利用pop的数据修改EIP。
6、函数的返回值是通过寄存器返回的。
7、调用函数的空间时间开销主要来自于栈帧的开辟与释放。