如何编译生成 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工程”,确定,完成即可。

一个简单的dll

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

  1. #include <Windows.h>
  2. #include <stdio.h>
  3. BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
  4. {
  5. switch (ul_reason_for_call)
  6. {
  7. case DLL_PROCESS_ATTACH:
  8. printf("DLL_PROCESS_ATTACH\n");
  9. break;
  10. case DLL_THREAD_ATTACH:
  11. printf("DLL_THREAD_ATTACH\n");
  12. break;
  13. case DLL_THREAD_DETACH:
  14. printf("DLL_THREAD_DETACH\n");
  15. break;
  16. case DLL_PROCESS_DETACH:
  17. printf("DLL_PROCESS_DETACH\n");
  18. break;
  19. }
  20. return TRUE;
  21. }

之后,我们直接编译,即可以在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等,都是用的这种方法

显式链接调用

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

  1. HINSTANCE LoadLibrary(LPCTSTR lpszLibFile);
  2. HINSTANCE LoadLibraryEx(LPCSTR lpszLibFile,HANDLE hFile,DWORD dwFlags);

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

  1. LoadLibrary("MyDLL")

或者

  1. LoadLibrary("MyDLL.dll")

的方式进行调用,不带后缀和带后缀在搜索策略上有区别,这里不再详解。

显式释放DLL

在显式加载DLL后,在任意时刻可以调用FreeLibrary函数来显式地从进程的地址空间中解除该文件的映像。

  1. BOOL FreeLibrary(HINSTANCE hinstDll);

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

线程可以调用GetModuleHandle函数:

  1. GetModuleHandle(LPCTSTR lpszModuleName);

来判断一个DLL是否被映射进进程的地址空间。例如,下面的代码判断MyDLL.dll是否已被映射到进程的地址空间,如果没有,则装入它:

  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高级编程指南》中的两个图来说明上述四种参数的调用情况。

如何编译生成 dll如何编译生成 dll

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

  1. #include <iostream>
  2. #include <Windows.h>
  3. using namespace std;
  4. DWORD WINAPI someFunction(LPVOID lpParam)
  5. {
  6. cout << "enter someFunction!" << endl;
  7. Sleep(1000);
  8. cout << "This is someFunction!" << endl;
  9. Sleep(1000);
  10. cout << "exit someFunction!" << endl;
  11. return 0;
  12. }
  13. int main()
  14. {
  15. HINSTANCE hinstance = LoadLibrary("MyDLL");
  16. if(hinstance!=NULL)
  17. {
  18. cout << "Load successfully!" << endl;
  19. }else {
  20. cout << "Load failed" << endl;
  21. }
  22. HANDLE hThread;
  23. DWORD dwThreadId;
  24. cout << "createThread before " << endl;
  25. hThread = CreateThread(NULL,0,someFunction,NULL,0,&dwThreadId);
  26. cout << "createThread after " << endl;
  27. cout << endl;
  28. Sleep(3000);
  29. cout << "waitForSingleObject before " << endl;
  30. WaitForSingleObject(hThread,INFINITE);
  31. cout << "WaitForSingleObject after " << endl;
  32. cout << endl;
  33. FreeLibrary(hinstance);
  34. return 0;
  35. }

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

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

如何编译生成 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。

  1. #include <Windows.h>
  2. #include <stdio.h>
  3. class A{
  4. public:
  5. A(){
  6. printf("A construct...");
  7. }
  8. ~A(){
  9. printf("A deconstruct...");
  10. }
  11. };
  12. A a;
  13. BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
  14. {
  15. switch (ul_reason_for_call)
  16. {
  17. case DLL_PROCESS_ATTACH:
  18. printf("DLL_PROCESS_ATTACH\n");
  19. break;
  20. case DLL_THREAD_ATTACH:
  21. printf("DLL_THREAD_ATTACH\n");
  22. break;
  23. case DLL_THREAD_DETACH:
  24. printf("DLL_THREAD_DETACH\n");
  25. break;
  26. case DLL_PROCESS_DETACH:
  27. printf("DLL_PROCESS_DETACH\n");
  28. break;
  29. }
  30. return TRUE;
  31. }

编译为DLL后,替换掉原来的的MyDLL.dll,可以直接运行TestDLL.exe,可以看到,在DLL_PROCESS_ATTACH调用了类A的构造函数,而在DLL_PROCESS_DETACH之后,调用了类A 的析构函数。

如何编译生成 dll

在经过以上的证实之后,实际上,我们也可以理解为什么我们之前说不必在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工程中的代码(注意,这里代码没有按照头文件与源代码的分离,仅仅为了更好理解知识,在工程项目中请勿模仿

  1. #include <Windows.h>
  2. #include <stdio.h>
  3. extern "C"{
  4. class _declspec(dllexport) A{
  5. public:
  6. A(){
  7. printf("A construct...\n");
  8. }
  9. const char * whoIsMe()
  10. {
  11. return "My name is A";
  12. }
  13. ~A(){
  14. printf("A deconstruct...\n");
  15. }
  16. };
  17. _declspec(dllexport) A a;
  18. _declspec(dllexport) int Add(int x,int y)
  19. {
  20. return x+y;
  21. }
  22. _declspec(dllexport) int g_nUsageCount = 3195;
  23. }

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

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

如何编译生成 dll

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

  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\n",Add(5,3) );
  17. printf("%d\n",g_nUsageCount);
  18. printf("%s\n",a.whoIsMe());
  19. printf("-----------------------------\n");
  20. A b;
  21. printf("%s\n",b.whoIsMe());
  22. return 0;
  23. }

这里,注意的是,在导出的位置,我们用的是_declspec(dllexport) ,而在这里导入的时候,我们声明的时候,用的是 _declspec(dllimport) ,这个例子当中,我们分别导出了变量,函数,类。读者仅仅需要注意的是导入和导出关键字的使用。运行结果如下:

如何编译生成 dll

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

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

  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 参数,如下,

如何编译生成 dll

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

  1. #include <Windows.h>
  2. #include <stdio.h>
  3. //extern "C"{
  4. class _declspec(dllexport) A{
  5. public:
  6. A(){
  7. printf("A construct...\n");
  8. }
  9. const char * whoIsMe()
  10. {
  11. return "My name is A";
  12. }
  13. ~A(){
  14. printf("A deconstruct...\n");
  15. }
  16. };
  17. _declspec(dllexport) A a;
  18. _declspec(dllexport) int Add(int x,int y)
  19. {
  20. return x+y;
  21. }
  22. _declspec(dllexport) int g_nUsageCount = 3195;
  23. //}

再次编译成DLL,同样使用dumpbin工具,再次运行,如下,

如何编译生成 dll

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

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

  1. #include <Windows.h>
  2. #include <stdio.h>
  3. int main()
  4. {
  5. HINSTANCE h = LoadLibrary("MyDLL");
  6. int(*pAdd)(int,int);
  7. pAdd = (int(__cdecl *)(int,int))(GetProcAddress(h,"Add"));
  8. int sum = pAdd(239,23);
  9. printf("sum is %d\n",sum);
  10. FreeLibrary(h);
  11. return 0;
  12. }

之后,最后一幅图,运行效果,

如何编译生成 dll

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

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

上一篇:TortoiseGit客户端密钥配置


下一篇:C/C++通过WMI和系统API函数获取获取系统硬件配置信息