如何编译生成 dll

原文:http://blog.csdn.net/qianchenglenger/article/details/21599235

 

目录(?)[-]

  1. 创建DLL工程
  2. 一个简单的dll
  3. 隐式链接调用
  4. 显式链接调用
  5. 显式释放DLL
  6. DLL的进入与退出函数
  7. DllMain与C运行库
  8. 从DLL中输出函数和变量
 

动态链接库是Windows的基石。所有的Win32 API函数都包含在DLL中。3个最重要的DLL是KERNEL32.DLL,它由管理内存、进程和线程的函数组成;USER32.DLL,它由执行用户界面的任务(如创建窗口和发送消息)的函数组成;GDI32.DLL,它由绘图和显示文本的函数组成。在此,我们主要用实际的操作过程,简要的说明如何创建自己的 Win32 DLL。

创建DLL工程

这里,我们为了简要说明DLL的原理,我们决定使用最简单的编译环境VC6.0,如下图,我们先建立一个新的Win32 Dynamic-Link Library工程,名称为“MyDLL”,在Visual Studio中,你也可以通过建立Win32控制台程序,然后在“应用程序类型”中选择“DLL”选项,

点击确定,选择“一个空的DLL工程”,确定,完成即可。

一个简单的dll

在第一步我们建立的工程中建立一个源码文件”dllmain.cpp“,在“dllmain.cpp”中,键入如下代码

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #include <Windows.h>  
  2. #include <stdio.h>  
  3.   
  4. BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)  
  5. {  
  6.     switch (ul_reason_for_call)  
  7.     {  
  8.     case DLL_PROCESS_ATTACH:  
  9.         printf("DLL_PROCESS_ATTACH ");  
  10.         break;  
  11.     case DLL_THREAD_ATTACH:  
  12.         printf("DLL_THREAD_ATTACH ");  
  13.         break;  
  14.     case DLL_THREAD_DETACH:  
  15.         printf("DLL_THREAD_DETACH ");  
  16.         break;  
  17.     case DLL_PROCESS_DETACH:  
  18.         printf("DLL_PROCESS_DETACH ");  
  19.         break;  
  20.     }  
  21.     return TRUE;  
  22. }  

之后,我们直接编译,即可以在Debug文件夹下,找到我们生成的dll文件,“MyDLL.dll”,注意,代码里面的printf语句,并不是必须的,只是我们用于测试程序时使用。而DllMain函数,是dll的进入/退出函数。

实际上,让线程调用DLL的方式有两种,分别是隐式链接显式链接,其目的均是将DLL的文件映像映射进线程的进程的地址空间。我们这里只大概提一下,不做深入研究,如果感兴趣,可以去看《Window高级编程指南》的第12章内容。

隐式链接调用

隐士地链接是将DLL的文件影响映射到进程的地址空间中最常用的方法。当链接一个应用程序时,必须制定要链接的一组LIB文件。每个LIB文件中包含了DLL文件允许应用程序(或另一个DLL)调用的函数的列表。当链接器看到应用程序调用了某个DLL的LIB文件中给出的函数时,它就在生成的EXE文件映像中加入了信息,指出了包含函数的DLL文件的名称。当操作系统加载EXE文件时,系统查看EXE文件映像的内容来看要装入哪些DLL,而后试图将需要的DLL文件映像映射到进程的地址空间中。当寻找DLL时,系统在系列位置查找文件映像。

  • 1.包含EXE映像文件的目录
  • 2.进程的当前目录
  • 3.Windows系统的目录
  • 4.Windows目录
  • 5.列在PATH环境变量中的目录

这种方法,一般都是在程序链接时控制,反映在链接器的配置上,网上大多数讲的各种库的配置,比如OPENGL或者OPENCV等,都是用的这种方法

显式链接调用

这里我们只提到两种函数,一种是加载函数

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. HINSTANCE LoadLibrary(LPCTSTR lpszLibFile);  
  2.   
  3. HINSTANCE LoadLibraryEx(LPCSTR lpszLibFile,HANDLE hFile,DWORD dwFlags);  

