关于调用约定和DLL导出的一些总结

调用约定

函数的调用约定,顾名思义就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的.它决定以下内容:

  1. 函数参数的压栈顺序
  2. 由调用者还是被调用者把参数弹出栈
  3. 产生函数修饰名的方法

我们熟悉的函数构成为:返回值类型 函数名(参数列表).

其实函数的构成还有一部分,那就是调用约定.

那么函数的构成为:返回值类型 调用约定 函数名(参数列表)

因此函数的声明和定义处的调用约定要相同,不能只在声明处有调用约定,而定义处没有或与声明不同.

 

C函数名修饰

__cdecl:functionname

__stdcall:[email protected]

__fastcall:@[email protected]

__vectorcall:[email protected]@number

注意:

  1. 以上结果用VS 2017在x86测试时是正确的,但在x64测试时有出入.x64,不论__cdecl,__stdcall,__fastcall都为 functionname
  2. number是参数的字节数

C++函数名修饰

  1. "?"标识函数开始,后跟函数名
  2. 函数名后面标识调用约定,然后跟参数列表
  3. 参数表第一项为该函数的返回值类型
  4. 参数表后以"@Z"标识整个函数名结束,如果该函数无参数,则以"Z"标识结束
  • 有参:?functionname[调用约定标识][返回值][参数列表]@Z
  • 无参:?functionname[调用约定标识][返回值]XZ

注意:函数修饰名实际不含"[","]",以上"[","]"只是为了方便阅读

  • 调用约定标识

__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+类名[email protected]@"

如void __stdcall func(long* pL,long* pL2,double* pD)的修饰名为[email protected]@Z,int __stdcall func(Node* pNode,int* pVal)的修饰名为[email protected]@@[email protected]

 关于调用约定的总结如下表:

 

参数压栈顺序

参数出栈

函数修饰名

备注

__cdecl

从右到左

由调用者把参数弹出栈,也称手动清栈

  C编译修饰约定 C++编译修饰约定
x86 functionname

有参:[email protected]@YA[返回值类型][参数表]@Z

无参:[email protected]@YA[返回值类型]XZ

x64 functionname

有参:[email protected]@YA[返回值类型][参数表]@Z

无参:[email protected]@YA[返回值类型]XZ

1.C/C++,MFC的默认函数调用约定

2.对于传送参数的内存栈是由调用者来维护的,因此对于可变参数的函数必须使用这种约定

__stdcall

从右到左

由被调用者把参数弹出栈,也称自动清栈

  C编译修饰约定 C++编译修饰约定
x86

_functionname

@number,其中number=参数的字节数

 

有参:[email protected]@YG[返回值类型][参数表]@Z

无参:[email protected]@YG[返回值类型]XZ

x64

functionname

有参:[email protected]@YA[返回值类型][参数表]@Z

无参:[email protected]@YA[返回值类型]XZ

 

C++的标准调用方式,通常用于Win32 API

__thiscall

从右到左

 

如果参数个数确定,this指针通过ECX传递给被调用者;

 

如果参数个数不确定,this指针在所有参数压栈后被压入栈

对参数个数确定,自动清栈;

 

对参数个数不确定,手动清栈

 

C++类成员函数缺省的调用约定,但它没有显示的声明形式

__fastcall

x86:用ECX和EDX传送前两个DWORD或更小的参数,剩下的参数仍自右向左压栈

 

x64:用RCX,RDX,R8,R9传前四个参数,剩下的参数仍自右向左压栈

由被调用者把参数弹出栈,也称自动清栈

 

C编译修饰约定

C++编译修饰约定

x86

@functionname

@number,其中number=参数的字节数

有参:[email protected]@YI[返回值类型][参数表]@Z

无参:[email protected]@YI[返回值类型]XZ

x64

functionname

有参:[email protected]@YA[返回值类型][参数表]@Z

无参:[email protected]@YA[返回值类型]XZ

 

x86:通过寄存器传送的两个参数是从左向右的,即第1个参数进ECX,第2个参数进EDX,其他参数是从右向左压栈

 

x64:通过寄存器传送的四个参数是从左向右的,即第1个参数进RCX,第2个参数进RDX,第3个参数进R8,第4个参数进R9,其他参数是从右向左压栈

 

2.速度快

__vectorcall

   
 

C编译修饰约定

C++编译修饰约定

x86

[email protected]

@number,其中number=参数的字节数

有参:[email protected]@YQ[返回值类型][参数表]@Z

无参:[email protected]@YQ[返回值类型]XZ

x64

[email protected]

@number,其中number=参数的字节数

有参:[email protected]@YQ[返回值类型][参数表]@Z

无参:[email protected]@YQ[返回值类型]XZ

1.目的是用于优化浮点向量运算,intel处理器中有很多浮点向量寄存器(xmm系列),传统的调用约定(__cdelc,__stdcall,__fastcall,__thiscall)都是通过通用寄存器(ecx,edx/rcx,edx,r8,r9)以及堆栈进行参数传递,所有调用的时候,浮点参数需要从栈获取.

2.继承于__fastcall,但对于整数仍然按照__fastcall规则传递,而浮点及向量将通过浮点向量寄存器传递,比传统调用约定更快速

 

关于DLL导出

可以用VS的”dumpbin /exports ProjectName.dll命令查看dll的接口

  • extern "C":

通过关键字extern "C" __declspec(dllexport)声明的接口函数可以保证__cdecl调用约定的函数名称不被改变,却不能保证__stdcall和__fastcall调用约定的函数名称不被改变

  • .def:

def文件必要元素:

LIBRARY XXX.dll  #dll名称不是必须的.写了就必须保证和要生成的dll名称一致;不写,就默认与要生成的dll名称一致

EXPORTS

要导出的函数名 @ 序号 #@ 序号不是必须的.写了的话,生成的dll就会按照该序号导出该函数;不写的话,生成的dll则会按其默认规则生成序号

要使__stdcall和__fastcall调用约定的函数不被改变,可以使用模块文件.

使用def时可以不用__declspec(dllexport),即可导出def中的名称

使用模块文件(.def)对x86和x64均适用

*:在项目属性->链接器->输入->模块导入文件中填写def文件的名称

  • #pragma:

#pragma comment(linker, "/EXPORT:要导出的函数名=函数修饰名,PRIVATE"),PRIVATE可以不写,其作用暂时没查

需要事先知道函数修饰名.编译方式(C/C++),调用约定(__cdecl,__stdcall,__fastcall,__vectorcall),编译位数(x86/x64)均会影响导出名称

使用#pragma对x86和x64均适用

不添加任何调用约定时,使用项目的默认调用约定,C/C++默认调用约定为__cdecl

*:通过项目属性->C/C++->高级->调用约定可以修改默认调用约定

DLL的使用方式

显式调用:使用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导出测试如下图

关于调用约定和DLL导出的一些总结

参考

  1. DLL 函数导出的规则和方法 <https://blog.csdn.net/xiaominggunchuqu/article/details/72837760>
  2. 带你玩转Visual Studio——调用约定__cdecl、__stdcall和__fastcall <https://blog.csdn.net/luoweifu/article/details/52425733>