• 函数调用约定


    1. 函数调用约定

    关于 C/C++ 函数调用约定,大多数时候并不会影响程序逻辑,但遇到跨语言编程时,了解一下还是有好处的。

    VC 中默认调用是 __cdecl 方式,Windows API 使用 __stdcall 调用方式,在 DLL 导出函数中,为了跟Windows API 保持一致,建议使用 __stdcall 方式。

    调用约定跟堆栈清除密切相关。如果写一个汇编函数,给 C/C++ 调用,在 __cdecl 方式下,则汇编函数无需清除堆栈,在 __stdcall 方式下,汇编函数需要在返回(RET)之前恢复堆栈。

    C 语言有 __cdecl、__stdcall、__fastcall、naked、__pascal。

    C++ 语言有 __cdecl、__stdcall、__fastcall、naked、__pascal、__thiscall,比 C 语言多出一种__thiscall 调用方式。

    但是尤其需要强调的是,目前VC6.0下只支持三种:__cdecl、__stdcall、__fastcall。

    下面详细介绍如上六种调用方式:

    __cdecl

    __cdecl调用约定又称为 C 调用约定,是 C/C++ 语言缺省的调用约定。参数按照从右至左的方式入栈,函数本身不清理栈,此工作由调用者负责,返回值在EAX中。由于由调用者清理栈,所以允许可变参数函数存在,如int sprintf(char* buffer,const char* format,...);。

    __stdcall

    __stdcall 很多时候被称为 pascal 调用约定。pascal 语言是早期很常见的一种教学用计算机程序设计语言,其语法严谨。参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在EAX中。

    __fastcall

    顾名思义,__fastcall 的特点就是快,因为它通过 CPU 寄存器来传递参数。他用 ECX 和 EDX 传送前两个双字(DWORD)或更小的参数,剩下的参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在 EAX 中。

    naked

    naked 是一个很少见的调用约定,一般不建议使用。编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果,此调用约定必须跟 __declspec 同时使用。例如定义一个求和程序,如__declspec(naked) int add(int a,int b);。

    __pascal

    这是 pascal 语言的调用约定,跟 __stdcall 一样,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在EAX中。VC 中已经废弃了这种调用方式,因此在写 VC 程序时,建议使用 __stdcall 代替。

    __thiscall

    这是 C++ 语言特有的一种调用方式,用于类成员函数的调用约定。如果参数确定,this 指针存放于 ECX 寄存器,函数自身清理堆栈;如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈。__thiscall 不是关键字,程序员不能使用。参数按照从右至左的方式入栈。

    函数调用约定包括传递参数的顺序,谁负责清理参数占用的堆栈等,例如 :

    参数传递顺序 谁负责清理参数占用的堆栈
    __pascal 从左到右 调用者
    __stdcall 从右到左 被调函数
    __cdecl 从右到左 调用者

    调用函数的代码和被调函数必须采用相同的函数的调用约定,程序才能正常运行。在Windows上,__cdecl是C/C++程序的缺省函数调用约定。

    在有的cpu上,编译器会用寄存器传递参数,函数使用的堆栈由被调函数分配和释放。这种调用约定在行为上和__cdecl有一个共同点:实参和形参数目不符不会导致堆栈错误。

    不过,即使用寄存器传递参数,编译器在进入函数时,还是会将寄存器里的参数存入堆栈指定位置。参数和局部变量一样应该在堆栈中有一席之地。参数可以被理解为由调用函数指定初值的局部变量。

    2 例子:__cdecl和__stdcall

    不同的CPU,不同的编译器,堆栈的布局可能是不同的。本文以x86,VC++的编译器为例。

    VC++编译器的已经不再支持__pascal, __fortran, __syscall等函数调用约定。目前只支持__fastcall,__cdecl和__stdcall。

    采用__cdecl或__stdcall调用方式的程序,在刚进入子函数时,堆栈内容是一样的。esp指向的栈顶是返回地址。这是被call指令压入堆栈的。下面是参数,左边参数在上,右边参数在下(先入栈)。

    如前表所示,__cdecl和__stdcall的区别是:__cdecl是调用者清理参数占用的堆栈,__stdcall是被调函数清理参数占用的堆栈。

    由于__stdcall的被调函数在编译时就必须知道传入参数的准确数目(被调函数要清理堆栈),所以不能支持变参数函数,例如printf。而且如果调用者使用了不正确的参数数目,会导致堆栈错误。

    通过查看汇编代码,__cdecl函数调用在call语句后会有一个堆栈调整语句,例如:

      a = 0x1234;
      b = 0x5678;
      c = add(a, b);

    对应x86汇编:

      mov dword ptr [ebp-4],1234h
      mov dword ptr [ebp-8],5678h
      mov eax,dword ptr [ebp-8]
      push eax
      mov ecx,dword ptr [ebp-4]
      push ecx
      call 0040100a
      add esp,8
      mov dword ptr [ebp-0Ch],eax


    __stdcall的函数调用则不需要调整堆栈:

      call 00401005
      mov dword ptr [ebp-0Ch],eax

    函数

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

    产生以下汇编代码(Debug版本):

      push ebp
      mov ebp,esp
      sub esp,40h
      push ebx
      push esi
      push edi
      lea edi,[ebp-40h]
      mov ecx,10h
      mov eax,0CCCCCCCCh
      rep stos dword ptr [edi]
      mov eax,dword ptr [ebp+8]
      add eax,dword ptr [ebp+0Ch]
      pop edi
      pop esi
      pop ebx
      mov esp,ebp
      pop ebp
      ret // 跳转到esp所指地址,并将esp+4,使esp指向进入函数时的第一个参数

    再查看__stdcall函数的实现,会发现与__cdecl函数只有最后一行不同:

      ret 8 // 执行ret并清理参数占用的堆栈

    对于调试版本,VC++编译器在“直接调用地址”时会增加检查esp的代码,例如:

      ta = (TAdd)add; // TAdd定义:typedef int (__cdecl *TAdd)(int a, int b);
      c = ta(a, b);

    产生以下汇编代码:

      mov [ebp-10h],0040100a
      mov esi,esp
      mov ecx,dword ptr [ebp-8]
      push ecx
      mov edx,dword ptr [ebp-4]
      push edx
      call dword ptr [ebp-10h]
      add esp,8
      cmp esi,esp
      call __chkesp (004011e0)
      mov dword ptr [ebp-0Ch],eax

    __chkesp 代码如下。如果esp不等于函数调用前保存的值,就会转到错误处理代码。

      004011E0 jne __chkesp+3 (004011e3)
      004011E2 ret
      004011E3 ;错误处理代码

    __chkesp的错误处理会弹出对话框,报告函数调用造成esp值不正确。 Release版本的汇编代码要简洁得多。也不会增加 __chkesp。如果发生esp错误,程序会继续运行,直到“遇到问题需要关闭”。

    3 补充说明

    函数调用约定只是“调用函数的代码”和被调用函数之间的关系。

    假设函数A是__stdcall,函数B调用函数A。你必须通过函数声明告诉编译器,函数A是__stdcall。编译器自然会产生正确的调用代码。

    如果函数A是__stdcall。但在引用函数A的地方,你却告诉编译器,函数A是__cdecl方式,编译器产生__cdecl方式的代码,与函数A的调用约定不一致,就会发生错误。

    以delphi调用VC函数为例,delphi的函数缺省采用__pascal约定,VC的函数缺省采用__cdecl约定。我们一般将VC的函数设为__stdcall,例如:

      int __stdcall add(int a, int b);

    在delphi中将这个函数也声明为__stdcall,就可以调用了:

      function add(a: Integer; b: Integer): Integer;
      stdcall; external 'a.dll';

    因为考虑到可能被其它语言的程序调用,不少API采用__stdcall的调用约定。

    4. 在VC6.0中设置函数调用约定

    project->property->C/C++->Code Generation->calling convention.

    @GX{6WTH90F_)7]9SF3{SDB

  • 相关阅读:
    初学C++到底应该用什么工具比较合适——工具简析
    便携式办公套件LibreOffice Portable 4.0.1
    Hibernate和JPA之间的联系
    央视《家有妙招》整理版,共250招,值得永远收藏
    思科Vs华为:不可避免的对决
    Facebook手机刺激了谁?
    Facebook利用Home平台加速进军移动领域
    英特尔Haswell处理器已出货 预计6月推出
    苹果自建街景地图 或与谷歌针锋相对
    图片链接
  • 原文地址:https://www.cnblogs.com/jiayouwyhit/p/3288150.html
Copyright © 2020-2023  润新知