首页 > 代码库 > 3 种关键函数调用约定

3 种关键函数调用约定

高级语言翻译成机器码后,计算机没有办法知道函数调用的参数个数、类型,也没有硬件可以保护这些参数。

另外,在C++中,因为重载的原因,所以对函数的命名方式和普通C语言并不一致,该方式称为名字改编。

函数调用者与函数之间,尤其是跨语言调用接口时,需要一个协议约定来传递参数——栈。

 

关键流程:

调用时,调用者依次把参数压栈,然后调用函数,

被调用函数,在堆栈中取得数据,并进行计算。

函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。

 

常见的函数调用约定:

  • stdcall 
  • cdecl 
  • fastcall 
  • thiscall 
  • naked call

 

关键字__cdecl、__stdcall和__fastcall可以直接加在要输出的函数前,

也可以在编译环境的Setting...-> C/C++->Code Generation项选择。

它们对应的命令行参数分别为/Gd、/Gz和/Gr。缺省状态为/Gd,即__cdecl。

当加在输出函数前的关键字与编译 环境中的选择不同时,直接加在输出函数前的关键字有效。 

 

其中有 2 个关键问题:

  1. 参数入栈顺序
  2. 谁负责清空栈

入栈顺序上,一般都是按照从右往左入栈。

关键分歧在于谁负责清空,分别对应 2 个典型的约定:stdcall, cdecl

 

stdcall 约定——pascal调用约定。WINAPI, CALLBACK

  • 参数从右向左压入堆栈。
  • 函数自身修改堆栈。
  • 函数名自动加前导的下划线"_"、后缀"@参数的尺寸"。

cdecl 约定——C调用约定,C语言默认

  • 参数从右向左压入堆栈。
  • 函数本身不清理堆栈,调用者负责清理堆栈。
  • 函数名自动加前导的下划线"_"、没有后缀。

函数本身不清理堆栈,调用者负责清理堆栈。由于这种变化,C调用约定允许函数的参数的个数是不固定的。

由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此

当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个或者后续的明确的参数确定下来,就可以使用不定参数,

例如 sprintf 函数,

int sprintf(char* buffer,const char* format,...)

由于所有的不定参数都可以通过format确定,因此使用不定个数的参数是没有问题的。

 

thiscall

唯一不能明确指明的函数修饰,

因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。

由于成员函数调用还有一个this指针,因此必须特殊处理,

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

 

WINAPI
可以被翻译成适当的调用约定以供函数使用。该宏定义于windef.h之中。下面是在windef.h中的部分内容:

#define CDECL _cdecl#define WINAPI CDECL#define CALLBACK __stdcall#define WINAPI __stdcall#define APIENTRY WINAPI

 

C++编译时函数名修饰约定规则:

  __stdcall调用约定:

  1、以“?”标识函数名的开始,后跟函数名;

  2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;

  3、参数表以代号表示:

  X--void ,

  D--char,

  E--unsigned char,

  F--short,

  H--int,

  I--unsigned int,

  J--long,

  K--unsigned long,

  M--float,

  N--double,

  _N--bool,

  ....

  PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代

  表一次重复;

  4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前

  ;

  5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。

  其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如

  int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”

  void Test2() -----“?Test2@@YGXXZ”

  __cdecl调用约定:

  规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。

  __fastcall调用约定:

  规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。

  VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.

 

 

如果定义的约定和使用的约定不一致,则将导致堆栈被破坏。