C++调用约定

本文深入探讨C++中调用约定的概念,包括__cdecl、__stdcall和__fastcall的特点及应用场景,帮助读者理解函数调用过程及参数传递方式。

    <div class="markdown_views"><p>有一定C++开发经验的人一定对”__cdecl、__stdcall、__fastcall”肯定不陌生吧!但你真正理解了吗?是的,我曾在这采了无数个坑,栽了无数个跟头,终于忍无可忍要把它总结一下(虽然我已经有能力解决大部分这种问题了)!</p>

什么是调用约定

函数的调用约定,顾名思义就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的。它决定以下内容:(1)函数参数的压栈顺序,(2)由调用者还是被调用者把参数弹出栈,(3)以及产生函数修饰名的方法。

我们知道函数由以下几部分构成:返回值类型 函数名(参数列表),如:
【code1】

void function();
int add(int a, int b);

以上是大家所熟知的构成部分,其实函数的构成还有一部分,那就是调用约定。如下:
【code2】

void __cdecl function();
int __stdcall add(int a, int b);

上面的__cdecl和__stdcall就是调用约定,其中__cdecl是C和C++默认的调用约定,所以通常我们的代码都如 【code1】中那样定义,编译器默认会为我们使用__cdecl调用约定。常见的调用约定有__cdecl、__stdcall、fastcall,应用最广泛的是__cdecl和__stdcall,下面我们会详细进行讲述。。还有一些不常见的,如 __pascal、__thiscall、__vectorcall。

声明和定义处调用约定必须要相同

在VC++中,调用约定是函数类型的一部分,因此函数的声明和定义处调用约定要相同,不能只在声明处有调用约定,而定义处没有或与声明不同。如下:
【code3】 错误的使用一:

int __stdcall add(int a, int b);
int add(int a, int b)
{
    return a + b;
}

报错:

error C2373: ‘add’: redefinition; different type modifiers
error C2440: ‘initializing’: cannot convert from ‘int (__stdcall *)(int,int)’ to ‘int’

【code4】 错误的使用二:

int  add(int a, int b);
int __stdcall add(int a, int b)
{
    return a + b;
}

报错:

error C2373: ‘add’: redefinition; different type modifiers
error C2440: ‘initializing’: cannot convert from ‘int (__cdecl *)(int,int)’ to ‘int’

【code5】 错误的使用三:

int __stdcall add(int a, int b);
int __cdecl add(int a, int b)
{
    return a + b;
}

报错:

error C2373: ‘add’: redefinition; different type modifiers
error C2440: ‘initializing’: cannot convert from ‘int (__stdcall *)(int,int)’ to ‘int’

【code6】 正确的用法:

int __stdcall add(int a, int b);
int __stdcall add(int a, int b)
{
    return a + b;
}

函数的调用过程

要深入理解函数调用约定,你须要了解函数的调用过程和调用细节。
假设函数A调用函数B,我们称A函数为”调用者”,B函数为“被调用者”。如下面的代码,ShowResult为调用者,add为被调用者。

int add(int a, int b)
{
    return a + b;
}

void ShowResult()
{
    std::cout << add(5, 10) << std::endl;
}

