动态链接库(DLL)导出:需要注意的问题

一、简介

从DLL中导出函数有很多种方式,然而当你自己做DLL导出时会遇到很多问题。如果你用一种语言编写DLL而用于其他语言时,就有很多需要注意的地方。我们将分析其中的一些。
解决问题的关键是对程序究竟发生了什么了如指掌。我将以C++创建的DLL开始,然后引出很常见的一些问题。最后我们会对DLL做出改变以修复这些问题。

二、背景

早晚有一天,你会需要使用DLL导出的函数。或者你会使用一个第三方库,再或者,给第三方提供你的公有API接口。
让我们想想当你VB很流行的时候吧。VB是一个能快速编写一定框架程序的语言。两三天你就能学会如何与GUI代码交互。
VB执行太慢,而且缺少像无符号整形和位转换这样基本的功能,我认为如果不能使用其他语言编写的DLL,VB绝不会有那么成功。
当执行速度是很重要的考虑因素时,人们便用c/c++或其他语言编写DLL供调用。相比于用GDI或MFC,VB创建GUI界面更简单。你可以用各自的语言试试。

三、创建一个简单DLL

本文主要是关于管理DLL导出函数名和顺序的,因此,简化了DLL创建过程。
创建Win32控制台应用程序:
选择“DLL”:

3、1DLLMain

不只是应用程序才有Main函数,DLL也有它的DLLMain。在DLLMain中你可以分配、初始化相应的数据。一下代码是自动生成的,并没有修改过:
  1. // dllmain.cpp : Defines the entry point for the DLL application.  
  2. #include "stdafx.h"  
  3. BOOL APIENTRY DllMain( HMODULE hModule,  
  4. DWORD  ul_reason_for_call,  
  5. LPVOID lpReserved  
  6.                      )  
  7. {  
  8. switch (ul_reason_for_call)  
  9.     {  
  10. case DLL_PROCESS_ATTACH:  
  11. case DLL_THREAD_ATTACH:  
  12. case DLL_THREAD_DETACH:  
  13. case DLL_PROCESS_DETACH:  
  14. break;  
  15.     }  
  16. return TRUE;  
  17. }  

3、2__declspec(export)

导出一个函数所需做的工作量很少。只需要在函数定义最前面加上__declspec(dllexport)。我在此使用__stdcall属性来改变调用约定。(默认的调用约定是__cdecl)
  1. __declspec(dllexport) int __stdcall Add(int a, int b)  
  2. {  
  3. return a+ b;  
  4. }  

当你编译完后,就会生成DLL和.lib文件。如果你想运行时让DLL自动加载到你的应用程序中,当你链接你的程序时需要使用.lib文件。
在应用程序中,你需要添加.lib文件的引用,可以通过工程设置或者程序中添加pragma命令。我个人喜好pragma命令,因为工程设置和配置相关,你必须记得改动所有的更新配置。
  1. #include "../DLLExports/DLLExports.h"  
  2. #ifdef _DEBUG  
  3. #pragma comment(lib, "../Debug/DLLExports.lib")  
  4. #else  
  5. #pragma comment(lib, "../Release/DLLExports.lib")  
  6. #endif   
  7. int main()  
  8. {  
  9. int res = Add(88, 23);  
  10. }  

以上代码是通过DLL导入导出函数最简单的形式,这并不是一个好的DLL,因为它和其他编译器、语言的互操作性很差。

四、提高DLL的互操作性

如果你想在C、Pascal、VB、C#或者其他语言中使用这个DLL,这会让你失望,因为目前为止这个DLL只适用于C++。
我们用dumpbin.exe(注:原文为bindump.exe,在vs的工具目录下)来看看为什么其他语言不识别此DLL。
  1. dumpbin.exe /EXPORTS D:programExampleothersDLLExport.dll  

红色框起来的名字就是我们的Add函数,类型信息被C++连接器编码。但是只要调用这个DLL的应用程序也是C++编译和链接的话这并没关系。然而C语言便并没有像C++这样管理命名。
加上 extern "C"来让让C和C++调用DLL能互操作
  1. extern "C"  
  2. {  
  3. __declspec(dllexport) int __stdcall Add(int a, int b);  
  4. }  

然后重新生成一次,这时候使用bindemp.exe查看DLL文件:
现在的名字变成了_Add@8。为什么是_Add@8而不是Add?现在是C编译器处理的结果了。
C编译器处理后开头使用一个下划线,接着是函数名,最终以参数占用的字节数多少来结束。我们传递了2个整形数据(共8字节),因此最后是8。你现在可以在C/C++环境中使用这个库。这已经进步了一些,但还是不够强大。
调用约定十分严格,但是修饰后的名字却不严整。有很多语言根本不修饰名字。通过使用dumpbin.exe查看DLL中被修饰了的名字,我们也能通过修饰了的函数名字来调用它,但是当你用其他编译器、更改参数等等,被修饰的名字就会改变。
从图中可以看出_Add@8重复了1次。 _Add@8 = @ILT+155(_Add@8)  左边是导出函数名,右边是内部使用的名字。在这种情况下它们是相同的。在下一节中,我们将学会如何改变导出函数名。

使用.def文件

.def文件能让你对导出的函数有更多的控制权。不知道为什么很多写DLL技术的人并没有提到这个文件的使用。
添加一个以.def结尾的文件到工程,名字无所谓。我通常使用exports.def
export文件应该是下面的格式
  1. LIBRARY DLLExports  
  2. EXPORTS  
  3.     Add     @1  

然后转到工程的属性页,在“Linker-》input”下添加此文件名
添加了这个文件之后编译生成,用dumpbin.exe打开DLL
可以看出,添加了.def文件后输出函数名就只有Add,没有其他的修饰了。

.def做了什么?

从DLL中导出的函数被从新分配了一个标号和一个可选的名字。对于导出给外部使用函数来说,添加一个函数名是有必要的。而对于只是DLL内部、而不公开的函数来说,给一个标号就行了。我们现在来看看USER32.dll的导出函数
由上可以看出,总共导出函数为1062个,但是只有822个是有名字的。意味着你只能按顺序访问它们。通常的顺序是从1开始的,但也可能是从其他数开始,比如上面的1502.
现在我们自己再来创建一个DLL。这个DLL包括两个函数:公开的Foo函数,有名字和下标;另一个私有函数Bar,只能通过下标访问。为了能让你更明白,我们将下标从1502开始,而且在下标数上故意让Bar从1505开始。
  1. LIBRARY DLLExports  
  2. EXPORTS  
  3.     Foo     @1502  
  4.     Bar     @1505   NONAME  

此时,如果你要访问Bar函数,就需要动态加载,使用下标1505访问:
  1. HMODULE hLoadedLibrary = LoadLibrary("DLLExports.dll");  
  2. // Notice __stdcall on function pointer typedef  
  3. typedef int (__stdcall* BarFunc)(int, int);   
  4. BarFunc Bar = (BarFunc) GetProcAddress(hLoadedLibrary, (LPCSTR)1505);  
  5. int result = Bar(6,3);  
  6. printf("#1505(6,3) = %i ", result);  
  7. FreeLibrary(hLoadedLibrary);  

转载:http://blog.csdn.net/fightforprogrammer/article/details/38051181

原文地址:https://www.cnblogs.com/DeeLMind/p/7346918.html