函数的调用过程(栈帧)

我们来深入研究一下函数的调用过程。

先看下面这个简单的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、调用函数的空间时间开销主要来自于栈帧的开辟与释放。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值