函数的调用约定涉及了函数参数的入栈顺序、清栈主体(负责清理栈的主体:函数自身还是调用函数者?)、部分名称重整。
如,在C编译方式下有_stdcall、_cdecl等调用约定,在C++编译方式下也有_stdcall、_cedecl等调用约定。
__cdecl是CDeclaration的缩写(declaration,声明),表示C/C++和MFC程序默认使用的调用约定:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl
__stdcall是StandardCall的缩写,是C++的标准调用方式(不是默认),用于调用Win32 API函数:所有参数从右到左依次入栈,如果是调用类成员的话,最后一个入栈的是this指针。这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是ret X,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈。函数在编译的时候就必须确定参数个数,并且调用者必须严格的控制参数的生成,不能多,不能少,否则返回后会出错。__stdcall可以写成_stdcall。
1、__cdecl和__stdcall都是参数从右到左入栈。
2、__cdecl是调用者负责清除栈中的参数,如A函数中调用B函数,参数由A函数负责清除;__stdcall是被调用者负责清除栈中的参数,如A函数中调用B函数,参数由B函数负责清除。
3、_cdecl调用方式不需要知道参数的个数,若要实现变参函数,则要使用这种调用方式。而__stdcall因为参数栈空间由被调用者清除,则必须知道参数的个数(栈空间大小)。
4、__stdcall比_cdecl调用方式 __cdecl是调用者恢复堆栈的,假设有一百个不同的函数调用函数B那么内存中就有一百端恢复堆栈的代码,__stdcall是被调用者恢复堆栈,只有在函数代码的结尾出现一次恢复堆栈的代码,所以节约空间
5、恢复堆栈的代码是编译器根据你给他的call方式自动生成的,所以无需考虑,而告诉编译器call方式的意义就在这里,如果一方用__cdecl一方用__stdcall可能出现没有人释放堆栈的情况,这明显是不允许的
PASCAL 是Pascal语言的函数调用方式,也可以在C/C 中使用,参数压栈顺序与前两者相反。返回时的清栈方式忘记了。。。
__fastcall 是编译器指定的快速调用方式。由于大多数的函数参数个数很少,使用堆栈传递比较费时。因此_fastcall通常规定将前两个(或若干个)参数由寄存器传 递,其余参数还是通过堆栈传递。不同编译器编译的程序规定的寄存器不同。返回方式和_stdcall相当。(x64程序是这种方式)。被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall。
__thiscall 是为了解决类成员调用中this指针传递而规定的。_thiscall要求把this指针放在特定寄存器中,该寄存器由编译器决定。VC使用ecx,Borland的C 编译器使用eax。返回方式和_stdcall相当。thiscall仅仅应用于“C++”成员函数。this指针存放于CX/ECX寄存器中,参数从右到左压栈。thiscall不是关键词,因此不能被程序员指定。
naked call当采用前面的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。(这些代码称作 prolog and epilog code,一般,ebp,esp的保存是必须的).但是naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。
1、__fastcall 和 __thiscall涉及的寄存器由编译器决定,因此不能用作跨编译器的接口。所以Windows上的COM对象接口都定义为__stdcall调用方式。
3、关键字__cdecl、__stdcall和__fastcall可以直接加在要输出的函数前,也可以在vs编译环境的Setting...->C/C++->Code Generation项选择。它们对应的命令行参数分别为/Gd、/Gz和/Gr。缺省状态为/Gd,即__cdecl。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。
4、x64程序使用__fastcall方式
5、c默认方式是__cdecl(并且只有这种方式),C++默认方式也是__cdecl,但可以修改。
windows上不管是C还是C++,默认使用的都是__stdcall方式。
不论__stdcall还是__cdecl函数参数都是从可向左入栈的,并且由调用者完成入栈操作。对于__stdcall方式被调用者自身在函数返回前清空堆栈;而__cdecl则由调用者维护内存堆栈,所以调用者函数生成的汇编代码比前一种方式长。
由__cdecl约定的函数只能被C/C++调用。
Windows上使用dumpbin工具查看函数名字修饰。
__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函数等等信息。
不管__cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”。
X--void
D--char
E--unsigned char
F--short
H--int
I--unsigned int
J--long
K--unsigned long(DWORD)
M--float
N--double
_N--bool
U--struct
PA--指针
PB--const类型的指针
如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;
U表示结构类型,通常后跟结构体的类型名,用“@@”表示结构类型名的结束;
函数参数表的第一项实际上是表示函数的返回值类型;
参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
举例:
int Function1 (char *var1,unsigned long);其函数修饰名为“?Function1@@YGHPADK@Z”
void Function2();其函数修饰名则为“?Function2@@YGXXZ”
extern "C" {
long func(int a);
char* strcat(char*,const char*);
}
extern "C" {
#include
}
要想在编译阶段就知道使用的编译器类型,可以使用:
|
通常应该这样声明头文件:
|
Linux上使用__stdcall和__cdecl的方式比较麻烦一些。
|
Linux上使用nm工具查看函数名字修饰。
__stdcall和__cdecl没有区别,有区别的是编程语言。
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
只是简单一个函数名,没有其他修饰信息。
char test(); ----- test
double func(unsigned int a,double *b,char c); ----- func
附:
Linux上的反汇编工具:objdump -x exefile
查看二进制文件:hexdump -C biFile
编辑二进制文件:hexedit biFile
关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting.../C/C++ /Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。
要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。
几乎我们写的每一个WINDOWS API函数都是__stdcall类型的,为什么??
首先,我们谈一下两者之间的区别:
WINDOWS的函数调用时需要用到栈(STACK,一种先入后出的存储结构)。当函数调用完成后,栈需要清除,这里就是问题的关键,如何清除??
如果我们的函数使用了_cdecl,那么栈的清除工作是由调用者,用COM的术语来讲就是客户来完成的。这样带来了一个棘手的问题,不同的编译器产生栈的方式不尽相同,那么调用者能否正常的完成清除工作呢?答案是不能。
如果使用__stdcall,上面的问题就解决了,函数自己解决清除工作。所以,在跨(开发)平台的调用中,我们都使用__stdcall(虽然有时是以WINAPI的样子出现)。
那么为什么还需要_cdecl呢?当我们遇到这样的函数如fprintf()它的参数是可变的,不定长的,被调用者事先无法知道参数的长度,事后的清除工作也无法正常的进行,因此,这种情况我们只能使用_cdecl。
到这里我们有一个结论,如果你的程序中没有涉及可变参数,最好使用__stdcall 关键字
这两个关键字看起来似乎很少和我们打交道,但是看了下面的定义(来自windef.h),你一定会觉得惊讶:
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define cdecl _cdecl
#ifndef CDECL
#define CDECL _cdecl
#endif
=================================================================================================
前面的extern "C" __declspec(dllexport) __declspec(dllimport)都是用于函数或者变量,甚至类的声明的(可以把extern "C"放在class的前面,但是编译器会忽略掉,最后产生的还是C++修饰符,而不是C修饰符)这样的用法有个好处就是下面的代码可以在混有类的函数和变量上使用下面的宏,虽然对类不起作用:
#ifdef __cplusplus
extern "C"
{
//函数声明
//变量声明,变量一般前面都有extern
//类声明,这个不起作用,编译器直接忽略掉class前面的extern “C”
#ifdef __cplusplus
}
#endif
__declspec(dllexport) __declspec(dllimport)一般也是使用宏的形式:
#ifdef ONEDLL_EXPORTS
#define ONEDLL_API __declspec(dllexport)
#else
#define ONEDLL_API __declspec(dllimport)
#endif
这样在DLL代码本身就是__declspec(dllexport) ,在使用DLL的程序中就变成了__declspec(dllimport),这两标志分别用来指明当前的函数将被导出,和当前函数是被导入的。
上面的两个宏结合一下就是下面这样的了:
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 ONEDLL_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// ONEDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef ONEDLL_EXPORTS
#define ONEDLL_API __declspec(dllexport)
#else
#define ONEDLL_API __declspec(dllimport)
#endif
// 此类是从 OneDll.dll 导出的
#ifdef __cplusplus
extern "C"
{
#endif
class ONEDLL_API COneDll {
public:
COneDll(void);
~COneDll(void);
// TODO: 在此添加您的方法。
int m_a;
int m_b;
int *m_p;
int m_n;
void AddValue();
};
extern ONEDLL_API int nOneDll;
ONEDLL_API int fnOneDll(void);
#ifdef __cplusplus
}
#endif
如果调用模块和被调用模块都是C++(而且是同一种编成环境,如VC,甚至需要同一版本的VC),那么就不需要extern “C”了,因为这个标志的作用就是用在函数和变量声明前,无论是调用模块,还是被调用模块,都将生成C修饰符,调用模块将需要C修饰符的函数,而被调用模块将产生C修饰符的函数,所以这个标志在两者都是C++的时候使用并不受影响,不使用这个标志,也不受影响。
但是如果C模块要调用C++ 模块,那么C++模块就需要使用extern “C”,当然C不用,由于是在头文件的声明中使用,所以使用下面的宏能够使得这个头文件也在C中顺利使用:
#ifdef __cplusplus
extern "C"
{
//函数声明
//变量声明,变量一般前面都有extern
//类声明,这个不起作用,编译器直接忽略掉class前面的extern “C”
#ifdef __cplusplus
}
#endif
如果C++模块要调用C模块,那么C++模块还是需要extern “C”,当然C不用,由于是在头文件的声明中使用,所以使用上面的宏同样能够使得这个头文件也在C中顺利使用。
总结一下就是加上extern “C”在什么情况下都没错,但是要注意函数重载的问题。
def文件是一种比较麻烦的方法,下面是MSDN中的部分内容:
模块定义 (.def) 文件是包含一个或多个描述 DLL 各种属性的 Module 语句的文本文件。如果不使用 __declspec(dllexport) 关键字导出 DLL 的函数,则 DLL 需要 .def 文件。
.def 文件必须至少包含下列模块定义语句:
1.文件中的第一个语句必须是 LIBRARY 语句。此语句将 .def 文件标识为属于 DLL。LIBRARY 语句的后面是 DLL 的名称。链接器将此名称放到 DLL 的导入库中。
2.EXPORTS 语句列出名称,可能的话还会列出 DLL 导出函数的序号值。通过在函数名的后面加上 @ 符和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从 1 到 N,其中 N 是 DLL 导出函数的个数。
例如,包含实现二进制搜索树的代码的 DLL 看上去可能像下面这样:
LIBRARY BTREE
EXPORTS
Insert @1
Delete @2
Member @3
Min @4