系统: Win10企业版 64位
vs版本: Microsoft Visual Studio 2010, 版本10.0
为从头了解dll项目中各文件的来源, 这里暂不使用vs新建项目列表中的动态链接库(DLL)向导, 选择创建一个空项目.
打开VS2010 -> 新建项目 -> win32控制台应用程序 -> 选中DLL类型 -> 空项目, 例这里创建了一个Dll1的空项目.
然后右键源文件添加Dll1…cpp源文件,我们在该文件中封装函数。
添加后如图:
然后在Dll1.cpp源文件中封装两个简单的函数:
编译运行后, 右键Dll1.cpp -> 打开文件所在位置, 返回上级目录, 进入Debug目录下, 可以看到刚生成的Dll1.dll文件:
注意: 这里没有生成引入库文件(.lib文件)
生成dll后, 可以发现这里没有任何关于该dll的说明, 比如它怎么用, 有哪些可供外部调用的函数等.
为了查看一个DLL中有哪些导出函数,可以利用Visual Studio提供的dumpbin命令行工具来实现。
有一点需要知道的是:如果应用程序想要访问某个DLL中的函数,那么该函数必须是已经被导出的函数。
该工具一般在VS的安装目录下, 命名为dumpbin.exe, 我的安装目录在E盘, 路径如下:
因为后面我们会在自己的dll项目的文件目录下使用dumpbin, 因此需要将该路径配置到系统的PATH环境变量中去,使得我们可以在任何目录使用该命令。
Win+r打开控制台,切换到Dll1.dll所在目录。或者直接在该目录下Shift+鼠标右键打开控制台。
调用如下命令:dumpbin –exports Dll1.dll, 可以看到如下信息:
发现并没有看到任何与函数(add或subtract)有关的信息,这说明Dll1.dll目前没有导出函数。
因为Dll1.cpp中并没有显式的将两个函数声明为导出函数,在函数声明前加_declspec(dllexport)声明符可将该函数声明为动态链接库的导出函数。
修改Dll1.cpp中的代码,如下:
当然_declspec(dllexport) 也可放在返回类型和函数名中间:
重新生成该动态链接库项目,可以看到如下输出信息:
至此, 就已经是一个简单生成动态链接库的的DLL项目了.
注意这里生成了Dll1.lib 和 Dll1.exp两个文件, 其中Dll1.lib就是前面提到的引入库文件。
Dll1.exp是一个输出库文件,因为后续都没涉及该文件,所以这里不再扩展。
紧接着后面也生成了Dll1.dll文件。
再次使用dumpbin命令查看Dll1.dll导出函数的信息,如下:
可以看到,这时多出了一些输出信息:
Ordinal hint RVA name
1 0 0001107D ?add@@YAHHH@Z = @ILT+120(?add@@YAHHH@Z)
2 1 00011069 ?subtract@@YAHHH@Z = @ILT+100(?subtract@@YAHHH@Z)
“ordinal”列出的信息:“1” 和 “2”是导出函数的序号;
“hint”列出的数字是提示码;
“RVA”列出的地址值是导出函数在DLL模块中的位置,也就是说,通过该地址值,可以在DLL中找到它们;
“name”列出的是导出函数的名称,可以看到这些名称和我们在Dll1.cpp中定义的不同,例我们定义的add函数在此处却显示为:?add@@YAHHH@Z
这是因为C++支持函数重载,也是为了区分多个DLL文件中可能重名的函数,在编译链接时,C++会按照自己的规则篡改函数的名称,这一过程称为“名字改编”,或也称为”名字粉碎”。
在编译链接时,C++会按照自己的规则篡改函数的名称,这一过程称为“名字改编” 或 ”名字粉碎”。
上面提到, 名字改编是为了应对 C++的函数重载机制 以及 多个Dll中重名函数 的情况, 虽都会发生名字改编, 但改编的规则却不仅相同, 例不同的编译器或调用约定改编规则会有明显差异.
这里以C 和 C++ 编译环境下, 不同调用约定的情况举例说明:
①C编译时名字改编规则
②C++编译时名字改编规则
_stdcall 调用约定: 以 ? 标识函数名的开始, 后跟函数名, 函数名后以 @@YG 标识参数表的开始, 后跟参数类型, 参数类型以以下代号表示:
X: void
D: char
E: unsigned char
F: short
H: int
I: unsigned int
J: long
K: unsigned log
M: float
N: double
_N: bool
PA: 表示指针, PA后会带代号, 表明指针的类型. 如果相同类型的指针连续出现, 以 0 代替重复, 一个 0 表示一次重复, 注意只针对指针类型.
参数表的第一项为该函数的返回值类型, 其后依次为参数的类型.
参数表后以 @Z 标识整个名字的结束, 如果该函数无参数, 则以 Z 标识结束.
例:
void _declspec(dllexport) _stdcall test(int a, int b);
改编后的函数名为: ?test@@YGXHH@Z
说明: 以?标识函数名开始, @@YG标识参数表开始, 参数表中第一项为返回类型void(对应X), 后跟第一个参数类型int(对应H), 第二个参数类型同一个参数类型一致, 但不是指针类型, 因此这里H标识int, 最后以@Z 标识整个名字的结束
int _declspec(dllexport) _stdcall add(int* a, int* b, int* c, char d);
改编后的函数名为: ?add@@YGHPAH00D@Z
说明: 依旧以?标识函数名开始, @@YG标识参数表开始, 参数表中第一项为返回类型int(对应H), 后跟第一个参数类型int*(对应PA-指针, H指针类型), 因为第二个参数类型也为int*, 因此这里以一个 0 表示重复, 第三个参数类型依旧是int*, 因此后再加一个 0 表示重复指针类型, 最后一个参数类型为char(对应D), 也依旧以 @Z 结束整个命名.
_cdecl 调用约定: 规则同上面的_stdcall调用约定, 只是参数表的开始标识由上面的 @@YG 变为 @@YA
_fastcall 调用约定: 规则同上面的_stdcall调用约定, 只是参数表的开始标识由上面的 @@YG 变为 @@YI