栈帧也就是函数的具体调用过程:函数的调用,参数的传递,函数执行完之后的返回等等!
我们先来一段简单的c语言代码:
#include <stdio.h> #include <windows.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; }
这是一段简单的c语言程序,不做解释,我们主要来看看调用add函数的具体过程。
在具体介绍之前,我们给出一些概念:
通用寄存器:EAX,EBX,ECX,EDX。
EIP(pc):程序计数器,用来存放将要执行代码的地址。
EBP:栈底。
ESP:栈顶。
pop指令:出栈,每pop一次,esp上移一个单位。
要看函数的具体调用过程,我们必须去汇编语言下,看计算机的具体做些什么操作(具体解释在代码后的注释):
12: int a = 0xAAAAAAAA; 0040EFC8 mov dword ptr [ebp-4],0AAAAAAAAh//这条指令,很显然是为变量a开辟空间,并把0xAAAAAAAA存入dword ptr [ebp-4] 13: int b = 0xBBBBBBBB; 0040EFCF mov dword ptr [ebp-8],0BBBBBBBBh//和上条指令一样,是为变量b开辟空间,并把0xAAAAAAAA存入dword ptr [ebp-8] 14: int ret = add(a,b); 0040EFD6 mov eax,dword ptr [ebp-8]//将b的值存入通用寄存器eax中 0040EFD9 push eax //将b的值入栈进行保存 0040EFDA mov ecx,dword ptr [ebp-4]//将a的值存入寄存器ecx中 0040EFDD push ecx //将a的值入栈进行保存 0040EFDE call @ILT+5(_add) (0040100a)//调用add函数,这条语句在下文还会继续说明 0040EFE3 add esp,8
为了让大家看的清楚,画张图给大家看:
我们可以明显的看到每push一次,栈顶下移一个单位,用于存放b和a,栈底位置保持不变。
下来我们来说说call指令:
call指令就是调用定义的add函数,它在执行时主要有两个工作:
1、将call指令的下一条指令地址进行入栈保存(在图中我们已经将0040EFE3压入栈中),这样做的目的是为了在执行完add函数之后,还能正确的返回到main函数中。
2、跳转到目标函数(add函数)的入口地址:将目的函数的入口地址写入EIP中即可。
这些工作做完之后,我们再来看看进入add函数之后,计算机又做了些什么事呢?
00401010 push ebp //将ebp(main函数的栈底)内容入栈 00401011 mov ebp,esp//让栈底指向目前栈顶的位置,其实说白了,现在的工作就是要形成新的栈帧结构 00401013 sub esp,44h//将栈顶下移,add函数的栈帧结构基本形成 00401016 push ebx //************ 00401017 push esi 00401018 push edi 00401019 lea edi,[ebp-44h] 0040101C mov ecx,11h 00401021 mov eax,0CCCCCCCCh 00401026 rep stos dword ptr [edi]//从上面星号位置到这里,是计算机对内存进行清零,或者压入一些必要的信息 6: int z = A + B; 00401028 mov eax,dword ptr [ebp+8]//将a的值存入eax中 0040102B add eax,dword ptr [ebp+0Ch]//eax的内容(a的值)加上b的值,将和存入eax中 0040102E mov dword ptr [ebp-4],eax//给z开辟空间,将eax的值存入dword ptr [ebp-4] 7: return z;//下面就要做返回值处理的工作 00401031 mov eax,dword ptr [ebp-4]//将z(a+b)的值存入寄存器中 8: } 00401034 pop edi 00401035 pop esi 00401036 pop ebx 00401037 mov esp,ebp //又将esp指向目前ebp的位置, 00401039 pop ebp //将下一条内容出栈,并将内容(main函数的)写入ebp中 0040103A ret //ret指令在后面进行解释
下面我们再来画两张图来解释一下,以便大家理解:
1、return命令之前:
这里我们明显可以看到,ebp和esp有了新的位置,也就是形成了add函数的栈帧结构。
2、return命令执行之后:
ret指令执行时也有两个工作:
1、将当前保存的函数返回值出栈。
2、修改EIP(将之前保存的0040EFE3写入EIP),以便成功返回main函数的适当位置。
程序返回到了main函数当中,但是调用工作真的做完了吗?
14: int ret = add(a,b);//很明显,函数返回到0040EFE3指令,这时“int ret = add(a,b)”并没有执行结束。 0040EFD6 mov eax,dword ptr [ebp-8] 0040EFD9 push eax 0040EFDA mov ecx,dword ptr [ebp-4] 0040EFDD push ecx 0040EFDE call @ILT+5(_add) (0040100a) 0040EFE3 add esp,8 //esp+8,其实就是让栈顶上移,释放掉调用add函数为实参所开辟的空间 0040EFE6 mov dword ptr [ebp-0Ch],eax //将eax的内容(add函数的返回值z)写入dword ptr [ebp-0Ch]
这里我们再来一张图,解释上面的(add esp,8)指令:
通过上图,可以明显的看到esp已经移动到形参b的上一个地址,释放掉了形参的地址空间。我们就可以解释:函数调用过程中形参实例化、为什么调用完函数之后,函数的形参就被释放等等函数调用的相关问题。
至此,add函数的调用过程已经全部结束。
/*编译环境:vc++6.0
希望大家多多交流,给出建议。
如有什么问题,下方留言,会第一时间回复。
*/