【DLL】【学习笔记】(三)延迟加载

本文详细介绍了Windows程序设计中的DLL延迟加载机制,包括DllMain的作用、延迟加载的实现方式、异常处理和卸载流程。延迟加载允许程序在运行时按需加载DLL,减少了初始化时间并提供了更好的兼容性。同时,文中提到了延迟加载的异常处理和卸载方法,强调了不能直接使用FreeLibrary卸载延迟加载DLL的注意事项。
摘要由CSDN通过智能技术生成

DllMain

DllMain也是DLL中一个很重要的概念,在【DLL】系列中就不以文章形式记录了,关于DllMain的相关说明可以看这个🉐DLL入口函数🉐

延迟加载

延迟加载是程序运行时很重要的一个特性。借助这一特性可以很容易对 DLL 进行操作。延迟加载的 DLL 是个隐含链接的 DLL,它实际上要等到你的代码试图引用DLL中包含的一个符号时才进行加载。

首先说明一下,延迟加载的 DLL 在以下情况是非常有用的:

  • 如果你的应用程序使用若干个DLL,那么他的初始化时间就比较长,因为加载程序要将所有需要的DLL映射到进程的地址空间中。解决这个问题的方法之一就是在程序运行的时候分开加载各个DLL。延迟加载的DLL能够更容易地完成这样的加载。
  • 如果调用代码中的一个新函数,然后试图在老版本的系统上运行你的应用程序,而该系统中没有该函数,那么加载程序就会报告也一个错误,并且不允许应用程序运行。你需要一种方法让你的应用程序运行,然后,如果(在运行时)发现该应用程序在老的系统上运行,那么你将不调用遗漏的函数。

1. 实现

延迟操作不是对需要的DLL进行操作,而是对可执行模块进行操作,必须修改两个链接程序的开关,并且重新链接可执行模块。下面是需要添加的两个链接程序开关:

/Lib:DelayImp.lib
/DelayLoad:MyDll.dll

Lib开关告诉链接程序将一个特殊的函数 --delayLoadHelper 嵌入你的可执行模块。第二个开关将下列事情告诉链接程序:

  • 从可执行模块的输入节中删除MyDll.dll,这样,当进程被初始化时,操作系统的加载程序就不会显式加载DLL。
  • 将新的Delay Import (延迟输入)节(称为.didata)嵌入可执行模块,以指明哪些函数正在从MyDll.dll输入。
  • 通过转移到对 --delayLoadHelper函数的调用,转换到对延迟加载函数的调用。

当应用程序运行时,对延迟加载函数的调用实际上是对 --delayLoadHelper 函数的调用。该函数引用特殊的Delay Import节,并且知道调用LoadLibrary 之后再调用 GetProAddress。一旦获得延迟加载函数的地址,--delayLoadHelper就要安排好对该函数的调用,这样,将来的调用就会直接转向对延迟加载函数的调用。注意,当第一次调用同一个DLL中的其他函数时,必须对它们做好安排。另外,可以多次设定 /delayload 链接程序的开关,为想要延迟加载的乜咯DLL设定一次开关。

2. 补充

整个操作过程如上所示,但还没完全结束。

通常情况下,当操作系统的加载程序加载可执行模块时,它将设法加载必要的DLL。如果一个DLL无法加载,那么加载程序就会显示一条错误信息。如果是延迟加载的DLL,那么在初始化时就将不检查是否存在DLL。如果调用延迟加载函数时无法找到该DLL,--delayLoadHelper 函数就会引发一个软件异常条件。可以使用结构化异常处理(SEH)方法来跟踪该异常条件。如果不跟踪该异常条件,进程就会终止运行。

--delayLoadHelper 确实找到你的DLL,但是要调用的函数不在该DLL中时,将会出现另一个问题。

比如,如果加载程序找到了一个老的DLL版本,就会发生这种情况。在这种情况下,--delayLoadHelper也会引发一个软件异常条件,对这个软件异常条件的处理方法与上面相同。
Visaual C++开发小组定义了两个软件异常条件底阿妈,即 VcppException(ERROR_SEVERITY_ERROR、ERROR_MOD_NOT_FOUND)VcppException(ERROR_SEVERITY_ERROR、ERROR_PROC_NOT_FOUND)。这些代码分别用于指明DLL模块没有找到和函数没有找到。

DelatLoadDllExceptionFilter 用于查找上述☝这两个异常代码。如果两个代码都没有知道,过滤函数将返回EXCEPTION_CONTINUE_SEARCH ,这与任何过滤函数返回的值是一样的。但是如果这两个代码中的一个已经找到,那么 --delayLoadHelper 函数将提供一个指向包含某些辅助信息的 DelayLoadInfo 结构的指针。

