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