《Windows核心编程》 第IV部分 动态链接库
第20章,DLL高级技术
问题一:动态加载DLL时,SetDllDirectory的作用
正常情况下, 一个可执行模块LoadLibrary单独一个DLL,没有任何问题;但如果该DLL还依赖于其他DLL模块,且和可执行模块不在同一级目录下,那么直接调用LoadLibrary会返回错误“找不到该DLL” ;
这种应用常见于集成使用其他公司提供的某功能模块,这个功能模块有一个主DLL和其他依赖DLL;如果我们把这些DLL都放在我们应用系统的根目录下,会显得很杂乱,且对以后的版本更新和维护带来很多问题,所以通常我们会将这些DLL存放在固定目录下;然后应用程序动态加载这些DLL模块。
这时候,我们就需要使用SetDllDirectory来通知应用程序,从指定目录动态加载DLL,这就是该函数的作用。
实际应用中,还遇到过一个可执行模块的两个线程,分别调用SetDllDirectory来加载不同目录下的DLL模块,很遗憾,这样做的结果是可执行模块在实际运行时崩溃。所以我们要避免在同一个进程中调用两次SetDllDirectory。
问题二:DllMain入口点函数,如何使用四个参数
在使用VS创建一个WIN32 DLL模块时,VS会自动生成带有DllMain的代码模块,如下:
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;
}
1. DLL_PROCESS_ATTACH:
只有当DLL文件影像第一次被映射到进程地址空间时,才会调用DllMain传入此参数;如果进程中其他模块再次调用该DLL,则不会再使用该参数调用DllMain。
2. DLL_PROCESS_DETACH
当系统将一个DLL从进程地址 空间撤销时,会调用DllMain传入此参数;DLL在处理此参数时,应执行相关的清理工作。
终止进程时,调用ExitProcess 会传入此参数调用DllMain;如果调用FreeLibrary ,DllMain在DLL_PROCESS_DETACH返回之前,线程不会返回。
3. DLL_THREAD_ATTACH
当进程创建一个线程时,系统会检查当前映射到该进程地址空间的所有DLL文件,并用该参数调用每一个DLL的DllMain函数。(每创建一个线程,就会向进程加载的所有DLL传入此参数)
比如一个进程中有两个线程:线程A和线程B,进程加载了一个名为SomeDLL.dll的模块。两个线程都准备调用CreateThread函数来创建另外两个线程:线程C和线程D。
则线程A创建线程C时,系统会用DLL_THREAD_ATTACH来调用SomeDLL.dll的DllMain函数;线程B创建线程D时,会再次用DLL_THREAD_ATTACH来调用DllMain(若线程C没有执行完DllMain中的代码,则DllMain收到第二次DLL_THREAD_ATTACH时会挂起等待,直到线程C执行完)
4. DLL_THREAD_DETACH
线程终止的首选方式是让线程函数返回,系统调用ExitThread来终止线程,但不是立即终止线程,而是会用DLL_THREAD_DETACH调用所有已加载的DLL的DllMain,通知DLL执行与线程相关的清理工作。如果是调用TerminateThread,那么系统不会用该参数调用DllMain
问题三:DllMain和 全局/静态变量的先后关系
DLL映射到进程地址空间时,先调用_DllMainCRTStartup函数,该函数会初始化C++运行库,并确保在收到DLL_PROCESS_ATTACH通知的时候,所有全局或静态C+对象都已构造完毕。也就是说,先初始化全局变量或调用全局对象的构造函数,再调用DllMain函数。