55_动态链接库,还在傻傻分不清楚?
动态链接库,还在傻傻分不清楚?
程序中用到的Windows API函数都包含在动态链接库
(Dynamic Link Library,DLL)中,其中3个最重要的动态链接库(DLL)分别是Kernel32.dll -User32.dIl和
Gdi32.dll oKernel32.dll中包含的函数用来管理内存、进程以及线程等,User32.dll中包含的函数用来执行与用户界面相关的任务,例如创建窗口和发送消息等,Gdi32.dIl中包含的酗数用来绘制图像和显示文字等。此外,Windows还提供了其他一些动态链接库,例如,AdvAPI32.dll中包含的函数与对象的安全性、注册表的操控以及事件日志有关,
ComCtl32.dll支持所有常用的窗口控件,Ws2_32.dll用于Windows套接字网络编程。
当运行一个程序时,PE加载器会为新的进程创建一个虚拟地址空间,并将可执行模块映射到新进程的地址空间中,PE加载器继续解析可执行模块的导入表,并把导入表中列出的每个DLL映射到进程的地址空间中,当可执行模块和所有DLL模块都映射到进程的地址空间中后,进程的主线程就开始执行。
动态链接库是Windows系统中实现共享函数库的一种方式,大部分动态链接库以扩展名为.dll的文件形式存在,但并不是只有扩展名为.dll的文件扌是动态链接库,系统中的某些可执行文件(.exe)、字体文件(.fon) 、一些驱动程序(.drv和.sys)、各种控件(.ocx)和输入法模块
(.ime)等都是动态链接库。一个文件是不是动态链接库取决于它的文件结构,DLL文件和可执行文件都是标准的PE文件格式,只是文件头中的属性位不同。与可执行文件一样,在动态链接库中也可以定义并使用各种资源,导入并使用其他动态链接库中的函数。
静态链接库
我们先来了解一下静态链接库(Static Link Library) ,静态链接库是扩展名为.lib的对象库(Object Library)文件,在编译链接时,程序中用到的静态链接库中的函数会复制到生成的可执行文件中,因此在发布软件时,不需要一同发布静态链接库.lib文件。
创建静态链接库.lib项目的方法与创建其他Windows桌面程序相同,只是在最后需要选择应用程序类型为静态库(.lib)

接下来我们编写一个简单的静态链接库文件,StaticLinkLibrary.h头文件中的内容为函数声明︰
#pragma once
int funAdd(int a, int b);
int funMul(int a, int b);
StaticLinkLibrary.cpp源文件中的内容为函数实现∶
#include "StaticLinkLibrary.h"
int funAdd(int a, int b)
{
return a + b;
}
int funMul(int a, int b)
{
return a * b;
}
单击VS的生成菜单项→生成StaticLinkLibrary(U)或者按快捷键Ctrl + B,即可生成
Chapter6\StaticLinkLibrarylDebug\StaticLinkLibrary.lib文件。
接下来创建一个Win32桌面应用程序
StaticLinkLibraryTest,StaticLinkLibraryTest.cpp源文件的内容如下∶
#include <Windows.h>
#include "StaticLinkLibrary.h" // StaticLinkLibrary.h头文件
#pragma comment(lib, "StaticLinkLibrary.lib") // StaticLinkLibrary.lib对象库
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
TCHAR szBuf[256] = { 0 };
wsprintf(szBuf, TEXT("funAdd(5, 6) = %d\nfunMul(5, 6) = %d"), funAdd(5, 6), funMul(5, 6));
MessageBox(NULL, szBuf, TEXT("提示"), MB_OK);
}
需要注意的是,StaticLinkLibrary项目中的StaticLinkLibrary.h头文件,以及生成的
StaticLinkLibrary.lib对象库文件,都需要复制到StaticLinkLibraryTest项目目录
Chapter6\StaticLinkLibrary TestlStaticLinkLibraryTest中。

编译运行程序,程序运行效果如下图所示。

