为什么在DllMain里不能调用LoadLibrary和FreeLibrary函数?

MSDN里对这个问题的答案十分的晦涩。不过现在我们已经有了足够的知识来解答这个问题。
考虑下面的情况:
       (a)DllB静态链接DllA 
       (b)DllB在DllMain里调用DllA的一个函数A1()
       (c)DllA在DllMain里调用LoadLibrary("DllB.dll")

分析:当执行到DllA中的DllMain的时侯,DllA.dll已经被映射到进程地址空间中,已经加入到了module list中。当它调用LoadLibrary("DllB.dll")时,首先会调用LdrpMapDll把DllB.dll映射到进程地址空间,并加入到InLoadOrderModuleList中。然后会调用LdrpLoadImportModule(...)加载它引用的DllA.dll,而 LdrpLoadImportModule会调用LdrpCheckForLoadedDll检查是否DllA.dll已经被加载。 LdrpCheckForLoadedDll会在哈希表LdrpHashTable中查找DllA.dll,而显然它能找到,所以加载DllA.dll这一步被成功调过。DllA在它的DllMain函数里能成功加载DllB,并要执行DllB的DllMain函数对其初始化。站在DllB的角度考虑,当程序运行到它的DllMain的时侯,它完全有理由相信它隐式链接的DllA.dll已经被加载并且成功地初始化。可事实上,此时DllA只是处在"正在初始化"的过程中!这种理想和现实的差距就是可能产生的Bug的根源,就是禁止在DllMain里调用LoadLibrary的理由!

本文附带的例子中说明了这种出错的情况:

  1. TestLoad主程序:
  2. int main(int argc, char* argv[])
  3. {
  4. HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ;
  5. FreeLibrary( hDll ) ;
  6. return 0;
  7. }
  8.  
  9. DllA
  10. HANDLE g_hDllB = NULL ;
  11. char *g_buf = NULL ;
  12.  
  13. BOOL APIENTRY DllMain( HANDLE hModule,
  14. DWORD ul_reason_for_call,
  15. LPVOID lpReserved
  16. )
  17. {
  18. switch (ul_reason_for_call)
  19. {
  20. case DLL_PROCESS_ATTACH:
  21. OutputDebugString( "==>DllA: Initialize begin!\n" ) ;
  22.  
  23. g_hDllB = LoadLibrary( "DllB.dll" ) ;
  24.  
  25. // g_buf在Load DllB.dll之后才初始化,显然它没有料到DllB在初始化时居然会用到g_buf!!
  26. g_buf = newchar[128] ;
  27. memset( g_buf, 0, 128 ) ;
  28.  
  29. OutputDebugString( "==>DllA: Initialize end!\n" ) ;
  30. break ;
  31.  
  32. case DLL_THREAD_ATTACH:
  33. case DLL_THREAD_DETACH:
  34. case DLL_PROCESS_DETACH:
  35. break;
  36. }
  37. returnTRUE;
  38. }
  39.  
  40. DLLA_API void A1( char *str )
  41. {
  42. OutputDebugString( "==>DllA: A1()\n" ) ;
  43.  
  44. // 当DllB.dll在它的DllMain函数里调用A1()时,g_buf还没有初始化,所以必然会出错!
  45. strcat( g_buf, "==>DllA: " ) ;
  46. strcpy( g_buf, str ) ;
  47.  
  48. OutputDebugString( g_buf ) ;
  49. }
  50.  
  51. DllB
  52. BOOL APIENTRY DllMain( HANDLE hModule,
  53. DWORD ul_reason_for_call,
  54. LPVOID lpReserved
  55. )
  56. {
  57. switch (ul_reason_for_call)
  58. {
  59. case DLL_PROCESS_ATTACH:
  60. OutputDebugString( "==>DllB: Initialize!\n" ) ;
  61. OutputDebugString( "==>DllB: DllB depend on DllA.\n" ) ;
  62. OutputDebugString( "==>DllB: I think DllA has been initialize.\n" ) ;
  63.  
  64. // 当程序运行到这时,DllB认为它引用的DllA.dll已经加载并初始化了,所以它调用DllA的函数A1()
  65. A1( "DllB Invoke DllA::A1()\n" ) ;
  66. break ;
  67.  
  68. case DLL_THREAD_ATTACH:
  69. case DLL_THREAD_DETACH:
  70. case DLL_PROCESS_DETACH:
  71. break;
  72. }
  73. returnTRUE;
  74. }

