原文: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”中,键入如下代码

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

好的,介绍了以上的情况,下面,我们来继续实践,这次,建立一个新的空的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"必须拷贝到能够让我们这个工程找到的地方,也就是上面我们提到的搜索路径中的一个地方。

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

有了上面我们介绍的知识,这个就不是很难理解,主进程在调用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的源代码中实现一个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文件夹下。

之后,修改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) ,这个例子当中,我们分别导出了变量,函数,类。读者仅仅需要注意的是导入和导出关键字的使用。运行结果如下:

另外,大家可能对为什么要用 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 参数,如下,

之后,去掉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工具,再次运行,如下,

两幅图对比着看,主要看我们用红框圈出来的部分,这样,特别是一会儿我们准备调用的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. }

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

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

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

如何编译生成 dll的更多相关文章

  1. python调取C/C++的dll生成方法

    本文针对Windows平台下,python调取C/C++的dll文件. 1.如果使用C语言,代码如下,文件名为test.c. __declspec(dllexport) int sum(int a,i ...

  2. c++ c# java 调用 c++ 写的dll

    1. vs 中新建win32 dll 项目   testdll 添加实现文件       test.cpp #include "stdafx.h" #include <ios ...

  3. 在C++中调用DLL中的函数 (3)

    1.dll的优点 代码复用是提高软件开发效率的重要途径.一般而言,只要某部分代码具有通用性,就可将它构造成相对独立的功能模块并在之后的项目中重复使用.比较常见的例子是各种应用程序框架,ATL.MFC等 ...

  4. C#动态引用DLL的方法

    C#编程中,使用dll调用是经常的事,这样做的好处是非常多的,比如把某些功能封装到一个dll中,然后主程序动态调用这个dll. 废话不多说,举例说明如下. 首先,我们需要封装一个dll,vs2008下 ...

  5. VC++创建、调用dll的方法步骤

    文章来源:http://www.cnblogs.com/houkai/archive/2013/06/05/3119513.html 代码复用是提高软件开发效率的重要途径.一般而言,只要某部分代码具有 ...

  6. Reflector+Reflexil 相结合实现对DLL文件修改

    在工作过程中,我们有可能遇到这样的问题:公司发给客户的软件包突然报错了,但是你知道哪里报错了,而这个代码已经编译成DLL文件了,源代码不在自己这里.怎么办呢?还好现在有Reflexil插件,这个插件只 ...

  7. VC++2008 用空工程创建 DLL

    VC++2008 用空工程创建 DLL 一.创建 DLL 工程项目: 1)点击菜单[File] -> [New] -> [Project...] 弹出 “New Project” 对话框: ...

  8. 动态链接库dll的 静态加载 与 动态加载

    dll 两种链接方式  : 动态链接和静态链接(链接亦称加载) 动态链接是指在生成可执行文件时不将所有程序用到的函数链接到一个文件,因为有许多函数在操作系统带的dll文件中,当程序运行时直接从操作系统 ...

  9. C# 调用 C++ DLL方法

    在C# 中,可以通过 DllImport 调用C++ 的非托管DLL程序. VS2010中C#调用C++的DLL示例: 一.新建C++ DLL程序 1.新建 C++ Win32项目,类型为DLL. 生 ...

随机推荐

  1. python http请求

    from httplib2 import Http def postDada(): http=Http() heads={"pragma":"no-cache" ...

  2. 编译 wl18xx驱动源码

    在做beagleboneblack移植的时候,wl18xx的驱动源码是自动编译的.但是移植到其他平台优越平台不一样,所以就不能自动编译 所以用其他方式编译.http://e2e.ti.com/supp ...

  3. Hibernate 系列教程14-继承-PerTable策略

    Employee public class Employee { private Long id; private String name; HourlyEmployee public class H ...

  4. preg_replace 方法

    标红关键字 $text = "Sample sentence from KomunitasWeb, regex has become popular in web programming. ...

  5. GCC: compilation process..

    gcc -Iproj/src myfile.c -o myfile gcc -c myfile.c "compile without linking gcc -D DEBUG myfile. ...

  6. AutoTile 自动拼接(一) 学习与实践

    恩,大家好,这两天江苏冷空气袭击,下了今年 第一场第二场雪. 不过今天我要说的 ,和 上面的 屁关系都没有. 今天要说的是 2d无缝自动拼接.大家有没有玩过  RPG Maker VX Ace. 类似 ...

  7. linux shell 远程执行命令

    经常要部署多台服务器上面的应用,如果一个个机器的登录太麻烦. 所有就想到编写一个脚本来部署不同的服务器 前提条件: 配置ssh免登陆 如果不会的请参加我的另外一篇文章 http://blog.csdn ...

  8. 扫盲: JAVA基本常识

    http://java-mzd.iteye.com/blog/838514

  9. java导读

    导读:      我们学习Java大概有3个方向,第一,桌面系统,包括C/S结构:第二,J2ME,面向无限领域,很有潜力的家伙,看看中国的手机用户就知道了.第三,面向企业应用.计算的平台,J2EE. ...

  10. Warning: Cannot modify header information - headers already sent by ... functions_general.php on line 45

    摘自:有用到 http://blog.csdn.net/besily/article/details/5396268 PHP错误:Warning: Cannot modify header infor ...