zoukankan      html  css  js  c++  java
  • 如何编译生成 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函数时,填第二个参数,将会令人头疼。

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

  • 相关阅读:
    HttpRuntime.Cache的使用经验
    js 字符串中取得第一个字符和最后一个字符
    CSAPP笔记-第一章
    共和党减税法案的个人减税
    bash学习进行中
    建站日志
    Python学习进行中
    Check your data! 数据预处理血泪教训
    bash
    【python技巧系列】在循环中处理异常并继续运行
  • 原文地址:https://www.cnblogs.com/oxspirt/p/5893613.html
Copyright © 2011-2022 走看看