在调用DllA的函数A1()时,因为DllA里有些变量还没初始化,所以会产生exception。以下是截取的部分LDR的输出,"==>"开头的是程序的输出。

  1. LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllA.dll
  2. LDR: KERNEL32.dll used by DllA.dll
  3. LDR: Snapping imports for DllA.dll from KERNEL32.dll
  4. LDR: Real INIT LIST
  5. H:\cm\vc6\TestLoad\bin\DllA.dll init routine 10001440
  6. LDR: DllA.dll loaded. - Calling init routine at 10001440
  7. ==>DllA: Initialize begin!
  8. LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllB.dll
  9. LDR: DllA.dll used by DllB.dll
  10. LDR: Snapping imports for DllB.dll from DllA.dll
  11. LDR: Refcount DllA.dll (2)
  12. LDR: Real INIT LIST
  13. H:\cm\vc6\TestLoad\bin\DllB.dll init routine 371260
  14. LDR: DllB.dll loaded. - Calling init routine at 371260
  15. ==>DllB: Initialize!
  16. ==>DllB: DllB depend on DllA.
  17. ==>DllB: I think DllA has been initialize.
  18. ==>DllA: A1()
  19. First-chance exception in Test.exe (DLLA.DLL): 0xC0000005: Access Violation.
  20. ==>DllA: Initialize end!

在前面已经说过LdrUnloadDll里对DllMain里调用FreeLibrary的情况进行了特殊处理。此时仍然会对各个相关的Dll引用计数减 1,并移入到unload list中,但然后LdrUnloadDll就返回了!并没有执行Dll的termination code。我构建了一个运行正确的例子TestUnload,说明LdrUnloadDll是怎么处理的。

考虑下面的情况:
       (a)DllA依赖于DllC,DllB也依赖于DllC
       (b)DllA里调用LoadLibrary("DllB.dll"),并保证其成功
       (c)DllA在DllMain的termination code里执行FreeLibrary(),释放DllB
       (d)在主程序里动态的加载DllA

下面的代码和注释说明了程序运行的细节:

  1. TestUnload主程序:
  2. int main(int argc, char* argv[])
  3. {
  4. HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ;
  5. // 在调用LoadLibrary之后
  6. // LoadOrderList: A(1) --> C(2) --> B(1), 括号内的代表LoadCount
  7. // MemoryOrderList: A(1) --> C(2) --> B(1)
  8. // InitOrderList: C(2) --> A(1) --> B(1)
  9.  
  10. FreeLibrary( hDll ) ;
  11. return 0;
  12. }
  13.  
  14. DllA:
  15. BOOL APIENTRY DllMain( HANDLE hModule,
  16. DWORD ul_reason_for_call,
  17. LPVOID lpReserved
  18. )
  19. {
  20. switch (ul_reason_for_call)
  21. {
  22. case DLL_PROCESS_ATTACH:
  23. OutputDebugString( "==>DllA: Initialize!\n" ) ;
  24.  
  25. // 这里用LoadLibrary是安全的
  26. g_hDllB = LoadLibrary( "DllB.dll" ) ;
  27. if (NULL == g_hDllB)
  28. returnFALSE ;
  29. break ;
  30.  
  31. case DLL_THREAD_ATTACH:
  32. case DLL_THREAD_DETACH:
  33. break ;
  34.  
  35. case DLL_PROCESS_DETACH:
  36. // 运行到这里时,DllA现在只留在LoadOrderList中,已经从另两个list中删除
  37. // LoadOrderList: A(0) --> C(1) --> B(1)
  38. // MemoryOrderList: C(1) --> B(1)
  39. // InitOrderList: C(1) --> B(1)
  40.  
  41. OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ;
  42.  
  43. FreeLibrary( g_hDllB ) ;
  44.  
  45. // 运行到这里时,DllB和DllC都从MemoryOrderList和InitOrderList中删除了
  46. // LoadOrderList: A(0) --> C(0) --> B(0)
  47. // MemoryOrderList:
  48. // InitOrderList:
  49.  
  50. OutputDebugString( "==>DllA: Uninitialize end!\n" ) ;
  51. break;
  52. }
  53. returnTRUE;
  54. }