动态链接库
静态链接库是传统的共享函数库的一种方式,显而易见的一个缺点是,如果有多个程序用到静态链接库中的同一个函数,那么所有这些可执行文件中都会包含一份完全相同的代码,这是对磁盘空间的一种浪费。另一个缺点是,如果某个函数因为发现有错误或者需要更新算法而进行版本升级时,必须找到所有使用该函数的可执行文件来重新编译,否则程序中使用的还是旧版本的代码。另外,Windows是多任务操作系统,如果有多个程序用到静态链接库中的同一个函数,并且这些程序同时运行,就会有多份相同的代码被载入内存,这是对内存空间的一种浪费。
Windows的解决方法是使用动态链接库。动态链接库也是共享函数库的一种方式,其提供的函数也可以被多个程序使用,但是它和静态链接库在使用方法上有很多不同点。静态链接库仅在编译链接时使用,编译完成后,可执行文件即可脱离库文件单独使用,而动态链接库中的代码在程序编译时并不会被插入可执行文件中,在程序运行时扌将动态链接库的代码载入内存,因此称为“动态链接”。如果有多个程序用到同一个动态链接库,Windows物理内存中只存在一份动态链接库的代码,然后通过分页机制将这份代码映射到不同进程的地址空间中,库代码实际占用的物理内存永远只有一份。当然,动态链接库使用的数据段会被映射到不同的物理内存中,有多少个程序在使用动态链接库就会有多少份数据段,每个使用DLL的进程都有自己的DLL数据副本。
创建DLL项目
一个DLL可以导出变量、C++类和函数供其他模块(可执行模块或其他DLL模块)使用,在实际开发中,应该避免从DLL中导出变量和C++类,因为从DLL中导出变量不利于代码维护,由不同C++编译器或同一个C++编译器但不同版本编译生成的DLL中的C++类互不兼容。在DLL中可以使用两种函数∶内部酗数和导出函数,内部函数只能在DLL内部使用,而导出函数可以被其他模块调用,DLL的主要功能是向外导出函数供其他模块调用。
在包含导出函数的DLL文件中,导出信息保存在DLL文件的导出表中,导出表包括导出酗数的名称、序数和入口地址等信息,PE加载器通过这些信息来完成动态链接的过程。后面章节会详细介绍导出表。
创建动态链接库项目(以下简称DLL项目)与创建其他
Windows桌面程序项目相同,只是在最后的步骤中需要选择应用程序类型为动态链接库(.dll) 。接下来创建一个DLL项DllSample,头文件DllSample.h的内容如下∶
// 定义DLL的导出变量、类和函数
#include <Windows.h>
#include <strsafe.h>
#include <tchar.h>
#define DLL_EXPORT
#include "DllSample.h"
// 变量
int nValue; // 普通变量
POSITION ps; // 结构体变量
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
nValue = 5;
ps.x = 6;
ps.y = 7;
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
// 类
CStudent::CStudent(LPTSTR lpName, int nAge)
{
if (m_szName)
StringCchCopy(m_szName, _countof(m_szName), lpName);
m_nAge = nAge;
}
CStudent::~CStudent()
{
}
LPTSTR CStudent::GetName()
{
return m_szName;
}
int CStudent::GetAge()
{
return m_nAge;
}
// 函数
int WINAPI funAdd(int a, int b)
{
return a + b;
}
int WINAPI funMul(int a, int b)
{
return a * b;
}
单击VS的生成菜单→生成DlISample(U)或者按快捷键
Ctrl + B,即可生成对应的DLL文件,同时生成的还有一个.lib导入库文件,在其他源程序中调用生成的DLL中的函数时,需要用到DIISample.h DllSample.lib和DllSample.dll这3个文件。
以.lib为后缀的库有两种,一种是静态链接库的对象库,另一种是动态链接库的导入库,对象库和导入库虽然使用相同的后缀名,但是实质上截然不同。对象库中包含函数名以及函数的实现代码,而导入库只包含对应函数的一些基本信息,函数的具体实现代码存在于DLL文件中。编译链接DLL时,链接程序查找关于导出变量、C++类和函数的信息,并自动生成一个.lib文件,该.lib文件包含一个DLL文件导出的符号列表,当调用一个DLL中的函数时,导入库是必不可少的。
如前所述,在实际开发过程中,应该避免从DLL中导出变量和C++类,因为从DLL中导出变量不利于代码维护,由不同C++编译器或同一C++编译器但不同版本编译生成的DLL中的C++类互不兼容。所以前面的头文件DllSample.h的内容可以简化为如下形式∶
#pragma once
#ifdef DLL_EXPORT
#define DLL_API extern "C" __declspec(dllexport)
#else
#define DLL_API extern "C" __declspec(dllimport)
#endif
// 导出函数
DLL_API int funAdd(int a, int b);
DLL_API int funMul(int a, int b);
DllSample.cpp源文件的内容可以简化为如下形式︰
// 定义DLL的导出变量、类和函数
#include <Windows.h>
#include <strsafe.h>
#include <tchar.h>
#define DLL_EXPORT
#include "DllSample.h"
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
// 函数
int WINAPI funAdd(int a, int b)
{
return a + b;
}
int WINAPI funMul(int a, int b)
{
return a * b;
}
从一个DLL中导入变量、类和函数的模块可以称为可执行模块,导出变量、类和函数以供可执行模块使用的模块称为DLL模块,DLL模块也可以导入一些包含在其他DLL模块中的变量、类和函数。在创建DLL时,应该首先创建一个头文件包含想要导出的变量、类和函数的声明,DLL的源文件中应该包含这个头文件,即DLL的源文件中含导出变量、类和函数的定义。调用DLL中的函数的可执行模块也需要用到该头文件。
_declspec(dllexport)表示从DLL模块中导出变量、类和函数,_declspec(dllimport)表示在可执行模块中导入DLL模块的变量、类和函数。
DLL_EXPORT宏(也可以是其他名称)应该在DLL的源代码中定义,然后再包含头文件,例如∶
#include <Windows.h>
#include <strsafe.h>
#include <tchar.h>
#define DLL_EXPORT
#include "DllSample.h"
如果DLL_EXPORT宏已经定义,则定义以下宏∶
#ifdef DLL_EXPORT
#define DLL_VARABLE extern "C" __declspec(dllexport)
#define DLL_CLASS __declspec(dllexport)
#define DLL_API extern "C" __declspec(dllexport)
在要导出的变量、类和函数前面使用DLL_VARABLE DLL_CLASS或DLL_API宏,表示要从DLL模块中导出变量、类和函数。
在可执行模块的源代码中,则不定义DLL_EXPORT宏,而是定义以下宏表示在可执行模块中导入DLL模块的变量、类和函数∶
#else
#define DLL_VARABLE extern "C" __declspec(dllimport)
#define DLL_CLASS __declspec(dllimport)
#define DLL_API extern "C" __declspec(dllimport)
#endif
在编写C++代码时建议使用extern "C"声明,如果编写C代码则不应该使用extern “C”。
C++编译器会对C++类名、函数名和变量名进行改编得到一个修饰名。这里以C++对函数名的改编为例进行说明,修饰名由编译器在编译函数时生成,包括函数名称和调用约定、函数类型、函数参数和其他信息,通过修饰名可以帮助链接器在链接可执行文件时找到正确的函数。
删除DIISample.h头文件中DLL_API宏的extern "C"声明,重新编译生成DLL,然后使用Depends.exe打开DIISample.dll(见下图)。