DelayLoadInfo 结构定义为下面的形式:

#include <DelayImp.h>

typedef struct DelayLoadInfo {
	DWORD 			cb;		// size of Struct
	PCImgDelayDescr pidd;	// Raw Data (everything is there)
	FARPROC *		ppfn;	// Points to address of function to load
	LPCSTR			szDll;	// Name of dll
	DelayLoadProc	dlp;	// Name of Ordinal of Procedure
	HMODULE			hmodCur;// Acutal function that will be called
	DWORD			dwLastError; // Error received
} DelayLoadInfo, *PDelayLoadInfo;

这个数据结构是由 --delayLoadHelper 函数来分配和初始化的。在该函数按步骤动态加载DLL并且获得被调用函数的地址的过程中,它将填写该结构的各个成员。在SEH结构的内部,成员szDll指向你要加载的DLL的名字,想要查看的函数则在成员dlp中。由于可以按序号或名字来查看各个函数,因此dlp成员类似下面的样子:

typedef struct DelayLoadProc {
	BOOL fImportByName;
	union {
		LPCSTR szProcName;
		DWORD dwOrdinal;
	};
} DelayLoadProc;

如果DLL已经加载成功,但是它不包含必要的函数,也可以查看成员 hmodCur,以了解D L L被加载到的内存地址。也可以查看成员dwLastError,以了解是什么错误导致了异常条件的引发。不过对于异常过滤函数来说,这是不必要的,因为异常代码能够告诉你究竟发生了什么问题。成员 pfnCur 包含了需要的函数的地址。在过滤函数中它总是置为 NULL,因为 --delayLoadHelper 无法找到该函数的地址。

在其余的成员中,cb用于确定该版本,pidd指向嵌入模块中包含延迟加载的DLL和函数的节,ppfn是函数找到时,函数的地址应该放入的地址。最后两个成员供 --delayLoadHelper 函数内部使用。

3. 卸载延迟加载的DLL

若要卸载延迟加载的DLL,必须执行两项操作。首先,当创建可执行文件时,必须设定另一个链接程序开关(/delay:unload)。其次,必须修改源代码,并且在你想要卸载DLL时调用 --FUnloadDelayLoadedDLL 函数:

BOOL __FUnloadDelayLoadedDLL(PCSTR szDll);

/Delay:umload 链接程序开关告诉链接程序将另一个节放入文件中。 该节包含了你清除已经嗲用的函数时需要的信息,这样它们就可以再次调用 --delayLoadHelper 函数。当调用 --FunloadDelayLoaderDll 时,你将想要卸载的延迟加载的DLL的名字传递给它,该函数进入文件中的未卸载节,并清除DLL的所有函数地址,然后 --FUnloadDelayLoadedDLL 调用 FreeLibrary,以便卸载该DLL。

4. 延迟卸载的补充

  1. 千万不要自己调用 FreeLibrary 来卸载DLL,否则函数的地址将不会被清除,这样,当下次试图调用 DLL 中的函数时,就会导致访问违规。
  2. 当调用 --FUnloadDelayLoadedDll 时,传递的 DLL 名字不应该包含路径,名字中的字母必须与你将 DLL 名字传递给 /Delayload 链接程序开关时使用的字母大小写相同,否则,--FUnloadDelayLoadedDll 的调用将会失败。
  3. 如果永远不打算卸载延迟加载的DLL,那么请不要设定 /Delay:unload 链接程序开关,并且你的可执行文件的长度应该比较小,最后,如果你不从用 /Delay:unload 开关创建的模块中调用 --FUnloadDelayLoadedDll ,那么什么也不会发生,--FUnloadDelayLoadedDll 什么操作也不执行,它将返回FALSE
  4. 延迟加载的DLL具备的另一个特性是,按照默认设置,调用的函数可以与一些内存地址相链接,在这些内存地址上,系统认为函数将位于一个进程的地址中。由于创建可链接的延迟加载的DLL节会使你的可执行文件变得比较大,因此链接程序也支持一个 /Delay:nobind 开关。因为人们通常都喜欢进行链接,因此绝大多数应用程序不应该使用这个链接开关。
  5. <高级特性!!!> 当 --delayLoadHelper 函数执行时,它可以调用你提供的挂钩函数。这些函数将接收 --delayLoadHelper 函数的进度通知和错误通知。此外,这些函数可以重载DLL如何加载的方法以及如何获取函数的虚拟内存地址的方法。关于这一重要特性,我会在后续专门写一篇学习笔记用以整理和演示。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

欧恩意

如有帮助,感谢打赏!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值