如果主程序是静态链接DllA又如何呢?LdrUnloadDll同样能判断这种情况:如果进程正在关闭那么LdrUnloadDll直接返回。我也构建了一个运行正确的例子TestUnload2来说明这种情况:

  1. TestUnload2主程序:
  2. int main(int argc, char* argv[])
  3. {
  4. // 此时DllA,DllB,DllC均已load
  5. // LoadOrderList: A(-1) --> C(-1) --> B(1), 括号内的代表LoadCount
  6. // MemoryOrderList: A(-1) --> C(-1) --> B(1)
  7. // InitOrderList: C(-1) --> A(-1) --> B(1)
  8.  
  9. return 0;
  10. }
  11.  
  12. DllA:
  13. BOOL APIENTRY DllMain( HANDLE hModule,
  14. DWORD ul_reason_for_call,
  15. LPVOID lpReserved
  16. )
  17. {
  18. switch (ul_reason_for_call)
  19. {
  20. case DLL_PROCESS_ATTACH:
  21. OutputDebugString( "==>DllA: Initialize!\n" ) ;
  22.  
  23. // 这里用LoadLibrary是安全的
  24. g_hDllB = LoadLibrary( "DllB.dll" ) ;
  25. if (NULL == g_hDllB)
  26. returnFALSE ;
  27.  
  28. break ;
  29.  
  30. case DLL_THREAD_ATTACH:
  31. case DLL_THREAD_DETACH:
  32. break ;
  33.  
  34. case DLL_PROCESS_DETACH:
  35. // 运行到这里时,DllB已经被卸载,因为它是InitOrderList中最后一项
  36. // 这里的卸载指的是调用了Init routine,发出了DLL_PROCESS_DETACH通知,而不是指unmap内存中的映像
  37. OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ;
  38.  
  39. // 这里不应该再调用DllB的函数!!!
  40.  
  41. // 尽管DllB已经被卸载,但这里调用FreeLibrary并无危险
  42. // 因为LdrUnloadDll判断出进程正在Shutdown,所以它什么也没做,直接返回
  43. FreeLibrary( g_hDllB ) ;
  44.  
  45. OutputDebugString( "==>DllA: Uninitialize end!\n" ) ;
  46.  
  47. break;
  48. }
  49. returnTRUE;
  50. }

在Jeffrey Richter的"Windows核心编程"和Matt Pietrek在1999年MSJ上的"Under theHood"里都说到,User32.dll在它的initializecode里会用LoadLibrary加载 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Windows\AppInit_DLLs" 下的dll,在它的terminate code里会用FreeLibrary卸载它们。跟踪它的FreeLibrary函数,发现同上面的例子一样,LdrUnloadDll发现进程正在 Shutdown中,就直接返回了,没有任何危险。(User32.dll是静态链接的函数,只可能在进程关闭时被卸载。另外,在我调试的时侯,发现即使 AppInit_DLLs下为空,User32.dll仍然会加载imm32.dll)。

总而言之,FreeLibrary本身是相当安全的,但MSDN里对它的警告也并非是胡说八道。在DllMain里使用FreeLibrary仍然是具有危险性的,与LoadLibrary一样,它们具有相同的Bug哲学,即理想和现实的差距!
TestUnload2虽然运行正确,但是它具有潜在的危险性
对DllA而言,释放DllB是它的责任,是它在收到DLL_PROCESS_DETACH通知之后用FreeLibrary卸载的,可事实上如果DllA被主程序静态链接,或者DllA是动态链接但没有用FreeLibrary显式卸载它的话,那么在进程结束时,在DllA卸载DllB之前,DllB就已经被主程序卸载掉了!这种认识上的错误就是养育Bug的沃土。如果DllA没有认识到这种可能性,而在FreeLibrary之前调用DllB的函数,就极可能出错!!!

为了加深理解,我用文章开头提到的那个Bug来说明这种情况,那可是血的教训。问题描述如下:
我用MFC写了一个OCX,OCX里动态加载了一些Plugin Dlls,在OCX的ExitInstance(相当于DllMain里处理DLL_PROCESS_DETACH通知)里调用这些Plugin的 Uninitialize code,然后用FreeLibrary将其释放。在我用MFC编写的一个Doc/View架构的测试程序里运行良好,但不久客户就报告了一个Bug:用 VB写了一个OCX2来包装我的OCX,在一个网页里使用OCX2,然后在IE里打开这个网页,在关掉IE时会当掉!发生在特定条件下的奇怪的错误!当时我可是费了不少功夫来解这个Bug,现在一切都那么清晰了。