可以发现,C++编译器使用的函数名称修饰方式比较复杂,函数名前面有一个"?“,然后是函数名,”@YA"后面的内容表示参数列表,参数列表的第1项表示函数返回值类型,其后依次为参数的数据类型,参数列表后面以"@Z"标识整个修饰名的结束。参数列表以如下代号表示∶X表示void,D表示char,E表示unsigned char,F表示short,H表示int,l表示unsigned int,N表示double等。
C++编译器采用修饰名的一个重要原因是函数重载,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。如果在DlISample.h和DlISample.cpp中再声明定义一个返回值和形式参数均为double的funMul函数∶
DLL_API DOUBLE funMul(DOUBLE a, DOUBLE b);
DOUBLE funMul(DOUBLE a, DOUBLE b)
{
return a * b;
}
重新编译生成DLL,可以看到上述函数的修饰名为?funMul@@YANNN@Z。
C++编译器采用了修饰名,因此采用C++修饰方式命名的DLL酗数无法被其他语言使用,在其他语言编写的源代码中调用funAdd funMul会出现找不到?funAdd@@YAHHH@Z?funMul@YAHHH@z等错误提示。在函数声明前面加上extern "C"关键字后,C++会对该函数强制使用标准c的函数名称修饰方式,不会对函数名、变量名进行改编,在DLL中用这种方式导出的函数可以被其他语言使用;反之,其他语言编写的DLL函数在C++的头文件中进行声明时,前面也必须添加extern "C"关键字,这样C++才会在lib文件中找到正确的函数修饰名。
添加extern "C"关键字后,生成的函数调用约定为_cdecl(C调用约定),函数参数按照从右到左的顺序压入栈,由函数调用方负责平衡栈。如果希望使用_stdcall(WINAPI CALLBACK等)函数调用约定,例如∶
// 导出函数
DLL_API int WINAPI funAdd(int a, int b);
DLL_API int WINAPI funMul(int a, int b);
当使用_stdcall来导出C函数时,编译器会对函数名进行改编,函数名前面有一个下划线"_",然后是函数名,函数名后面是一个@符号后跟作为参数传递给函数的字节数组成,如图6.4所示。

这种情况下,我们可以添加一个.def文件指示编译器导出funAdd和funMul函数,而不是_funAdd@8和_funMul@8函数。用鼠标右键单击VS左侧解决方案资源管理器中的源文件,然后选择添加→新建项命今,打开添加新项对话框,选择Visual C++→代码→模块定义文件(.def) ,文件名称可以设为DllSample.def,单击"添加"按钮,输入以下内容︰
EXPORTS
funAdd
funMul
DllSample.def文件中就是一个EXPORTS关键字,然后下面每一行指定一个要导出的函数名。然后生成DLL,这样即可导出未经改编的funAdd和funMul函数。
在可执行模块中使用DLL
我们编写一个Win32桌面应用程序项目Test,在源代码中使用DllSample.dll导出的变量、C++类和函数。Test.cpp源文件的内容如下︰
#include <Windows.h>
#include <strsafe.h>
#include <tchar.h>
#include "DllSample.h" // DllSample.h头文件
#pragma comment(lib, "DllSample.lib") // DllSample.lib导入库文件
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
TCHAR szBuf[256] = { 0 };
TCHAR szBuf2[512] = { 0 };
// 测试导出变量
wsprintf(szBuf, TEXT("nValue = %d\nps.x = %d, ps.y = %d\n"), nValue, ps.x, ps.y);
StringCchCopy(szBuf2, _countof(szBuf2), szBuf);
// 测试导出类
CStudent student((LPTSTR)TEXT("老王"), 40);
wsprintf(szBuf, TEXT("姓名:%s, 年龄:%d\n"), student.GetName(), student.GetAge());
StringCchCat(szBuf2, _countof(szBuf2), szBuf);
// 测试导出函数
wsprintf(szBuf, TEXT("funAdd(5, 6) = %d\nfunMul(5, 6) = %d"), funAdd(5, 6), funMul(5, 6));
StringCchCat(szBuf2, _countof(szBuf2), szBuf);
MessageBox(NULL, szBuf2, TEXT("提示"), MB_OK);
}
执行成功之后,效果如下所示:

在可执行模块源代码中不仅需要包含DIlSample.h,还需要链接.lib导入库文件,有以下两种方法。
- (使用VS配置链接信息。用鼠标右键单击项目名称打开属性页,然后选择配置属性→链接器→输入,在“附加依赖项"中输入所需要的.lib文件。如果.lib文件不在项目的当前目录,那么还需要在“配置属性→链接器→常规”的“附加库目录”中输入
该.lib文件所在的路径,建议把.lib文件复制到项目的当前目录。可以看到在“附加依赖项"中已经存在Kernel32.lib .User32.lib,Gdi32.lib等导入库文件,在创建VS项目时,最基本的导入库文件VS已经自动帮我们添加好了,因此在调用这些DLL中的函数时,只需要包含相关头文件,Windows.h头文件中包含了最基本、最常用的一些头文件。 - 使用#pragma链接命合,如下所示∶
#pragma comment(lib, "DllSample.lib") // DllSample.lib导入库文件
另外,DllSample.dll也需要复制到项目的当前目录。在发布软件时,Test.exe和DllSample.dll这两个文件需要同时发布,如果缺少DIlSample.dll,运行Test.exe时会提示下图所示的错误。

我们再看一下如果dll文件中不存在某个函数时的情况,删除DllSample.h头文件中最后一行funMul函数声明前面的DLL_API,这表示funMul函数是一个内部函数,不会导出。重新编译生成DLL文件,复制到Test项目中,运行Test.exe会有

在可执行模块(或其他DLL)能够调用一个DLL中的函数前,必须将该DLL的文件映射到调用进程的地址空间中,可以通过两种方法来达到这一目的∶隐式链接和显式链接。
隐式链接是最常见的链接类型,也是我们一直使用的包含头文件和导入库文件的方式,在编译链接生成可执行文件时,系统会把DLL文件名以及用到的函数写入可执行文件的导入表,这样一来当运行一个可执行文件时,PE加载器会解析可执行文件的导入表,把导入表中列出的每个DLL映射到进程的地址空间中,并根据函数名在每个DLL中查找导出函数。
显式链接是指在需要调用一个DLL文件中的导出酗数时,通过调用LoadLibrary(Ex)函数自行加载动态链接库到进程地址空间中,然后通过调用GetProcAddress函数获取导出函数在动态链接库中的地址,最后通过GetProcAddress函数返回的函数指针进行函数调用。在调用一些系统DLL提供的未公开函数时,通常需要显式链接。另外显式链接的好处是无论DLL文件是否存在,在加载DLL并使用其中的导出函数以前可执行文件都可以正常运行。
运行一个可执行文件时,PE加载器会为进程创建虚拟地址空间,并把可执行文件映射到进程的地址空间中,然后PE加载器
会解析可执行文件的导入表,对所需的DLL进行定位并将它们映射到进程的地址空间中。导入表中只包含DLL的名称,不包含DLL的路径,下面是定位DLL文件的顺序。
- 可执行文件目录。
- Windows系统目录。
- Windows目录。
- 进程的当前目录。
- PATH环境变量中列出的所有目录。
C标准定义了一系列常用的函数,称为C库函数,C标准仅仅定义了函数原型,并没有提供相关实现,这个任务留给了各个支持C语言标准的编译器,每个编译器通常会实现标准C的超集,称为C运行时库(C Run Time Libray,CRT库)。对VC++编译器来说,它提供的CRT库支持C标准定义的标准C函数,同时也有一些专内针对Windows系统特别设计的函数。与c语言类似,C++也定义了自己的标准,同时编译器提供相关支持库。
VC++完美支持C/C++标准,并按照C/C++标准定义的函数原型实现了C/C++运行时库。为方便有不同需求的客户使用,VC++分别实现了运行时库的静态库LIB版本和动态链接库DLL版本。在VS中,用鼠标右键单击项目名称,然后选择属性,打开属性页,配置属性→C/C++→代码生成→运行库,可以看到4个选项∶多线程(/MT) 、多线程调试(/MTd) 、多线程DLL (/MD)和多线程调试DLL (/MDd)。含义如下表所示。
| 模式 | 含义 |
|---|---|
多线程(/MT) | /MT多线程静态库发行版,编译器会静态链接相关C/C++链接库 |
多线程调试(/MTd) | /MTd多线程静态库调试版,编译器会静态链接相关C/C++链接库 |
多线程DLL (/MD) | /MD多线程动态库发行版,编译器会动态链接相关C/C++链接库 |
多线程调试DLL (/MDd) | /MDd多线程动态库调试版,编译器会动态链接相关C/C++链接库 |
对于MT /MTd,由于已经静态链接相关C/C++链接库,在编译链接时会将用到的C/C++运行库中的函数代码集成到程序中成为程序中的代码,程序体积增大,编译后的程序可以在其他机器上正常运行;但是对于MD /MDd,因为是动态链接相关C/C++链接库,在程序运行时会动态加载相关的DLL,程序体积会减小,但是在其他机器上运行时可能会提示缺少相关动态链接库的错误。
在平时做练习时,为了方便调试,通常应该编译为Debug调试版本,运行库选项可以选择多线程调试(/MTd)。在发布软件时,通常应该编译为Release发行版本,Release版本会对代码进行大量优化,运行库选项则可以选择多线程(/MT),如下图所示:

入口点函数DIIMain
一个DLL可以有一个入口点函数DIIMain(区分大小写),系统会在不同的时刻调用这个入口点函数,当系统装载、卸载动态链接库,以及进程中有线程被创建、退出时,系统会调用入口点函数。系统调用入口点函数是通知性质,通知DLL执行一些与进程或线程有关的初始化和清理工作,如果DLL不需要这些通知,可以不必在源代码中实现该入口点函数,例如要创建一个只包含资源的DLL,则不需要实现该函数。入口点函数的格式如下∶
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//dll 正在被映射到进程的地址空间中
break;
case DLL_THREAD_ATTACH:
// 进程中创建了一个新的线程
break;
case DLL_THREAD_DETACH:
// 进程中有一个线程正在退出
break;
case DLL_PROCESS_DETACH:
// dll 正在从进程的地址空间中卸载
break;
}
return TRUE;
}
hModule参数是DLL模块的句柄,也就是DLL加载到进程地址空间中的基地址,如果在动态链接库中定义了资源并且需要加载一个资源,那么在处理DLL_PROCESS_ATTACH时应该保存这个模块句柄到一个全局变量中,从DLL模块中加载资源时需要使用模块句柄。
ul_reason_for_call参数表示调用DLL入口点函数的原因码,可以是下表所示的值之一。
| 原因码 | 含义 |
|---|---|
DLL_PROCESS_ATTACH | 当系统第一次将一个DLL映射到进程的地址空间中时(隐式链接或显式链接),会调用DIlMain函数,并在ul_reason_for_call参数中传入DLL_PROCESS_ATTACH,如果后续进程中有一个线程再调用LoadLibrary(Ex)函数来加载一个已经被映射到进程地址空间中的DLL,则系统只是递增该DLL的引用计数,而不会再次使用DLL_PROCESS_ATTACH来调用DllMain函数。在处理DLL_PROCESS_ATTACH时,可以根据需要保存DLL的模块句柄,然后做一些初始化工作,处理完DLL_PROCESS_ATTACH通知以后应返回TRUE表示初始化成功,如果返回FALSE,则DLL加载会失败。如果任何一个DLL的DllMain函数在处理DLL_PROCESS_ATTACH时返回FALSE,即初始化失败,则系统会向用户显示一个消息框来告诉用户进程无法正常启动,并把所有的文件映像从地址空间中清除,然后终止整个进程。如果ul_reason_for_call参数是其他原因码(DLL_PROCESS_DETACH、DLL_THREAD_ATTACH或DLL_THREAD_DETACH ),系统会忽略DIIMain函数的返回值 |
DLL_PROCESS_DETACH | 当系统将一个DLL从进程的地址空间中撤销映射时,会调用DLL的DlIIMain函数,并在ul_reason_for_call参数中传入DLL_PROCESS_DETACH。DLL处理这个通知时,应该执行与之相关的清理工作。如果是系统中的某个线程调用了TerminateProcess函数终止进程,则系统不会使用DLL_PROCESS_DETACH来调用每个DLL的DllMain函数,这意味着在进程终止前,已映射到进程地址空间中的任何DLL将没有机会执行任何清理代码,这可能会导致数据丢失,因此除非万不得已,我们应该避免使用TerminateProcess函数。以DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH值调用DIIMain函数在动态链接库的生命周期中只可能出现一次 |
DLL_THREAD_ATTACH | 当进程中创建一个新线程时,系统会使用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数,这时DLL可以执行一些与线程相关的初始化工作,当所有DLL都完成了对该通知的处理后,系统才允许新线程开始执行它的线程函数。只有在创建新线程时DLL已经被映射到进程的地址空间中的情况下系统才会使用DLL_THREAD_ATTACH来调用DLL的DlIMain函数。当系统将一个新的DLL映射到进程的地址空间中时,如果进程中已经有多个线程在运行,那么系统不会允许任何已有的线程使用DLL_THREAD_ATTACH来调用DLL的DlIMain函数。另外,进程的主线程不会使用DLL_THREAD_ATTACH来调用DllMain函数,在创建进程时被映射到进程地址空间中的任何DLL会收到DLL_PROCESS_ATTACH通知,但不会收到DLL_THREAD_ATTACH通知 |
DLL_THREAD_DETACH | 当进程中有一个线程正在退出时,系统会使用DLL_THREAD_DETACH调用每个DLL的DIIMain函数,这时DLL可以执行一些相关的清理工作,只有当每个DLL都处理完DLL_THREAD_DETACH通知后,系统才会真正终止线程。如果系统中某个线程调用了TerminateThread函数终止线程,系统不会使用DLL_THREAD_DETACH来调用所有DLL的DllMain函数,这意味着在线程终止前,已映射到进程地址空间中的任何DLL都将没有机会执行任何清理代码,这可能会导致数据丢失,因此除非万不得已,我们应该避免使用TerminateThread函数。 如果是因为DLL加载失败、进程终止或调用FreeLibrary函数卸载DLL,则系统不会使用DLL_THREAD_DETACH来调用每个DLL的DllMain函数,仅向DLL发送DLL_PROCESS_DETACH通知 |
另外,当ul_reason_for_call参数为DLL_PROCESS_ATTACH时.
-
如果是隐式链接的DLL,则lpvReserved参数为非NULL;
-
如果是显式链接的DLL,则lpvReserved参数为NULL。
当ul_reason_for_call参数为DLL_PROCESS_DETACH时
- 如果是因为调用FreeLibrary函数或DLL加载失败而卸载DLL,则lpvReserved参数为NULL;
- 如果是因为进程终止导致正在卸载DLL,则lpvReserved参数为非NULL。
延迟加载DLL
延迟加载指的是通过隐式链接的DLL,可执行模块开始运行时并不加载延迟加载的DLL(也不会检查该DLL是否存在),只有当代码中调用延迟加载DLL中的函数时,系统扌会实际载入该DLL。延迟加载DLL有如下特性。
- (1)提高应用程序的加载速度。当运行一个程序时,PE加载器会解析可执行模块的导入表,并把导入表中列出的每个DLL加载到进程的地址空间中,加载每个DLL时还会调用入口点豳数DlIMain并做一些初始化工作。如果一个应用程序使用了特别多的DLL,则上述操作会严重拖慢程序的加载速度,而延迟加载则可以很好地解决这个问题,把加载DLL的工作延迟到代码中调用DLL中的函数时完成。
- (2) 提高应用程序的兼容性。当操作系统升级时,一些系统DLL中的函数会有所优化,系统DLL中的导出酗数会有所增加,新版本DLL可能提供了旧版本DLL中不包含的新豳数,如果我们的程序用到了一个只有新版本DLL中扌具有的函数,而程序却运行在旧版本操作系统中,则程序初始化时会提示无法定位函数并终止程序的运行。延迟加载可以很好地解决这个问题,当应用程序初始化时,可以调用GetVersionEx等函数来检查操作系统的版本,如果发现程序运行在旧版本操作系统中,则通过其他方法实现该函数的功能。
- (3)提高应用程序的可整合性。一个软件除了可执行文件本身,通常还需要一些DLL、配置文件、数据库等文件,丢失任何一个文件都可能导致软件不能正常运行,我们可以通过添加资
源的方式把软件所需的数据文件添加到可执行文件资源中,运行程序时释放这些文件到相关目录中,但是如果在程序初始化时发现相关目录中不存在程序运行所需的DLL,那么程序的初始化同样会失败。延迟加载可以很好地解决这个问题。采用延迟加载后,可执行模块在开始运行时并不加载延迟加载的DLL(也不会检查该DLL是否存在)
需要注意的是,如果一个DLL中导出了变量,则该DLL不支持延迟加载。Kernel32.dIl不能延迟加载,因为延迟加载需要用到该DLL中的LoadLibrary(Ex)和GetProcAddress等函数。不应该在入口点函数DIIMain中调用延迟加载DLL中的函数,否则可能会导致程序崩溃。
加密解密是当今信息社会的永恒话题,以下为GetMd5.dll导出的一个函数GetMd5:
BOOL GetMd5(LPCTSTR lpFileName,LPTSTR lpMd5)
该函数获取指定文件IpFileName的MD5,并将计算结果返回到lpMd5参数指定的缓冲区中,MD5 ·SHA是常用的加密算法,具体代码参见Chapter6\GetMd5项目。实际上获取一个文件或一段数据的MD5 SHA等,不一定必须使用微软提供的API,完全可以根据规定算法编写一个自定义函数。
接下来我们实现一个延迟加载GetMd5.dll的例子,GetMd5Test.exe程序运行效果如下图所示。

