c++中的几种函数调用约定(转)

C++中的函数调用约定(调用惯例)主要针对三个问题:

1、参数传递的方式(是否采用寄存器传递参数、采用哪个寄存器传递参数、参数压桟的顺序等);

  参数的传递方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。

  对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序,是从左往右还是从右往左。有些调用惯例还允许使用寄存器传递参数。

2、函数调用结束后的栈指针由谁恢复(被调用的函数恢复还是调用者恢复);

  栈的维护方式:在函数将参数压栈之后,函数体 会被调用,此后需要将被压入的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出工作可以由函数的调用方来完成,也可以由函数本身完成。

3、函数编译后的名称;

  名称修饰策略,为了链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。

对实例代码有几点说明(使用的平台为vs2012+intel x86架构

1、栈顶指针即为esp;

2、int型占32字节内存;

3、桟顶为小地址端,栈底为大地址端,因此出栈需要增大esp;

下面对C++中见到的stdcall、cdecl、fastcall和thiscall做简要说明。

1、stdcall

stdcall是standard call的缩写,也被称为pascal调用约定,因为pascal使用的函数调用约定就是stdcall。

使用stdcall的函数声明方式为:int __stdcall function(int a,int b)

stdcall的调用约定意味着:

1)采用桟传递全部参数,参数从右向左压入栈;

2)被调用函数负责恢复栈顶指针 ;

3)   函数名自动加前导的下划线,后面是函数名,之后紧跟一个@符号,其后紧跟着参数的尺寸,例如_function@4;