下面是我用MFC写的测试程序在关闭时的堆栈:

  1. PDFREA_1!CPDFReaderOCXApp::ExitInstance+0x1d
  2. PDFREA_1!DllMain+0x1bb
  3. PDFREA_1!_DllMainCRTStartup+0x80
  4. ntdll!LdrpCallInitRoutine+0x14
  5. ntdll!LdrUnloadDll+0x29a
  6. KERNEL32!FreeLibrary+0x3b
  7. ole32!CClassCache::CDllPathEntry::CFinishObject::Finish+0x2b
  8. ole32!CClassCache::CFinishComposite::Finish+0x19
  9. ole32!CClassCache::FreeUnused+0x192
  10. ole32!CoFreeUnusedLibraries+0x35
  11. MFCO42D!AfxOleTerm+0x7b
  12. MFCO42D!AfxOleTermOrFreeLib+0x12
  13. MFC42D!AfxWinTerm+0xa9
  14. MFC42D!AfxWinMain+0x103
  15. ReaderContainerMFC!WinMain+0x18
  16. ReaderContainerMFC!WinMainCRTStartup+0x1b3
  17. KERNEL32!BaseProcessStart+0x3d

可以看到OCX被FreeLibrary显式地释放,抢在Plugin被进程释放之前,所以不会出错。

下面是关闭IE时的堆栈:

  1. CPDFReaderOCXApp::ExitInstance() line 44
  2. DllMain(HINSTANCE__ * 0x04e10000, unsigned long 0, void * 0x00000001) line 139
  3. _DllMainCRTStartup(void * 0x04e10000, unsigned long 0, void * 0x00000001) line 273 + 17 bytes
  4. NTDLL! LdrShutdownProcess + 238 bytes
  5. KERNEL32! ExitProcess + 85 bytes

可以看到OCX是在LdrShutdownProcess里被释放的,而此时Plugin已经被释放掉了,因为在 InInitializationOrderModuleList表里Plugin Dlls在OCX之后,所以它们被先释放!这种情况要是还不出错真是奇迹了。

总结:虽然MS警告不要在DllMain里不能调用LoadLibrary和FreeLibrary函数,可实际上它还是做了很多的工作来处理这种情况。只不过因为他不想或者懒得说清楚到底哪些情况不能这么用,才干脆一棒子打死统统不许。在你自己的程序里不是绝对不能这么用,只是你必须清楚地知道每件事是怎么发生的,以及潜在的危险。

  • DllMain函数中不能Load(Unload)别的dll;
  • DllMain函数中不能调用其它dll暴露的函数!(System32.dll、User32.dll、Advapi32.dll除外)
  • Dll中声明的全局(或静态)变量的构造和析构函数中同样不能执行以上的操作!因为这些函数甚至在DllMain执行之前就已经执行了!
请各位务必牢记这些原则,不要再犯这样的错误!因为这种错误追查起来非常非常麻烦,因为它的表现受环境影响,缺乏一致性。

