C语言:函数的栈帧结构

    栈帧也就是函数的具体调用过程:函数的调用,参数的传递,函数执行完之后的返回等等!

    我们先来一段简单的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 

   希望大家多多交流,给出建议。

   如有什么问题,下方留言,会第一时间回复。

*/

猜你喜欢

转载自blog.csdn.net/weixin_41890097/article/details/80262793
今日推荐