一、C /C++中函数调用约定简介
C /C++开发中,程序编译没有问题,但链接的时候报告函数不存在,或程序编译和链接都没有错误,但只要调用库中的函数就会出现堆栈异常等现象。
C++语言中的函数调用约定主要针对三个问题:
- A、函数参数的入栈顺序
- B、清理栈的主体(负责清理栈的主体:函数自身还是调用函数者)
- C、函数名称重整
上述现象出现在C和C++的代码混合使用的情况下或在C++程序中使用第三方库(非C++语言开发)的情况下,原因是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则导致的。函数调用约定决定函数参数入栈的顺序,以及由调用者函数还是被调用函数负责清除栈中的参数等问题,而函数名修饰规则决定编译器使用何种名字修饰方式来区分不同的函数,如果函数之间的调用约定不匹配或者名字修饰不匹配就会产生以上的问题。
调用约定主要是指函数被调用的方式,C++语言的函数调用约定主要有**__stdcall,__fastcall,__pascal,__cdecl,thiscall
**等约定。
在C++中,为了允许操作符重载和函数重载,C++编译器通常按照某种规则改写每一个入口点的符号名,以便允许同一个名字(具有不同的参数类型或者是不同的作用域)有多个用法,而不会打破现有的基于C的链接器。这项技术通常被称为名称改编(Name Mangling)或者名称修饰(Name Decoration)。C++编译器厂商通常选择自己的名称修饰方案。
二、C++语言常见函数调用约定
1、 __stdcall
_stdcall是StandardCall的缩写,是C++的标准调用方式(不是默认),用于调用Win32 API函数。_stdcall调用约定的规则如下:
A、所有参数从右到左依次入栈,如果是调用类成员的话,最后一个入栈的是this指针。
B、被调用函数自动清理堆栈,返回值在EAX。
C、函数修饰名约定:VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。
2、__cdecl
__cdecl是C DECLaration的缩写(declaration,声明),表示C/C++和MFC程序默认使用的调用约定。__cdecl调用约定规则如下:
A、所有参数从右到左依次入栈
B、所有参数由调用者清除,称为手动清栈。返回值在EAX中。
C、函数修饰名约定:VC将函数编译后会在函数名前面加上下划线前缀
由于由调用者清理栈,所以允许可变参数函数存在,如int sprintf(char* buffer,const char* format,…)。
3、__fastcall
__fastcall是快速调用约定,通过寄存器来传送参数。__fastcall调用约定的规则如下:
A、用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送
B、被调用函数在返回前清理传送参数的内存栈 ,返回值在EAX中
C、函数修饰名约定:VC将函数编译后会在函数名前面加上"@“前缀,在函数名后加上”@"和参数的字节数 。
4、thiscall
thiscall是唯一一个不能明确指明的函数修饰符,thiscall只能用于C++类成员函数的调用,同时thiscall也是C++成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理。
thiscall调用约定如下:
A、采用桟传递参数,参数从右向左入栈。如果参数个数确定,this指针通过ECX传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。
B、对参数个数不定的,调用者清理堆栈,否则由被调函数清理堆栈
__thiscall 不是关键字,程序员不能使用。
5、 __pascal
pascal 语言的调用约定,跟 __stdcall 一样,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在EAX中。VC 中已经废弃,建议使用 __stdcall 代替。
三、平台相关
Windows
windows上不管是C还是C++,默认使用的都是__stdcall方式。
不论__stdcall还是__cdecl函数参数都是从可向左入栈的,并且由调用者完成入栈操作。对于__stdcall方式被调用者自身在函数返回前清空堆栈;而__cdecl则由调用者维护内存堆栈,所以调用者函数生成的汇编代码比前一种方式长。
由__cdecl约定的函数只能被C/C++调用。
Windows上使用dumpbin工具查看函数名字修饰。
- C语言
__stdcall方式:_FuncName@sizeofParameters
例如:int __stdcall test(int a,double b)编译之后完整的函数名为_test@12
__cdecl方式:_FuncName
例如:int __stdcall test(int a,double b)编译之后完整的函数名为_test
由于C++允许重载函数,所以函数的名字修饰就不能像C这么简单,C++中的函数名字修饰应该包含返回类型,各参数类型等信息,如果是类成员函数,还应该包含类名、访问级别、是否为const函数等等信息。
- C++语言
不管__cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和按照参数类型代号拼出的参数表。
对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”。
Linux平台
Linux上使用__stdcall和__cdecl的方式比较麻烦一些。
int attribute((cdecl)) test();
Linux上使用nm工具查看函数名字修饰。
__stdcall和__cdecl没有区别,有区别的是编程语言。
- C++语言
char test(); ----- _Z4testv
_Z表示C++,4代表函数名有4个字节,test是函数名,v代表参数为空
double func(unsigned int a,double *b,char c); ----- _Z4funcjPdc
j代表int,Pd代表double型指针,c代表char**
- C语言
只是简单一个函数名,没有其他修饰信息。
char test(); ----- test
double func(unsigned int a,double *b,char c); ----- func