为什么在DllMain里不能调用LoadLibrary和FreeLibrary函数?的更多相关文章

  1. 动态载入DLL所需要的三个函数详解(LoadLibrary,GetProcAddress,FreeLibrary)

    动态载入 DLL 动态载入方式是指在编译之前并不知道将会调用哪些 DLL 函数, 完全是在运行过程中根据需要决定应调用哪些函数. 方法是:用 LoadLibrary 函数加载动态链接库到内存,用 Ge ...

  2. 【转载】动态载入DLL所需要的三个函数详解(LoadLibrary,GetProcAddress,FreeLibrary)

    原文地址:https://www.cnblogs.com/westsoft/p/5936092.html 动态载入 DLL 动态载入方式是指在编译之前并不知道将会调用哪些 DLL 函数, 完全是在运行 ...

  3. win系统动态载入DLL所需要的三个函数详解(LoadLibrary,GetProcAddress,FreeLibrary)

    动态载入 DLL 动态载入方式是指在编译之前并不知道将会调用哪些 DLL 函数, 完全是在运行过程中根据需要决定应调用哪些函数. 方法是:用 LoadLibrary 函数加载动态链接库到内存,用 Ge ...

  4. 在c或c+程序里打印调用栈。转

    在C/C++程序里打印调用栈信息 我们知道,GDB的backtrace命令可以查看堆栈信息.但很多时候,GDB根本用不上.比如说,在线上环境中可能没有GDB,即使有,也不太可能让我们直接在上面调试.如 ...

  5. [转载]C#控制台应用程序里调用自己写的函数的方法

    (2011-08-15 15:52:13) 转载▼ 标签: 转载 分类: 技术类 原文地址:C#控制台应用程序里调用自己写的函数的方法作者:萧儿 最近写程序,遇到了一个很白痴的问题,记录下来,免得下次 ...

  6. c语言里如何调用汇编里的变量?

    c语言里如何调用汇编里的变量? 汇编语言:是声明全局变量 .globl _end_ofs _end_ofs: .word _end - _start c语言:声明这个变量,然后再调用这个变量 void ...

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

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

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

    应用程序使用DLL可以采用两种方式: 一种是隐式链接,另一种是显式链接.在使用DLL之前首先要知道DLL中函数的结构信息. Visual C++6.0在VC\bin目录下提供了一个名为Dumpbin. ...

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

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

随机推荐

  1. ArcGIS API for Silverlight中专题地图的实现浅析

    原文http://www.gisall.com/html/32/7232-2418.html 专题地图是突出表现特定主题或者属性的地图.常见专题地图类型有唯一值渲染,分类渲染,柱状图,饼状图,点密度图 ...

  2. Linux 搭建SVN 服务器

    一. SVN 简介 Subversion(SVN) 是一个开源的版本控制系統, 也就是说 Subversion 管理着随时间改变的数据. 这些数据放置在一个中央资料档案库 (repository) 中 ...

  3. poj 2299 Ultra-QuickSort(归并排序或是bit 树+离散化皆可)

    题意:给一个数组,计算需要的冒泡排序的次数,元素个数很大,不能用n^2的冒泡排序计算. 解析:这题实际上就是求逆序对的个数,可以用归并排序的方法,我这里用另一种方法写,bit树+离散化.由于元素的值可 ...

  4. VS2015 MVC5项目部署

    刚看到一个年初的一个帖子说VS2015新建的MVC5项目部署后报错,自己捣鼓了一下,发现是Roslyn编译器的错误,简单处理后运行成功,分享如下: 新建一个MVC5的项目,保持不要动,执行以下几个步骤 ...

  5. Expected authority at index 7: hdfs://

    hadoop版本:1.0.4 今天在跑TestForest的时候,居然出现了这个问题: Exception in thread "main" java.lang.IllegalAr ...

  6. rem布局下使用背景图片和sprite图

    现在移动端页面用rem布局已经是一大流派了,成熟的框架如淘宝的flexiable.js,以及更轻量级的hotcss.用rem作单位使得元素能够自适应后,还有一块需要关注的,那就是背景图片.本文就来聊聊 ...

  7. 基于内容的自适应变长编码[CAVLC]

    基于内容自适应的变长编码方式用于编码zigzag顺序扫描的4x4和2x2残差变换系数块. 1.编码系数个数和零序列(coeff_token): coeff_token = <TotalCoeff ...

  8. ASP.NET Signalr 2.0 实现一个简单的聊天室

    学习了一下SignalR 2.0,http://www.asp.net/signalr 文章写的很详细,如果头疼英文,还可以机翻成中文,虽然不是很准确,大概还是容易看明白. 理论要结合实践,自己动手做 ...

  9. MySQL Update 使用

    备忘: USE `xxx`; ; UPDATE `TB_MB_1` T SET T.`MedicalCount` = ( SELECT S.Total-- ,S.`HospitalID` FROM( ...

  10. 《JavaScript 闯关记》之数组

    数组是值的有序集合.每个值叫做一个元素,而每个元素在数组中有一个位置,以数字表示,称为索引. JavaScript 数组是无类型的,数组元素可以是任意类型,并且同一个数组中的不同元素也可能有不同的类型 ...