浅析c++的函数调用

以前学汇编,很清楚函数是怎么调用的,但是久不用之,又忘了~~不知其他人有没有这种经历,写c/c++程序时如果了解许多编译器底层细节,是很爽的;否则,有时会很沮丧。虽然是比较简单的内容,让我们也来回忆一下...

我们知道,函数调用最通常的传递参数的方式莫过于使用堆栈;函数的局部变量也是在栈上创建。具体怎么做呢?

假如我们有这么一个小小的程序:

void Test(int a){
 int b;
}

int main(int argc, char* argv[])
{
 int a;
 Test(a);
 return 0;
}

在VC6.0的调试版中反编译结果如下(加了些注释):

6:    void Test(int a){;函数Tes
00401020   push        ebp ;保存主调函数栈帧指针
00401021   mov         ebp,esp ;激活当前栈帧指针
00401023   sub         esp,44h ;为Test留出44h的“私人空间”:44h=4*17=4*11h(联系下面)
00401026   push        ebx ;保存寄存器值
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h ;11h=17为循环次数
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi] ;循环,对“私人空间”填充(int)3
7:        int b;
8:    }

.......

10:   int main(int argc, char* argv[])
11:   {
00401050   push        ebp;
00401051   mov         ebp,esp ;激活当前栈帧指针
00401053   sub         esp,44h ;为main函数留出44h的“私人空间”
00401056   push        ebx   
00401057   push        esi
00401058   push        edi
00401059   lea         edi,[ebp-44h]
0040105C   mov         ecx,11h
00401061   mov         eax,0CCCCCCCCh
00401066   rep stos    dword ptr [edi]
12:       int a;//变量定义没有对应的汇编代码,编译器会以EBP-4来表示(int)a
13:       Test(a);
00401068   mov         eax,dword ptr [ebp-4] ;dword ptr [ebp-4]也即main函数中int a的值
0040106B   push        eax ;int a 作为参数压栈
0040106C   call        @ILT+0(Test) (00401005) ;调用Test函数
00401071   add         esp,4 ;因为在call之前压入了一个参数int a,占四个字节,Test函数退出后esp要回退到未压入参数前的位置。
14:       return 0;
00401074   xor         eax,eax
15:   }
00401076   pop         edi
00401077   pop         esi
00401078   pop         ebx
00401079   add         esp,44h
0040107C   cmp         ebp,esp
0040107E   call        __chkesp (004010a0)
00401083   mov         esp,ebp
00401085   pop         ebp
00401086   ret
在堆栈中,每个函数都有一个相关的栈桢(stack frame)来保存它所有的局部对象和表达式计算过程中用到的临时对象。通常编译器使用EBP寄存器来指示当前活动的栈桢。编译器在编译时将所有局部对象解析成相对于栈桢指针(EBP)的固定偏移,函数则通过栈桢指针来间接访问局部对象。

编译器编译一个函数时,会在它的开头添加一些代码来为其创建并初始化栈桢,这些代码被称为序言(prologue);同样,它也会在函数的结尾处放上代码来清除栈桢,这些代码叫做尾声(epilogue)。
一般情况下,序言是这样的:

Push EBP ; 把原来的栈桢指针保存到栈上 
Mov EBP, ESP ; 激活新的栈桢 
Sub ESP, 10 ; 减去一个数字,让ESP指向栈桢的末尾
第一条指令把原来的栈桢指针EBP保存到栈上;第二条指令通过让EBP指向主调函数的EBP的保存位置来激活被调函数的栈桢;第三条指令把ESP减去了一个数字,这样ESP就指向了当前栈桢的末尾,而这个数字是函数要用到的所有局部对象和临时对象的大小。编译时,编译器知道函数的所有局部对象的类型和“体积”,所以,它能很容易的计算出栈桢的大小。 
尾声所做的正好和序言相反,它必须把当前栈桢从栈上清除掉:
Mov ESP, EBP 
Pop EBP ; 激活主调函数的栈桢 
Ret ; 返回主调函数

它让ESP指向主调函数的栈桢指针的保存位置(也就是被调函数的栈桢指针指向的位置),弹出EBP从而激活主调函数的栈桢,然后返回主调函数。
一旦CPU遇到返回指令,它就要做以下两件事:把返回地址从栈中弹出,然后跳转到那个地址去。返回地址是主调函数执行call指令调用被调函数时自动压栈的。Call指令执行时,会先把紧随在它后面的那条指令的地址(被调函数的返回地址)压入栈中,然后跳转到被调函数的开始位置。主调函数把被调函数的参数也压进了堆栈,所以参数也是栈桢的一部分。函数返回后,主调函数需要移除这些参数,它通过把所有参数的总体积加到ESP上来达到目的。

如果有一个函数调用链,foo()->bar()->widget();则其堆栈示意图如下:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值