我们在编写代码的时候都会调用函数,有点函数有多个参数,例如:
int test(int a,char b, char* c);
上面的函数调用方式是test(10, ‘c’, “tinus”);那么这个函数编译器是怎么知道有多少个参数,参数类型是什么了?因为函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。那么在参数传递中当参数个数多于一个时,按照什么顺序把参数压入堆栈,并且函数调用后,栈里面的数据由谁来释放处理?
为了解决上面的问题,C/C++通过函数调用约定来说明这些问题。
函数调用约定
是函数调用者和被调用的函数体之间关于参数传递、返回值传递、堆栈清除、寄存器使用的一种约定。
常见的调用约定有:
stdcall(pascal)
cdecl
fastcall
thiscall
- 调用协议常用场合
-
- __stdcall: stdcall(pascal)–Standard Call的缩写,C++的标准调用方式,在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。一般WIN32的函数都是__stdcall。
- __cdecl:cdecl–C Declaration的缩写,C语言缺省的调用约定,C/C++默认的函数调用协议。
- __fastcall:适用于对性能要求较高的场合。
- __thiscall :C++类成员函数缺省的调用约定。thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。由于成员函数调用还有一个this指针,因此必须特殊处理
- 函数参数入栈方式
-
- __stdcall:函数参数由右向左入栈。 对于参数个数固定情况下,类似于stdcall,不定时则类似cdecl。
- __cdecl:函数参数由右向左入栈。 对于参数个数固定情况下,类似于stdcall,不定时则类似cdecl。
- __fastcall:从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈。__fastcall在寄存器中放入不大于4字节的参数,故性能较高,适用于需要高性能的场合。
- __thiscall :参数从右向左入栈,如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。如果参数个数确定,this指针通过ecx传递给被调用者。
- 栈内数据清除方式
-
- __stdcall:函数调用结束后由被调用函数清除栈内数据。
- __cdecl:函数调用结束后由函数调用者清除栈内数据。
- __thiscall :如果参数个数不确定,调用者清理堆栈,否则函数自己清理堆栈
- __fastcall:函数调用结束后由被调用函数清除栈内数据。
- 问题一:不同编译器设定的栈结构不尽相同,跨开发平台时由函数调用者清除栈内数据不可行。
- 问题二:某些函数的参数是可变的,如printf函数,这样的函数只能由函数调用者清除栈内数据。
- 问题三:由调用者清除栈内数据时,每次调用都包含清除栈内数据的代码,故可执行文件较大。
- C语言编译器函数名称修饰规则
-
- __stdcall:编译后,函数名被修饰为“_functionname@number”。
- __cdecl:编译后,函数名被修饰为“_functionname”。
- __fastcall:编译后,函数名给修饰为“@functionname@nmuber”。
- 注:“functionname”为函数名,“number”为参数字节数。
- 注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。
- C++语言编译器函数名称修饰规则
-
- __stdcall:编译后,函数名被修饰为“?functionname@@YG******@Z”。
- __cdecl:编译后,函数名被修饰为“?functionname@@YA******@Z”。
- __fastcall:编译后,函数名被修饰为“?functionname@@YI******@Z”。
- 注:“******”为函数返回值类型和参数类型表。
- 注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。
- C语言和C++语言间如果不进行特殊处理,也无法实现函数的互相调用。
具体可以查看下表:
C++编译时函数名修饰约定规则:
__stdcall调用约定:
以“?”标识函数名的开始,后跟函数名;
函数名后面以“@@YG”标识参数表的开始,后跟参数表;
参数表以代号表示:
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”代表一次重复
…
参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
例如:
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2() -----“?Test2@@YGXXZ”
__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。
函数调用约定导致的常见问题
如果定义的约定和使用的约定不一致,则将导致堆栈被破坏,导致严重问题,下面是两种常见的问题:
函数原型声明和函数体定义不一致
DLL导入函数时声明了不同的函数约定
调用函数的代码和被调函数必须采用相同的函数的调用约定,程序才能正常运行。