下面给出实例:

  1. int _stdcall funb(int p,int q)           //声明为stdcall方式  
  2. {  
  3.     return p-q;  
  1. e=funb(3,4);  
  2. 012C42F7  push        4                   //参数q入栈  
  3. 012C42F9  push        3                   //参数p入栈       
  4. 012C42FB  call        funb (012C1244h)    //调用函数  
  5. 012C4300  mov         dword ptr [e],eax   //调用者没有处理esp  

函数编译后的汇编代码为:

  1. int _stdcall funb(int p,int q)  
  2. {  
  3. 012C3D80  push        ebp    
  4. 012C3D81  mov         ebp,esp                //将esp保存入ebp中  
  5. 012C3D83  sub         esp,0C0h    
  6. 012C3D89  push        ebx                      
  7. 012C3D8A  push        esi    
  8. 012C3D8B  push        edi    
  9. 012C3D8C  lea         edi,[ebp-0C0h]    
  10. 012C3D92  mov         ecx,30h    
  11. 012C3D97  mov         eax,0CCCCCCCCh    
  12. 012C3D9C  rep stos    dword ptr es:[edi]    
  13.     return p-q;  
  14. 012C3D9E  mov         eax,dword ptr [p]    
  15. 012C3DA1  sub         eax,dword ptr [q]    
  16. }  
  1. 012C3DA4  pop         edi    
  2. 012C3DA5  pop         esi    
  3. 012C3DA6  pop         ebx    
  4. 012C3DA7  mov         esp,ebp    
  5. 012C3DA9  pop         ebp    
  6. 012C3DAA  ret         8    //注意此处,用被调函数负责恢复esp  

以上面函数为例,参数q首先被压栈,然后是参数p(参数从右向左入栈),然后利用call调用函数,

而在编译时,这个函数的名字被翻译成_funb@8,其中8代表参数为8个字节(2个int型变量)。

另外,stdcall可以用于类成员函数的调用,这种情况下唯一的不同就是,所有参数从右向左依次入栈后,this指针会最后一个入栈。下面给出示例。

  1. class A  
  2. {  
  3. public:  
  4.     A(int a)  
  5.     {  
  6.         this->val=a;  
  7.     }  
  8.     int _stdcall fun(int par)  //类成员函数采用stdcall  
  9.     {  
  10.         return val-par;  
  11.     }  
  12. private:  
  13.     int val;  
  14. };  

函数调用代码如下:

  1. A t(3);  
  2. int d,e,f,g;  
  3. g=t.fun(4);  

函数调用代码编译后为:

  1. g=t.fun(4);  
  2. 00DB4317  push        4                  //参数4入栈  
  3. 00DB4319  lea         eax,[t]    
  4. 00DB431C  push        eax                //this指针入栈,下面会验证eax内容即为A的对象的地址  
  5. 00DB431D  call        A::fun (0DB1447h)    
  6. 00DB4322  mov         dword ptr [g],eax    

编译后的代码为:

  1. int _stdcall fun(int par)  
  2.     {  
  3. 00DB3CF0  push        ebp    
  4. 00DB3CF1  mov         ebp,esp    
  5. 00DB3CF3  sub         esp,0C0h    
  6. 00DB3CF9  push        ebx    
  7. 00DB3CFA  push        esi    
  8. 00DB3CFB  push        edi    
  9. 00DB3CFC  lea         edi,[ebp-0C0h]    
  10. 00DB3D02  mov         ecx,30h    
  11. 00DB3D07  mov         eax,0CCCCCCCCh    
  12. 00DB3D0C  rep stos    dword ptr es:[edi]    
  13.         return val-par;  
  14. 00DB3D0E  mov         eax,dword ptr [this]    
  15. 00DB3D11  mov         eax,dword ptr [eax]    
  16. 00DB3D13  sub         eax,dword ptr [par]    
  17.     }  
  18. 00DB3D16  pop         edi    
  19. 00DB3D17  pop         esi    
  20. 00DB3D18  pop         ebx    
  21. 00DB3D19  mov         esp,ebp    
  22. 00DB3D1B  pop         ebp    
  23. 00DB3D1C  ret         8          //由被调用函数负责恢复栈顶指针,由于参数为int型变量(4字节)和一个指针(32为,4字节),共8字节  

下面验证入栈时eax中的内容为A对象的地址。

入栈时eax内容如下,为0x0035F808。

找到内存中0x0035F808的内容,为3,。

再看main函数中实例化对象的代码。

可见,this指针正是通过eax入栈。

由此可见,用于类成员函数时,唯一的不同就是在参数入栈完毕后,this指针会最后一个入栈。

2、cdecl

cdecl是C Declaration的缩写,又称为C调用约定,是C语言缺省的调用约定,采用这种方式调用的函数的声明是:

int function (int a ,int b)                   //不加修饰就是采用默认的C调用约定

int _cdecl function(int a,int b)         //明确指出采用C调用约定

cdecl调用方式规定:

1、采用桟传递参数,参数从右向左依次入栈;

2、由调用者负责恢复栈顶指针;

3、在函数名前加上一个下划线前缀,格式为_function;

要注意的是,调用参数个数可变的函数只能采用这种方式(如printf)。

下面给出实例。

  1. int _cdecl funa(int p,int q)        //采用cdecl方式  
  2. {  
  3.     return p-q;  
  4. }  

调用处的代码编译为:

  1. d=funa(3,4);  
  2. 012C42E8  push        4    
  3. 012C42EA  push        3    
  4. 012C42EC  call        funa (012C1064h)       //调用funca  
  5. 012C42F1  add         esp,8                  //调用者恢复栈顶指针esp  
  6. 012C42F4  mov         dword ptr [d],eax      //返回值传递给变量d  

函数编译后的代码为:

  1. int _cdecl funa(int p,int q)  
  2. {  
  3. 012C3D40  push        ebp    
  4. 012C3D41  mov         ebp,esp    
  5. 012C3D43  sub         esp,0C0h    
  6. 012C3D49  push        ebx    
  7. 012C3D4A  push        esi    
  8. 012C3D4B  push        edi    
  9. 012C3D4C  lea         edi,[ebp-0C0h]    
  10. 012C3D52  mov         ecx,30h    
  11. 012C3D57  mov         eax,0CCCCCCCCh    
  12. 012C3D5C  rep stos    dword ptr es:[edi]    
  13.     return p-q;  
  14. 012C3D5E  mov         eax,dword ptr [p]    
  15. 012C3D61  sub         eax,dword ptr [q]    
  16. }  
  17. 012C3D64  pop         edi    
  18. 012C3D65  pop         esi    
  19. 012C3D66  pop         ebx    
  20. 012C3D67  mov         esp,ebp    
  21. 012C3D69  pop         ebp    
  22. 012C3D6A  ret                   //注意此处,被调函数没有恢复esp  

因此,stdcall与cdecl的区别就是谁负责恢复栈顶指针和编译后函数的名称问题。

cedcal同样可以用于类成员函数的调用。此时,cdedl与stdcall的区别在于由谁恢复栈顶指针。

类定义如下:

  1. class A  
  2. {  
  3. public:  
  4.     A(int a)  
  5.     {  
  6.         this->val=a;  
  7.     }  
  8.     int _cdecl fun(int par)       //采用cedcl方式  
  9.     {  
  10.         return val-par;  
  11.     }  
  12. private:  
  13.     int val;  
  14. };  

调用代码编译如下:

 
  1.     g=t.fun(4)  
  2. 013D4317  push        4    
  3. 013D4319  lea         eax,[t]    
  4. 013D431C  push        eax                     //先入栈参数4,后入栈this指针  
  5. 013D431D  call        A::fun (013D144Ch)    
  6. 013D4322  add         esp,8                   //由调用者恢复栈顶指针  
  7. 013D4325  mov         dword ptr [g],eax     

3、fastcall

采用fasecall的函数声明方式为:

int __fastcall function(int a,int b)

fastcall调用约定意味着:
1、函数的第一个和第二个(从左向右)32字节参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过桟传递。从第三个参数(如果有的话)开始从右向左的顺序压栈;
2、被调用函数恢复栈顶指针;
3、在函数名之前加上"@",在函数名后面也加上“@”和参数字节数,例如@function@8;

示例代码如下:

  1. int __fastcall func(int p,int q,int r)    //采用fastcall  
  2. {  
  3.     return p-q-r;  
  4. }  

调用代码如下:

  1. f=func(3,4,5);  
  2. 00E74303  push        5           //第三个参数r压桟  
  3. 00E74305  mov         edx,4       //p q通过ecx和edx传递  
  4. 00E7430A  mov         ecx,3    
  5. 00E7430F  call        func (0E71442h)    
  6. 00E74314  mov         dword ptr [f],eax    //调用者不负责恢复栈顶指针esp  

函数编译后的代码如下:

  1. int __fastcall func(int p,int q,int r)  
  2. {  
  3. 00E73DC0  push        ebp    
  4. 00E73DC1  mov         ebp,esp    
  5. 00E73DC3  sub         esp,0D8h    
  6. 00E73DC9  push        ebx    
  7. 00E73DCA  push        esi    
  8. 00E73DCB  push        edi    
  9. 00E73DCC  push        ecx    
  10. 00E73DCD  lea         edi,[ebp-0D8h]    
  11. 00E73DD3  mov         ecx,36h    
  12. 00E73DD8  mov         eax,0CCCCCCCCh    
  13. 00E73DDD  rep stos    dword ptr es:[edi]    
  14. 00E73DDF  pop         ecx    
  15. 00E73DE0  mov         dword ptr [q],edx    
  16. 00E73DE3  mov         dword ptr [p],ecx    
  17.     return p-q-r;  
  18. 00E73DE6  mov         eax,dword ptr [p]    
  19. 00E73DE9  sub         eax,dword ptr [q]    
  20. 00E73DEC  sub         eax,dword ptr [r]    
  21. }  
  22. 00E73DEF  pop         edi    
  23. 00E73DF0  pop         esi    
  24. 00E73DF1  pop         ebx    
  25. 00E73DF2  mov         esp,ebp    
  26. 00E73DF4  pop         ebp    
  27. }  
  28. 00E73DF5  ret         4     //恢复栈顶指针,由于只有一个参数r被压桟,因此esp+4即可  

可以看到,fasecall利用寄存器ecx与edx传递参数,避免了访存带来的开销。适合少量参数提高效率的场合。

4、thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall只能用于C++类成员函数的调用,同时thiscall也是C++成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理。

thiscall意味着:

1、采用桟传递参数,参数从右向左入栈。如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针
在所有参数压栈后被压入堆栈;
2、对参数个数不定的,调用者清理堆栈,否则由被调函数清理堆栈

原文地址:https://www.cnblogs.com/wsw-seu/p/10544044.html