转 函数调用约定

谈到函数,一般首先要分析一下各种函数调用约定,比如_cdecl、 _stdcall等。这两种调用约定调用时都是最右侧的参数先进栈,栈最上面的就是函数的第一个参数。不同之处在于,_cdecl由调用者清理参数占用的 栈空间,而_stdcall由被调用者清理参数占用的栈空间。很明显,对于接受可变参数的函数,如printf,被调用函数是无法知道到底有几个参数的, 所以只能采用由调用者清理参数栈的方式。_stdcall调用方式生成的代码会小一点。

     下面的分析采用VC++6.0进行。

一.函数内部的汇编代码

 

  1. void func()  
  2. {  
  3. }  
  4.   
  5. int main()  
  6. {  
  7.     func();  
  8.     return 0;  
  9. }  

对应的汇编代码是:

 1:    void func()
2:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,40h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-40h]
0040102C   mov         ecx,10h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
3:    }
00401038   pop         edi
00401039   pop         esi
0040103A   pop         ebx
0040103B   mov         esp,ebp
0040103D   pop         ebp
0040103E   ret

 

 

5:    int main()
6:    {
00401050   push        ebp
00401051   mov         ebp,esp
00401053   sub         esp,40h
00401056   push        ebx
00401057   push        esi
00401058   push        edi
00401059   lea         edi,[ebp-40h]
0040105C   mov         ecx,10h
00401061   mov         eax,0CCCCCCCCh
00401066   rep stos    dword ptr [edi]
7:        func();
00401068   call        @ILT+0(func) (00401005)
8:        return 0;
0040106D   xor         eax,eax
9:    }
0040106F   pop         edi
00401070   pop         esi
00401071   pop         ebx
00401072   add         esp,40h
00401075   cmp         ebp,esp
00401077   call        __chkesp (00401090)
0040107C   mov         esp,ebp
0040107E   pop         ebp
0040107F   ret
   

     

     一般来说,函数开头的代码如下:

 push        ebp           //保存ebp
 mov         ebp,esp     //将esp的值送ebp,在函数内部可能还会使用push、pop等操作,这时esp的值会不断变化,如果采用esp来寻址局 部变量或者参数的话,可能要不断修正偏移量,而采用ebp寻址变量就方便很多。当然,这样会浪费一个寄存器,编译器可以优化。
 sub          esp,40h     //为局部变量分配空间,这里的40h大小是vc默认分配的。

    而结尾代码如下:

mov         esp,ebp  //恢复esp,其实相当于把栈里局部变量的空间回收了 
pop         ebp
ret

     可以看到,即使函数没有定义任何局部变量,编译器仍然为我们非配了0x40大小的空间,并全部初始化0xcccccccc。有时候我们会见到烫烫€这样的字符串信息,就是这部分内存在作怪。

二 带参数和局部变量的函数

  1. #include <cstdio>  
  2. void func(int a,int b)  
  3. {  
  4.     int m = a;  
  5.     int n = b;  
  6. }  
  7.   
  8. int main()  
  9. {  
  10.     func(16,32);  
  11.     return 0;  
  12. }  
 

反汇编代码:

 

main中调用func的代码是:

00401088   push        20h  //32压栈
0040108A   push        10h  //16压栈
0040108C   call        @ILT+10(func) (0040100f)
00401091   add         esp,8  //调用完毕后清理参数占用的栈空间,这里采用的是_cdecl调用约定。

 

再看func的代码:

2:    void func(int a,int b)
3:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,48h  //默认分配0x40,现在有两个局部int类型变量,空间大小增大8
……
4:        int m = a;
00401038   mov         eax,dword ptr [ebp+8] 
0040103B   mov         dword ptr [ebp-4],eax
5:        int n = b;
0040103E   mov         ecx,dword ptr [ebp+0Ch]
00401041   mov         dword ptr [ebp-8],ecx
6:    }
……
00401047   mov         esp,ebp
00401049   pop         ebp
0040104A   ret

     函数中通过ebp加偏移量的方式来寻址局部变量,很容易看出,栈布局如下图所示:

     图中,地址从上到下增加。因为这里是near调用,所以没有保存ECS寄存器的内容。

posted @ 2011-11-19 22:51  规格严格-功夫到家  阅读(343)  评论(0编辑  收藏  举报