函数调用过程可以这么描述:
(1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。
(2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。
(3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。
(4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。
这个过程在AT&T汇编中通过两条指令完成,即:

   leave
   ret
  这两条指令更直白点就相当于:
  mov   %ebp , %esp
  pop    %ebp

此部分内容参考:http://blog.youkuaiyun.com/zsy2020314/article/details/9429707

__cdecl的特点

__cdecl 是 C Declaration 的缩写,表示 C 和 C++ 默认的函数调用约定。是C/C++和MFCX的默认调用约定。

  • 按从右至左的顺序压参数入栈、。
  • 由调用者把参数弹出栈。切记:对于传送参数的内存栈是由调用者来维护的,返回值在EAX中。因此对于像printf这样可变参数的函数必须用这种约定。
  • 编译器在编译的时候对这种调用规则的函数生成修饰名的时候,在输出函数名前加上一个下划线前缀,格式为_function。如函数int add(int a, int b)的修饰名是_add。

(1).为了验证参数是从右至左的顺序压栈的,我们可以看下面这段代码,Debug进行单步调试,可以看到我们的调用栈会先进入GetC(),再进入GetB(),最后进入GetA()。

(2).第二点“调用者把参数弹出栈”,这是编译器的工作,暂时没办法验证。要深入了解这部分,需要学习汇编语言相关的知识。

(3).函数的修饰名,这个可以通过对编译出的dll使用VS的”dumpbin /exports ProjectName.dll”命令进行查看(后面章节会进行详细介绍),或直接打开.obj文件查找对应的方法名(如搜索add)。

从代码和程序调试的层面考虑,参数的压栈顺序和栈的清理我们都不用太观注,因为这是编译器的决定的,我们改变不了。但第三点却常常困扰我们,因为如果不弄清楚这点,在多个库之间(如dll、lib、exe)相互调用、依赖时常常出出现莫名其妙的错误。这个我在后面章节会进行详细介绍。

__stdcall的特点

__stdcall是Standard Call的缩写,是C++的标准调用方式,当然这是微软定义的标准,__stdcall通常用于Win32 API中(可查看WINAPI的定义)。

  • 按从右至左的顺序压参数入栈。
  • 由被调用者把参数弹出栈。切记:函数自己在退出时清空堆栈,返回值在EAX中。
  • __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_function@number。如函数int sub(int a, int b)的修饰名是_sub@8。

__fastcall的特点

__fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数的。

  • 实际上__fastcall用ECX和EDX传送前两个DWORD或更小的参数,剩下的参数仍自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈。
  • __fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@function@number,如double multi(double a, double b)的修饰名是@multi@16。
  • __fastcall和__stdcall很象,唯一差别就是头两个参数通过寄存器传送。注意通过寄存器传送的两个参数是从左向右的,即第1个参数进ECX,第2个进EDX,其他参数是从右向左的入栈,返回仍然通过EAX。

以上内容参考:http://www.3scard.com/index.php?m=blog&f=view&id=10

__thiscall

__thiscall是C++类成员函数缺省的调用约定,但它没有显示的声明形式。因为在C++类中,成员函数调用还有一个this指针参数,因此必须特殊处理,thiscall调用约定的特点:

  • 参数入栈:参数从右向左入栈
  • this指针入栈:如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入栈。
  • 栈恢复:对参数个数不定的,调用者清理栈,否则函数自己清理栈。

总结

这里主要总结一下_cdecl、_stdcall、__fastcall三者之间的区别:

要点__cdecl__stdcall__fastcall
参数传递方式右->左右->左左边开始的两个不大于4字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数自右向左压栈传送
清理栈方调用者清理被调用函数清理被调用函数清理
适用场合C/C++、MFC的默认方式; 可变参数的时候使用;Win API要求速度快
C编译修饰约定_functionname_functionname@number@functionname@number

本文章转自:luoweifu
带你玩转Visual Studio——绑定进程调试

*



(function () {('pre.prettyprint code').each(function () {
var lines = (this).text().split(\n).length;var numbering = $('
    ').addClass('pre-numbering').hide();
    (this).addClass(hasnumbering).parent().append( numbering);
    for (i = 1; i <= lines; i++) {
    numbering.append( ('
    • ').text(i));
      };
      $numbering.fadeIn(1700);
      });
      });

      欢迎使用Markdown编辑器写博客

      本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦:

      • Markdown和扩展Markdown简洁的语法
      • 代码块高亮
      • 图片链接和图片上传
      • LaTex数学公式
      • UML序列图和流程图
      • 离线写博客
      • 导入导出Markdown文件
      • 丰富的快捷键

      快捷键

      • 加粗 Ctrl + B
      • 斜体 Ctrl + I
      • 引用 Ctrl + Q
      • 插入链接 Ctrl + L
      • 插入代码 Ctrl + K
      • 插入图片 Ctrl + G
      • 提升标题 Ctrl + H
      • 有序列表 Ctrl + O
      • 无序列表 Ctrl + U
      • 横线 Ctrl + R
      • 撤销 Ctrl + Z
      • 重做 Ctrl + Y

      Markdown及扩展

      Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式丰富的HTML页面。 —— [ 维基百科 ]

      使用简单的符号标识不同的标题,将某些文字标记为粗体或者斜体,创建一个链接等,详细语法参考帮助?。

      本编辑器支持 Markdown Extra ,  扩展了很多好用的功能。具体请参考Github.

      表格

      Markdown Extra 表格语法:

      项目价格
      Computer$1600
      Phone$12
      Pipe$1

      可以使用冒号来定义对齐方式:

      项目价格数量
      Computer1600 元5
      Phone12 元12
      Pipe1 元234

      定义列表

      Markdown Extra 定义列表语法: 项目1 项目2
      定义 A
      定义 B
      项目3
      定义 C

      定义 D

      定义D内容

      代码块

      代码块语法遵循标准markdown代码,例如:

      @requires_authorization
      def somefunc(param1='', param2=0):
          '''A docstring'''
          if param1 > param2: # interesting
              print 'Greater'
          return (param2 - param1 + 1) or None
      class SomeClass:
          pass
      >>> message = '''interpreter
      ... prompt'''

      脚注

      生成一个脚注1.

      目录

      [TOC]来生成目录:

      数学公式

      使用MathJax渲染LaTex 数学公式,详见math.stackexchange.com.

      • 行内公式,数学公式为: Γ(n)=(n1)!nN
      • 块级公式:

      x=b±b24ac2a

      更多LaTex语法请参考 这儿.

      UML 图:

      可以渲染序列图:

      Created with Raphaël 2.1.0 张三 张三 李四 李四 嘿,小四儿, 写博客了没? 李四愣了一下,说: 忙得吐血,哪有时间写。

      或者流程图:

      Created with Raphaël 2.1.0 开始 我的操作 确认? 结束 yes no
      • 关于 序列图 语法,参考 这儿,
      • 关于 流程图 语法,参考 这儿.

      离线写博客

      即使用户在没有网络的情况下,也可以通过本编辑器离线写博客(直接在曾经使用过的浏览器中输入write.blog.youkuaiyun.com/mdeditor即可。Markdown编辑器使用浏览器离线存储将内容保存在本地。

      用户写博客的过程中,内容实时保存在浏览器缓存中,在用户关闭浏览器或者其它异常情况下,内容不会丢失。用户再次打开浏览器时,会显示上次用户正在编辑的没有发表的内容。

      博客发表后,本地缓存将被删除。 

      用户可以选择 把正在写的博客保存到服务器草稿箱,即使换浏览器或者清除缓存,内容也不会丢失。

      注意:虽然浏览器存储大部分时候都比较可靠,但为了您的数据安全,在联网后,请务必及时发表或者保存到服务器草稿箱

      浏览器兼容

      1. 目前,本编辑器对Chrome浏览器支持最为完整。建议大家使用较新版本的Chrome。
      2. IE9以下不支持
      3. IE9,10,11存在以下问题
        1. 不支持离线功能
        2. IE9不支持文件导入导出
        3. IE10不支持拖拽文件导入


      1. 这里是 脚注内容.
    评论
    成就一亿技术人!
    拼手气红包6.0元
    还能输入1000个字符
     
    红包 添加红包
    表情包 插入表情
     条评论被折叠 查看
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值