具体的实现如下所示:
#pragma once
// 声明导出的函数
#ifdef DLL_EXPORT
#define DLL_API extern "C" __declspec(dllexport)
#else
#define DLL_API extern "C" __declspec(dllimport)
#endif
// 导出函数
DLL_API BOOL GetMd5(LPCTSTR lpFileName, LPTSTR lpMd5);
// 定义DLL的导出函数
#include <Windows.h>
#define DLL_EXPORT
#include "GetMd5.h"
// 函数
BOOL GetMd5(LPCTSTR lpFileName, LPTSTR lpMd5)
{
HANDLE hFile, hFileMap;
HCRYPTPROV hProv, hHash;
TCHAR szContainer[] = TEXT("MyKeyContainer");
LPVOID lpMemory;
// 打开文件
hFile = CreateFile(lpFileName, GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
// 获取指定加密服务提供程序(CSP)中指定密钥容器的句柄
if (!CryptAcquireContext(&hProv, szContainer, NULL, PROV_RSA_FULL, 0))
{
if (!CryptAcquireContext(&hProv, szContainer, NULL, PROV_RSA_FULL, CRYPT_NEWKEYSET))
return FALSE;
}
// 创建加密服务提供程序(CSP)哈希对象的句柄,第2个参数指定为不同的值可以获取不同的哈希例如SHA
if (!CryptCreateHash(hProv, CALG_MD5, NULL, 0, &hHash))
return FALSE;
// 为了能够处理大型文件,所以使用内存映射文件。为hFile文件对象创建一个文件映射内核对象
hFileMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (!hFileMap)
return FALSE;
// 获取文件大小,内存分配粒度
__int64 qwFileSize;
DWORD dwFileSizeHigh;
SYSTEM_INFO si;
qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
qwFileSize += (((__int64)dwFileSizeHigh) << 32);
GetSystemInfo(&si);
// 把文件映射对象hFileMap不断映射到进程的虚拟地址空间中
__int64 qwFileOffset = 0; // 文件映射对象偏移量
DWORD dwBytesInBlock; // 本次映射大小
while (qwFileSize > 0)
{
dwBytesInBlock = si.dwAllocationGranularity;
if (qwFileSize < dwBytesInBlock)
dwBytesInBlock = (DWORD)qwFileSize;
lpMemory = MapViewOfFile(hFileMap, FILE_MAP_READ,
(DWORD)(qwFileOffset >> 32), (DWORD)(qwFileOffset & 0xFFFFFFFF), dwBytesInBlock);
if (!lpMemory)
return FALSE;
// 对已映射部分进行操作,将数据添加到哈希对象
if (!CryptHashData(hHash, (LPBYTE)lpMemory, dwBytesInBlock, 0))
return FALSE;
// 取消本次映射,进行下一轮映射
UnmapViewOfFile(lpMemory);
qwFileOffset += dwBytesInBlock;
qwFileSize -= dwBytesInBlock;
}
// 获取哈希值中的字节数
DWORD dwHashLen = 0;
if (!CryptGetHashParam(hHash, HP_HASHVAL, NULL, &dwHashLen, 0))
return FALSE;
// 获取哈希值
LPBYTE lpHash = new BYTE[dwHashLen];
if (!CryptGetHashParam(hHash, HP_HASHVAL, lpHash, &dwHashLen, 0))
return FALSE;
for (DWORD i = 0; i < dwHashLen; i++)
{
wsprintf(lpMd5 + i * 2, TEXT("%02X"), lpHash[i]);
}
delete[]lpHash;
// 关闭文件映射内核对象句柄
CloseHandle(hFileMap);
// 关闭文件句柄
CloseHandle(hFile);
// 释放哈希句柄
CryptDestroyHash(hHash);
// 释放CSP句柄
CryptReleaseContext(hProv, 0);
return TRUE;
}
/*
CryptAcquireContext函数用于获取指定加密服务提供程序(CSP)中指定密钥容器的句柄,返回的句柄用于调用所选CSP的CryptoAPI函数;
CryptCreateHash函数启动数据流的哈希处理, 该函数创建加密服务提供程序(CSP)哈希对象的句柄,该句柄可以用于后续的CryptHashData函数调用;
CryptHashData函数用于将数据添加到指定的哈希对象,可以多次调用该函数来计算长数据流或不连续数据流的哈希;
CryptGetHashParam函数用于获取具体的哈希值。
*/
与前面介绍的使用DLL编写可执行程序(例如Test程序)的方法完全相同,GetMd5Test项目同样需要GetMd5.h GetMd5.lib和GetMd5.dll,编译运行程序,一切工作正常,如果删除GetMd5.dll,GetMd5Test程序会初始化失败。
用鼠标右键单击项目名称,然后选择属性→配置属性→链接器→输入,在延迟加载的DLL中输入GetMd5.dll,单击“确定"按钮,重新编译运行程序,这时即使删除了GetMd5.dll,
GetMd5Test程序仍然可以正常运行。但是,因为不存在GetMd5.dll,如果按下“获取MD5"按钮,程序会调用GetMd5.dll中的导出函数GetMd5,导致触发一个异常并终止。
前面说过,延迟加载可以提高应用程序的可整合性,我们可以把GetMd5.dll这个文件添加到GetMd5Test项目资源中,例如资源类型为自定义类型“MyDll",资源ID为IDR_MYDLL,程序处理WM_INITDIALOG消息时可以释放GetMd5.dIl到可执行程序的当前目录中∶
#include <windows.h>
#include <Shlwapi.h>
#include <delayimp.h>
#include "resource.h"
#include "GetMd5.h"
#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "GetMd5.lib")
// 全局变量
HINSTANCE g_hInstance;
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
g_hInstance = hInstance;
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
}
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HRSRC hResBlock;
HANDLE hRes;
LPVOID lpDll;
DWORD dwDllSize;
HANDLE hFile;
TCHAR szFileName[MAX_PATH] = { 0 };
TCHAR szMd5[64] = { 0 };
HDROP hDrop;
switch (uMsg)
{
case WM_INITDIALOG:
// 关于ChangeWindowMessageFilterEx函数,请参见用户界面特权隔离
ChangeWindowMessageFilterEx(hwndDlg, WM_DROPFILES, MSGFLT_ALLOW, NULL);
ChangeWindowMessageFilterEx(hwndDlg, 0x49, MSGFLT_ALLOW, NULL);// 0x49 == WM_COPYGLOBALDATA
//ChangeWindowMessageFilter(WM_DROPFILES, MSGFLT_ADD);
//ChangeWindowMessageFilter(0x49, MSGFLT_ADD);
// 如果当前目录下不存在GetMd5.dll
if (!PathFileExists(TEXT("GetMd5.dll")))
{
hResBlock = FindResource(g_hInstance, MAKEINTRESOURCE(IDR_MYDLL), TEXT("MyDll"));
if (!hResBlock)
return FALSE;
hRes = LoadResource(g_hInstance, hResBlock);
if (!hRes)
return FALSE;
lpDll = LockResource(hRes);
dwDllSize = SizeofResource(g_hInstance, hResBlock);
hFile = CreateFile(TEXT("GetMd5.dll"), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
WriteFile(hFile, lpDll, dwDllSize, NULL, NULL);
CloseHandle(hFile);
}
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BTN_GETMD5:
// 获取指定文件的MD5
if (GetDlgItemText(hwndDlg, IDC_EDIT_FILENAME, szFileName, _countof(szFileName)))
{
if (GetMd5(szFileName, szMd5))
{
SetDlgItemText(hwndDlg, IDC_EDIT_MD5, szMd5);
// 卸载延迟加载的dll
__FUnloadDelayLoadedDLL2("GetMd5.dll");
}
}
break;
case IDCANCEL:
EndDialog(hwndDlg, 0);
break;
}
return TRUE;
case WM_DROPFILES:
hDrop = (HDROP)wParam;
DragQueryFile(hDrop, 0, szFileName, _countof(szFileName));
SetDlgItemText(hwndDlg, IDC_EDIT_FILENAME, szFileName);
DragFinish(hDrop);
return FALSE;
}
return FALSE;
}
用户可以在编辑框中输入一个文件名,或者拖动一个文件到对话框中,如果是拖动文件,则程序会响应WM_DROPFILES消息,把所拖动文件的完整路径显示到编辑框中。如果
CreateWindowEx函数的dwExStyle参数指定为
WS_EX_ACCEPTFILES,则表示所创建的窗口可以接受拖放文件,对对话框程序来说可以通过对话框的Accept Files属性来设置。
如果窗口具有WS_EX_ACCEPTFILES样式,当用户拖动一个或多个文件到窗口中时,窗口过程会收到WM_DROPFILES消息,该消息的wParam参数是一个描述所拖动文件信息的HDROP类型结构句柄(用户不需要关心结构句柄的具体含义,直接使用即可),该句柄可以用于DragQueryFile .DragQueryPoint和DragFinish酗数调用;lParam参数没有用到。程序处理完WM_DROPFILES消息后应返回0。
DragQueryFile函数用于查询所拖放文件的名称︰
UINT DragQueryFile(
_In_ HDROP hDrop, // HDROP句柄
_In_ UINT iFile,//要查询的文件的索引,设置为oxFFFFFFFF则函数返回所拖放文件的总数
_out_ LPTSTR lpszFile,//返回指定索引的文件名的缓冲区
UINT cch); //pszFile缓冲区的大小,以字符为单位
-
iFile参数指定要查询的文件的索引(从O开始),设置为OxFFFFFFFF,函数返回所拖放文件的总数。如果该参数的值介于O和所拖放文件的总数之间,函数会将指定索引的文件名复制到lpszFile参数指向的缓冲区。
-
lpszFile参数是返回指定索引的文件名的缓冲区,如果该参数设置为NULL,那么函数会返回指定索引的文件名所需缓冲区的大小,以字符为单位,不包括终止的空字符。
如果函数执行成功,则返回值是复制到IpszFile参数指向的缓冲区中的字符数,不包括终止的空字符。
HDROP hDrop;
TCHAR szFileName[MA×_PATH]= { 0 };
case WM_DROPFILES:
hDrop =(HDROP)wParam;
DragQueryFile(hDrop,0,szFileName,_countof(szFileName));
return FALSE;
如果需要获取所有拖动文件的文件名,可以按如下方式使用∶
HDROP hDrop;
UINT uDragCount;
TCHAR szFileName[MAX_PATH]= { 0 };
case WM_DROPFILES:
hDrop =(HDROP)wParam;
uDragCount = DragQueryFile(hDrop,OxFFFFFFFF,NULL,o);
for (UINTi= o; i < uDragCount; i++)
{
//处理本次查询到的文件名
DragQueryFile(hDrop, i, szFileName,_countof(szFileName));
}
DragQueryPoint函数用于查询拖动操作期间鼠标指针的位置,DragFinish函数用于释放系统为拖动操作所分配的内存∶
BOOL DragQueryPoint(
_In_ HDROP hDrop, //HDROP句柄
_out_ POINT* lppt);//POINT结构
VOID DragFinish(HDROP hDrop);//HDROP句柄
用鼠标右键单击项目名称,然后选择属性→配置属性→链接器→输入,在延迟加载的DLL中输入GetMd5.dll(可以指定多个DLL,以;分隔),该设置告知编译器不要在可执行模块的导入表中设置GetMd5.dlI及相关函数的信息,这样当进程初始化时,PE加载器不会隐式链接该DLL;
同时该设置使编译器在可执行模块中创建一个延迟加载导入表,延迟加载导入表记录了可执行模块要导入的DLL及相关函数的信息,与导入表不同的是,在可执行模块一开始运行时这些DLL并不会被PE加载器加载,只有当代码中调用这些DLL中的酗数时,系统扌会实际
载入DLL;编译器还会在可执行模块中嵌入一个delayLoadHelper2函数,对延迟加载DLL中函数的调用会跳转到该函数,该函数会解析延迟加载导入表,并调用LoadLibrary(Ex)和GetProcAddress等酗数完成对延迟加载DLL的加载以及函数地址的解析。
当代码中调用延迟加载DLL中的一个函数但是DLL不存在时,delayLoadHelper2函数会抛出一个软件异常;如果DLL已经存在,但是试图调用该DLL中一个不存在的函数,_delayLoadHelper2函数也会抛出一个软件异常,对于这两种情况,可以使用结构化异常处理(Structured Exception Handling,SEH)来捕捉异常并使应用程序继续运行;如果不捕捉该异常,则进程将会终止。
如果想在不需要延迟加载DLL时可以卸载该DLL,可以用鼠标右键单击项目名称,然后选择属性→配置属性→链接器→高级,在卸载延迟加载的DLL中选择(/DELAY:UNLOAD) ,即可在不需要延迟加载DLL的地方调用
FUnloadDelayLoadedDLL2函数卸载DLL,该函数会自动调用FreeLibrary函数卸载DLL并重置DLL中的函数地址。FUnloadDelayLoadedDLL2酗数在delayimp.h头文件中的定义如下∶
BOOL WINAPIFUnloadDelayLoadedDLL2(LPCSTR SzDll);
szDII参数是一个ANSI字符串指针,并且区分大小写。
的导入表中设置GetMd5.dlI及相关函数的信息,这样当进程初始化时,PE加载器不会隐式链接该DLL;
同时该设置使编译器在可执行模块中创建一个延迟加载导入表,延迟加载导入表记录了可执行模块要导入的DLL及相关函数的信息,与导入表不同的是,在可执行模块一开始运行时这些DLL并不会被PE加载器加载,只有当代码中调用这些DLL中的酗数时,系统扌会实际
载入DLL;编译器还会在可执行模块中嵌入一个delayLoadHelper2函数,对延迟加载DLL中函数的调用会跳转到该函数,该函数会解析延迟加载导入表,并调用LoadLibrary(Ex)和GetProcAddress等酗数完成对延迟加载DLL的加载以及函数地址的解析。
当代码中调用延迟加载DLL中的一个函数但是DLL不存在时,delayLoadHelper2函数会抛出一个软件异常;如果DLL已经存在,但是试图调用该DLL中一个不存在的函数,_delayLoadHelper2函数也会抛出一个软件异常,对于这两种情况,可以使用结构化异常处理(Structured Exception Handling,SEH)来捕捉异常并使应用程序继续运行;如果不捕捉该异常,则进程将会终止。
如果想在不需要延迟加载DLL时可以卸载该DLL,可以用鼠标右键单击项目名称,然后选择属性→配置属性→链接器→高级,在卸载延迟加载的DLL中选择(/DELAY:UNLOAD) ,即可在不需要延迟加载DLL的地方调用
FUnloadDelayLoadedDLL2函数卸载DLL,该函数会自动调用FreeLibrary函数卸载DLL并重置DLL中的函数地址。FUnloadDelayLoadedDLL2酗数在delayimp.h头文件中的定义如下∶
BOOL WINAPIFUnloadDelayLoadedDLL2(LPCSTR SzDll);
szDII参数是一个ANSI字符串指针,并且区分大小写。
更多推荐


所有评论(0)