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、对参数个数不定的,调用者清理堆栈,否则由被调函数清理堆栈

c++中的几种函数调用约定(转)的更多相关文章

  1. C语言函数调用约定

    在C语言中,假设我们有这样的一个函数: int function(int a,int b) 调用时只要用result = function(1,2)这样的方式就可以使用这个函数.但是,当高级语言被编译 ...

  2. 宏WINAPI和几种调用约定

    在VC SDK的WinDef.h中,宏WINAPI被定义为__stdcall,这是C语言中一种调用约定,常用的还有__cdecl和__fastcall.这些调用约定会对我们的代码产生什么样的影响?让我 ...

  3. 64位只有一种调用约定stdcall

    procedure TForm2.Button1Click(Sender: TObject); function EnumWindowsProc(Ahwnd: hwnd; AlParam: lPara ...

  4. 关于函数调用约定-thiscall调用约定

    函数调用约定描述了如何以正确的方式调用某些特定类型的函数.包括了函数参数在栈上的分配顺序.有哪些参数将通过寄存器传入,以及在函数返回时函数栈的回收方式等. 函数调用约定的几种类型 stdcall,cd ...

  5. C/C++函数调用约定与this指针

    关于 C/C++ 函数调用约定,大多数时候并不会影响程序逻辑,但遇到跨语言编程时,了解一下还是有好处的. VC 中默认调用是 __cdecl 方式,Windows API 使用 __stdcall 调 ...

  6. 函数调用约定_stdcall[转]

    关键字 清理堆栈 参数入栈顺序 函数名称修饰(C) __cdecl 调用函数 右 à 左 _函数名 __stdcall 被调用函数 右 à 左 _函数名@数字 __fastcall 被调用函数 右 à ...

  7. 关于 C/C++ 函数调用约定

    关于 C/C++ 函数调用约定,大多数时候并不会影响程序逻辑,但遇到跨语言编程时,了解一下还是有好处的. VC 中默认调用是 __cdecl 方式,Windows API 使用 __stdcall 调 ...

  8. Android 中调试手段 打印函数调用栈信息

    下面来简单介绍下 android 中的一种调试方法. 在 android 的 app 开发与调试中,经常需要用到打 Log 的方式来查看函数调用点. 这里介绍一种方法来打印当前栈中的函数调用关系 St ...

  9. __cdecl __stdcall __fastcall之函数调用约定讲解

    首先讲解一下栈帧的概念: 从逻辑上讲,栈帧就是一个函数执行的环境:函数参数.函数的局部变量.函数执行完后返回到哪里等等. 实现上有硬件方式和软件方式(有些体系不支持硬件栈) 首先应该明白,栈是从高地址 ...

随机推荐

  1. C 语言因为疫情重登最流行编程语言榜第一名!其实它一直都在~

      C 语言时隔五年后再次荣登最流行编程语言榜单第一名. 通过调查得出结论, C 语言再次受欢迎的原因竟然是因为病毒,这听起来愚蠢但确实是事实,嵌入式语言 C 和 C++ 被用于医疗设备中的幅度因为疫 ...

  2. composer 打印美化

    { "name": "brady_frmwork", "description":"php framwork", &qu ...

  3. 五分钟详解MySQL并发控制及事务原理

    在如今互联网业务中使用范围最广的数据库无疑还是关系型数据库MySQL,之所以用"还是"这个词,是因为最近几年国内数据库领域也取得了一些长足进步,例如以TIDB.OceanBase等 ...

  4. ECMAScript 6 入门 - 阮一峰

    body #home { width: 100%; max-width: 1368px } #inlineFrame { width: 100%; height: calc(100vh - 30px) ...

  5. vue获取路由中的值

    vue中获取路由中的值 在vue中如何获取路由中的值呢?大家先看下面这段代码: this.$route.params && this.$route.params.id 这行代码就是在v ...

  6. mysql 事务的日志

    事务的日志 1.redo log redo:"重做",记录的是,内存数据页的变化过程 1)作用 在事务ACID过程中,实现的是 "D" 持久化的作用. 2)工作 ...

  7. JavaScript监听滚动条的进度条

    <style type="text/css"> *{ margin: 0; padding: 0; } .g-box{ width: 100%; height: 400 ...

  8. NB-IoT应用分类与技术特点分析

      NB-Iot作为一种窄带物联网技术在各大行业脱颖而出,其应用涵盖多个领域.此文计讯小编将讲解NB-IoT的主要应用分类及相关特点.   一.NB-IoT是什么   NB-IoT是指窄带物联网(Na ...

  9. linux修改进程名

    一.linux中的进程名    linux中有很多查看/操作进程的命令.    这些命令的参数或显示的结果,有的是真实的进程名(top/pstree/pgrep/kill/killall),有的是进程 ...

  10. Java学习的第十七天

    1.静态变量 静态方法 静态代码块 2.今天没问题 3.明天学习abstract和综合实例