返回值HINSTANCE值指出了文件映像映射的虚拟内存地址。如果DLL不能被映进程的地址空间,函数就返回NULL。你可以使用类似于

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. LoadLibrary("MyDLL")  
或者
[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. LoadLibrary("MyDLL.dll")  
的方式进行调用,不带后缀和带后缀在搜索策略上有区别,这里不再详解。

显式释放DLL

在显式加载DLL后,在任意时刻可以调用FreeLibrary函数来显式地从进程的地址空间中解除该文件的映像。
[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. BOOL FreeLibrary(HINSTANCE hinstDll);  

这里,在同一个进程中调用同一个DLL时,实际上还牵涉到一个计数的问题。这里也不在详解。

线程可以调用GetModuleHandle函数:

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. GetModuleHandle(LPCTSTR lpszModuleName);  
来判断一个DLL是否被映射进进程的地址空间。例如,下面的代码判断MyDLL.dll是否已被映射到进程的地址空间,如果没有,则装入它:
[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. HINSTANCE hinstDll;  
  2. hinstDll = GetModuleHandle("MyDLL");  
  3. if (hinstDll == NULL){  
  4.     hinstDll = LoadLibrary("MyDLL");  
  5. }  
实际上,还有一些函数,比如 GetModuleFileName用来获取DLL的全路径名称,FreeLibraryAndExitThread来减少DLL的使用计数并退出线程。具体内容还是参见《Window高级编程指南》的第12章内容,此文中不适合讲太多的内容以至于读者不能一下子接受。

DLL的进入与退出函数

说到这里,实际上只是讲了几个常用的函数,这一个小节才是重点。

在上面,我们看到的MyDLL的例子中,有一个DllMain函数,这就是所谓的进入/退出函数。系统在不同的时候调用此函数。这些调用主要提供信息,常常被DLL用来执行进程级或线程级的初始化和清理工作。如果你的DLL不需要这些通知,就不必再你的DLL源代码中实现此函数,例如,如果你创建的DLL只含有资源,就不必实现该函数。但如果有,则必须像我们上面的格式。

DllMain函数中的ul_reason_for_call参数指出了为什么调用该函数。该参数有4个可能值: DLL_PROCESS_ATTACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH、DLL_PROCESS_DETACH。

其中,DLL_PROCESS_ATTACH是在一个DLL首次被映射到进程的地址空间时,系统调用它的DllMain函数,传递的ul_reason_for_call参数为DLL_PROCESS_ATTACH。这只有在首次映射时发生。如果一个线程后来为已经映射进来的DLL调用LoadLibrary或LoadLibraryEx,操作系统只会增加DLL的计数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。

而DLL_PROCESS_DETACH是在DLL被从进程的地址空间解除映射时,系统调用它的DllMain函数,传递的ul_reason_for_call值为DLL_PROCESS_DETACH。我们需要注意的是,当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明初始化不成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain。因此,必须确保没有清理那些没有成功初始化的东西。

DLL_THREAD_ATTACH:当进程中创建一个线程时,系统察看当前映射到进程的地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用所有的这些DLL的DllMain函数。该通知告诉所有的DLL去执行线程级的初始化。注意,当映射一个新的DLL时,进程中已有的几个线程在运行,系统不会为已经运行的线程用值DLL_THREAD_ATTACH调用DLL的DllMain函数。

而DLL_THREAD_DETACH,如果线程调用ExitThread来终结(如果让线程函数返回而不是调用ExitThread,系统会自动调用ExitThread),系统察看当前映射到进程空间的所有DLL文件映像,并用值DLL_THREAD_DETACH来调用所有的DLL的DllMain函数。该通知告诉所有的DLL去执行线程级的清理工作。

这里,我们需要注意的是,如果线程的终结是因为系统中的一个线程调用了TerminateThread,系统就不会再使用DLL_THREAD_DETACH来调用DLL和DllMain函数。这与TerminateProcess一样,不再万不得已时,不要使用。

下面,我们贴出《Window高级编程指南》中的两个图来说明上述四种参数的调用情况。

好的,介绍了以上的情况,下面,我们来继续实践,这次,建立一个新的空的win32控制台工程TestDLL,不再多说,代码如下:

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #include <iostream>  
  2. #include <Windows.h>  
  3. using namespace std;  
  4.   
  5. DWORD WINAPI someFunction(LPVOID lpParam)  
  6. {  
  7.     cout << "enter someFunction!" << endl;  
  8.     Sleep(1000);  
  9.     cout << "This is someFunction!" << endl;  
  10.     Sleep(1000);  
  11.     cout << "exit someFunction!" << endl;  
  12.     return 0;  
  13. }  
  14.   
  15. int main()  
  16. {  
  17.     HINSTANCE hinstance = LoadLibrary("MyDLL");  
  18.     if(hinstance!=NULL)  
  19.     {  
  20.         cout << "Load successfully!" << endl;  
  21.     }else {  
  22.         cout << "Load failed" << endl;  
  23.     }  
  24.     HANDLE hThread;  
  25.     DWORD dwThreadId;  
  26.       
  27.     cout << "createThread before " << endl;  
  28.     hThread = CreateThread(NULL,0,someFunction,NULL,0,&dwThreadId);  
  29.     cout << "createThread after " << endl;  
  30.     cout << endl;  
  31.       
  32.     Sleep(3000);  
  33.   
  34.     cout << "waitForSingleObject before " << endl;  
  35.     WaitForSingleObject(hThread,INFINITE);  
  36.     cout << "WaitForSingleObject after " << endl;  
  37.     cout << endl;  
  38.   
  39.     FreeLibrary(hinstance);  
  40.     return 0;  
  41. }  

代码很好理解,但是前提是,你必须对线程有一定的概念。另外,注意,我们上面编译的获得的“MyDLL.dll"必须拷贝到能够让我们这个工程找到的地方,也就是上面我们提到的搜索路径中的一个地方。

这里,我们先贴结果,当然,这只是在我机器上其中某次运行结果。

有了上面我们介绍的知识,这个就不是很难理解,主进程在调用LoadLibrary时,用DLL_PROCESS_ATTACH调用了DllMain函数,而线程创建时,用DLL_THREAD_ATTACH调用了DllMain函数,而由于主线程和子线程并行的原因,可能输出的时候会有打断。但是,这样反而能让我们更清楚的理解程序。

DllMain与C运行库

”在前面对DllMain函数的讨论中,我假设读者使用Microsoft的Visual C++编译器来建立自己的动态链接库。当编写DLL时,可能会需要一些C运行库的启动帮助。比方说,你建立的DLL中包含一个全局变量,它是一个C++类的实例。在DLL能使用该全局变量之前,必须调用了它的构造函数——这就是C运行时库的DLL启动代码的工作。“

上面一段话也就是告诉我们,实际上,当DLL文件被映射到进程的地址空间中时,系统实际上调用的并不直接是DllMain函数,而是另外一个函数,需要先完成一些初始化工作,实际上,这个函数便是_DllMainCRTStartup函数。该函数初始化了C运行时库,并确保当它接收到DLL_PROCESS_ATTACH通知时,所有的全局或静态C++对象都被创建了。为了解释这点,我们准备对以上的MyDLL.dll代码进行一些修改,如下,其中增加了一个类A,以及定义了一个全局变量a。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #include <Windows.h>  
  2. #include <stdio.h>  
  3.   
  4. class A{  
  5. public:  
  6.     A(){  
  7.         printf("A construct...");  
  8.     }  
  9.     ~A(){  
  10.         printf("A deconstruct...");  
  11.     }  
  12. };  
  13.   
  14. A a;  
  15.   
  16. BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)  
  17. {  
  18.     switch (ul_reason_for_call)  
  19.     {  
  20.     case DLL_PROCESS_ATTACH:  
  21.         printf("DLL_PROCESS_ATTACH ");   
  22.         break;  
  23.     case DLL_THREAD_ATTACH:  
  24.         printf("DLL_THREAD_ATTACH ");  
  25.         break;  
  26.     case DLL_THREAD_DETACH:  
  27.         printf("DLL_THREAD_DETACH ");  
  28.         break;  
  29.     case DLL_PROCESS_DETACH:  
  30.         printf("DLL_PROCESS_DETACH ");  
  31.         break;  
  32.     }  
  33.     return TRUE;  
  34. }  
编译为DLL后,替换掉原来的的MyDLL.dll,可以直接运行TestDLL.exe,可以看到,在DLL_PROCESS_ATTACH调用了类A的构造函数,而在DLL_PROCESS_DETACH之后,调用了类A 的析构函数。

在经过以上的证实之后,实际上,我们也可以理解为什么我们之前说不必在DLL的源代码中实现一个DllMain函数,因为如果你没有DllMain函数,C运行库有它自己的一个DllMain函数。链接器链接DLL时,如果它在DLL的OBJ文件中找不到一个DllMain函数,就会链接C运行时的DllMain函数的实现。

从DLL中输出函数和变量

当创建一个DLL时,实际上创建了一组能让EXE或其他DLL调用的一组函数。当一个DLL函数能被EXE或另一个DLL文件使用时,它被称为输出了(exported)。

这里,我们只说一种方法,即用_declspec(dllexport) 的方法。当然,也可以用def文件,但是,我们最常用的还是 _declspec(dllexport)的方法。什么是输出函数与输出变量。简单的来说,你开发一个dll之后,一般都是想让别的程序员开发的应用程序或dll调用,而输出变量就是为了完成这件事情的。想一想你在开发windows应用程序时调用的各种api,实际上,大部分也都是在dll中封起来的函数。废话不多少,上代码,我们还用以上的两个工程,MyDLL工程和TestDLL工程,但是这次,大幅度修改了代码,我们删去了DllMain函数,增加了一个函数,和一个全局整形变量,同时也修改了类A,这次,我们准备先来点正常。

其中MyDLL工程中的代码(注意,这里代码没有按照头文件与源代码的分离,仅仅为了更好理解知识,在工程项目中请勿模仿

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #include <Windows.h>  
  2. #include <stdio.h>  
  3. extern "C"{  
  4. class _declspec(dllexport) A{  
  5. public:  
  6.     A(){  
  7.         printf("A construct... ");  
  8.     }  
  9.     const char * whoIsMe()  
  10.     {  
  11.         return "My name is A";  
  12.     }  
  13.     ~A(){  
  14.         printf("A deconstruct... ");  
  15.     }  
  16. };  
  17.   
  18. _declspec(dllexport) A a;  
  19.   
  20. _declspec(dllexport) int Add(int x,int y)  
  21. {  
  22.     return x+y;  
  23. }  
  24.   
  25. _declspec(dllexport) int g_nUsageCount = 3195;  
  26. }  

这里需要注意的是 _declspec(dllexport)  ,代表了dll导出的意思,编译组建一下,你会发现,这次,我们得到的不在单单是一个dll文件,还有MyDLL.lib和MyDLL.exp文件,其中,这些文件的意思,请参见此文 .dll,.lib,.def 和 .exp文件 

之后,我们这里决定先用上面提到的隐式链接的方法进行调用。我们需要先配置一下我们的TestDLL工程,配置方法如下,选择“工程”->“设置”弹出一下窗口,选择“链接”标签页,然后按照我们下面圈红的部分,添加上“MyDLL.lib”文件以及相应的附加库路径(及lib所在的位置),这里我们为了方便起见,把MyDLL工程的Debug文件夹下生成的dll与lib均拷贝到了TestDLL的Debug文件夹下。

之后,修改TestDLL工程的源代码如下,(这里,我们再次声明,我们没有用头文件的方式,非常不建议这样用。)

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #include <Windows.h>  
  2. #include <stdio.h>  
  3. extern "C"{  
  4.     _declspec(dllimport) int Add(int x,int y);  
  5.     _declspec(dllimport) int g_nUsageCount;  
  6.     class _declspec(dllimport) A{  
  7.     public:  
  8.             A();  
  9.             const char * whoIsMe();  
  10.             ~A();  
  11.     };  
  12.     _declspec(dllimport) A a;  
  13. }  
  14. int main()  
  15. {  
  16.     printf("%d ",Add(5,3) );  
  17.     printf("%d ",g_nUsageCount);  
  18.     printf("%s ",a.whoIsMe());  
  19.     printf("----------------------------- ");  
  20.     A b;  
  21.     printf("%s ",b.whoIsMe());  
  22.     return 0;  
  23. }  
这里,注意的是,在导出的位置,我们用的是_declspec(dllexport) ,而在这里导入的时候,我们声明的时候,用的是 _declspec(dllimport) ,这个例子当中,我们分别导出了变量,函数,类。读者仅仅需要注意的是导入和导出关键字的使用。运行结果如下:

另外,大家可能对为什么要用 extern "C"括起来表示好奇,这里可以先推后考虑,我们在说到如何显式加载该文件时会提到。

这里,建议大家一下,如果将类A声明部分的构造函数删除,即改为

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. class _declspec(dllimport) A{  
  2. public:  
  3.         const char * whoIsMe();  
  4. };  
想想会发生什么,不妨动手试一下,这又是为什么?如果还理解,说明你可能对动态链接库的lib文件理解不够透彻,可以再读一读我们上面说的那篇文章 .dll,.lib,.def 和 .exp文件

到这里,实际上我们已经大致说完动态链接库的相关内容,但是,既然我们上面提到了显式调用,那么,想过没有如果才能显式调用我们现在的这个dll文件,还有,那个extern “C”到底是什么,这里,我们还是先推荐一篇文章,extern "C"的用法解析

看了这么多,快疯了吧? 有点儿接受不了,告诉你,笔者也写的快疯了,come on ! 动手干活,先找一个工具,dumpbin,一般在你VC或VS的安装目录的某个bin文件夹下,搜一下就出来了(笔者的VC下的dumpbin不能用,所以用VS2013下的dumpbin了,但是,应该变化不大,如果不同,还请见谅),然后再cmd中运行,如笔者一样以下的截图一样,加上 -EXPORTS 参数,如下,

之后,去掉extern “C”,如下,

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #include <Windows.h>  
  2. #include <stdio.h>  
  3. //extern "C"{  
  4. class _declspec(dllexport) A{  
  5. public:  
  6.     A(){  
  7.         printf("A construct... ");  
  8.     }  
  9.     const char * whoIsMe()  
  10.     {  
  11.         return "My name is A";  
  12.     }  
  13.     ~A(){  
  14.         printf("A deconstruct... ");  
  15.     }  
  16. };  
  17.   
  18. _declspec(dllexport) A a;  
  19.   
  20. _declspec(dllexport) int Add(int x,int y)  
  21. {  
  22.     return x+y;  
  23. }  
  24.   
  25. _declspec(dllexport) int g_nUsageCount = 3195;  
  26. //}  
再次编译成DLL,同样使用dumpbin工具,再次运行,如下,

两幅图对比着看,主要看我们用红框圈出来的部分,这样,特别是一会儿我们准备调用的Add方法,很容易发现,有extern "C"的直接为“Add”,而去掉extern "C"之后,变成了“?Add@@YAHHH@Z”,后面那一场串东西,实际上就是编译器为了解决重载问题加入的东西。

这里,我们为了调用方便,我们使用带有 extern "C"的版本,TestDLL代码如下:

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #include <Windows.h>  
  2. #include <stdio.h>  
  3.   
  4. int main()  
  5. {  
  6.     HINSTANCE h = LoadLibrary("MyDLL");  
  7.     int(*pAdd)(int,int);  
  8.     pAdd = (int(__cdecl *)(int,int))(GetProcAddress(h,"Add"));  
  9.     int sum = pAdd(239,23);  
  10.     printf("sum is %d ",sum);  
  11.     FreeLibrary(h);  
  12.     return 0;  
  13. }  
之后,最后一幅图,运行效果,

代码中主要用到的就是 GetProcAddress函数,来获取函数指针,之后通过函数指针调用Add函数,如果感兴趣的话,可以将pAdd的值输出出来,看一下,是否和我们用dumpbin看到的相互一致。而我们用extern "C"的原因在于,如果不使用的话,我们在调用GetProcAddress函数时,填第二个参数,将会令人头疼。

好了,结束。顺便一提,此文实际上还说的比较简略,如果想深入研究,还是找本书,细细研究几遍的好。

原文地址:https://www.cnblogs.com/oxspirt/p/5893613.html