函数的调用约定,顾名思义就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的.它决定以下内容:
我们熟悉的函数构成为:返回值类型 函数名(参数列表).
其实函数的构成还有一部分,那就是调用约定.
那么函数的构成为:返回值类型 调用约定 函数名(参数列表)
因此函数的声明和定义处的调用约定要相同,不能只在声明处有调用约定,而定义处没有或与声明不同.
__cdecl:functionname
__stdcall:_functionname@number
__fastcall:@functionname@number
__vectorcall:functionname@@number
注意:
注意:函数修饰名实际不含"[","]",以上"[","]"只是为了方便阅读
__cdecl:@@YA
__stdcall:@@YG
__fastcall:@@YI
__vectorcall:@@YQ
注意:
以上结果用VS 2017在x86测试时是正确的,但在x64测试时有出入.x64时,不论__cdecl,__stdcall,__fastcall都以 @@YA标
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:指针
说明:PA表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;如果PA表示的是类对象的指针,则PA后接"V+类名+@@"
如void __stdcall func(long* pL,long* pL2,double* pD)的修饰名为?func@YGXPAJ0PAN@Z,int __stdcall func(Node* pNode,int* pVal)的修饰名为?func@YGHPAVNode@@PAH@Z
关于调用约定的总结如下表:
参数压栈顺序 |
参数出栈 |
函数修饰名 |
备注 |
||||||||||
__cdecl |
从右到左 |
由调用者把参数弹出栈,也称手动清栈 |
|
1.C/C++,MFC的默认函数调用约定 2.对于传送参数的内存栈是由调用者来维护的,因此对于可变参数的函数必须使用这种约定 |
|||||||||
__stdcall |
从右到左 |
由被调用者把参数弹出栈,也称自动清栈 |
|
C++的标准调用方式,通常用于Win32 API |
|||||||||
__thiscall |
从右到左
如果参数个数确定,this指针通过ECX传递给被调用者; 如果参数个数不确定,this指针在所有参数压栈后被压入栈 |
对参数个数确定,自动清栈;
对参数个数不确定,手动清栈 |
C++类成员函数缺省的调用约定,但它没有显示的声明形式 |
||||||||||
__fastcall |
x86:用ECX和EDX传送前两个DWORD或更小的参数,剩下的参数仍自右向左压栈
x64:用RCX,RDX,R8,R9传前四个参数,剩下的参数仍自右向左压栈 |
由被调用者把参数弹出栈,也称自动清栈 |
|
x86:通过寄存器传送的两个参数是从左向右的,即第1个参数进ECX,第2个参数进EDX,其他参数是从右向左压栈
x64:通过寄存器传送的四个参数是从左向右的,即第1个参数进RCX,第2个参数进RDX,第3个参数进R8,第4个参数进R9,其他参数是从右向左压栈
2.速度快 |
|||||||||
__vectorcall |
|
1.目的是用于优化浮点向量运算,intel处理器中有很多浮点向量寄存器(xmm系列),传统的调用约定(__cdelc,__stdcall,__fastcall,__thiscall)都是通过通用寄存器(ecx,edx/rcx,edx,r8,r9)以及堆栈进行参数传递,所有调用的时候,浮点参数需要从栈获取. 2.继承于__fastcall,但对于整数仍然按照__fastcall规则传递,而浮点及向量将通过浮点向量寄存器传递,比传统调用约定更快速
|
可以用VS的”dumpbin /exports ProjectName.dll”命令查看dll的接口
通过关键字extern "C" __declspec(dllexport)声明的接口函数可以保证__cdecl调用约定的函数名称不被改变,却不能保证__stdcall和__fastcall调用约定的函数名称不被改变
def文件必要元素:
LIBRARY XXX.dll #dll名称不是必须的.写了就必须保证和要生成的dll名称一致;不写,就默认与要生成的dll名称一致
EXPORTS
要导出的函数名 @ 序号 #@ 序号不是必须的.写了的话,生成的dll就会按照该序号导出该函数;不写的话,生成的dll则会按其默认规则生成序号
要使__stdcall和__fastcall调用约定的函数不被改变,可以使用模块文件.
使用def时可以不用__declspec(dllexport),即可导出def中的名称
使用模块文件(.def)对x86和x64均适用
*:在项目属性->链接器->输入->模块导入文件中填写def文件的名称
#pragma comment(linker, "/EXPORT:要导出的函数名=函数修饰名,PRIVATE"),PRIVATE可以不写,其作用暂时没查
需要事先知道函数修饰名.编译方式(C/C++),调用约定(__cdecl,__stdcall,__fastcall,__vectorcall),编译位数(x86/x64)均会影响导出名称
使用#pragma对x86和x64均适用
不添加任何调用约定时,使用项目的默认调用约定,C/C++默认调用约定为__cdecl
*:通过项目属性->C/C++->高级->调用约定可以修改默认调用约定
显式调用:使用LoadLibrary载入dll,使用GetProcAddress获取某函数地址
隐式调用:可以使用#pragma comment(libm"xx.lib")的方式,也可以直接将xx.lib加入到工程中
使用静态装入,需要有头文件声明这个要被使用的dll中的函数,如果声明中指定了调用约定或者extern “C”,那么在调用这个函数的时候,编译器就通过Name Mangling之后的函数名去.lib中找这个函数,*.def中的内容是对*.lib里函数的名称不产生作用,*.def文件里的函数重命名只对dll有用。这就有lib 跟dll里函数名不一致的问题了,但并不会产生影响,DLL的制造者跟使用者采用的是一致函数声明
在编写DLL的时候,
写个头文件,头文件里声明函数的编译方式(即用C还是C++编译),调用约定(主要是为了隐式调用)
再写个*.def文件把函数重命名了(主要是为了显式调用)
提供*.DLL\*.lib\*.h给dll的使用者,这样无论是隐式的调用,还是显式的调用,都可以方便的进行
DLL导出测试如下图