為什么在DllMain里不能調用LoadLibrary和FreeLibrary函數?
為什么在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的理由!
本文附帶的例子中說明了這種出錯的情況:
TestLoad主程序: int main(int argc, char* argv[]) { HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ; FreeLibrary( hDll ) ; return 0; } DllA: HANDLE g_hDllB = NULL ; char *g_buf = NULL ; BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: OutputDebugString( "==>DllA: Initialize begin!\n" ) ; g_hDllB = LoadLibrary( "DllB.dll" ) ; // g_buf在Load DllB.dll之后才初始化,顯然它沒有料到DllB在初始化時居然會用到g_buf!! g_buf = newchar[128] ; memset( g_buf, 0, 128 ) ; OutputDebugString( "==>DllA: Initialize end!\n" ) ; break ; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } returnTRUE; } DLLA_API void A1( char *str ) { OutputDebugString( "==>DllA: A1()\n" ) ; // 當DllB.dll在它的DllMain函數里調用A1()時,g_buf還沒有初始化,所以必然會出錯! strcat( g_buf, "==>DllA: " ) ; strcpy( g_buf, str ) ; OutputDebugString( g_buf ) ; } DllB: BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: OutputDebugString( "==>DllB: Initialize!\n" ) ; OutputDebugString( "==>DllB: DllB depend on DllA.\n" ) ; OutputDebugString( "==>DllB: I think DllA has been initialize.\n" ) ; // 當程序運行到這時,DllB認為它引用的DllA.dll已經加載并初始化了,所以它調用DllA的函數A1() A1( "DllB Invoke DllA::A1()\n" ) ; break ; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } returnTRUE; }
在調用DllA的函數A1()時,因為DllA里有些變量還沒初始化,所以會產生exception。以下是截取的部分LDR的輸出,"==>"開頭的是程序的輸出。
LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllA.dll LDR: KERNEL32.dll used by DllA.dll LDR: Snapping imports for DllA.dll from KERNEL32.dll LDR: Real INIT LIST H:\cm\vc6\TestLoad\bin\DllA.dll init routine 10001440 LDR: DllA.dll loaded. - Calling init routine at 10001440 ==>DllA: Initialize begin! LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllB.dll LDR: DllA.dll used by DllB.dll LDR: Snapping imports for DllB.dll from DllA.dll LDR: Refcount DllA.dll (2) LDR: Real INIT LIST H:\cm\vc6\TestLoad\bin\DllB.dll init routine 371260 LDR: DllB.dll loaded. - Calling init routine at 371260 ==>DllB: Initialize! ==>DllB: DllB depend on DllA. ==>DllB: I think DllA has been initialize. ==>DllA: A1() First-chance exception in Test.exe (DLLA.DLL): 0xC0000005: Access Violation. ==>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
下面的代碼和注釋說明了程序運行的細節:
TestUnload主程序: int main(int argc, char* argv[]) { HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ; // 在調用LoadLibrary之后 // LoadOrderList: A(1) --> C(2) --> B(1), 括號內的代表LoadCount // MemoryOrderList: A(1) --> C(2) --> B(1) // InitOrderList: C(2) --> A(1) --> B(1) FreeLibrary( hDll ) ; return 0; } DllA: BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: OutputDebugString( "==>DllA: Initialize!\n" ) ; // 這里用LoadLibrary是安全的 g_hDllB = LoadLibrary( "DllB.dll" ) ; if (NULL == g_hDllB) returnFALSE ; break ; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break ; case DLL_PROCESS_DETACH: // 運行到這里時,DllA現在只留在LoadOrderList中,已經從另兩個list中刪除 // LoadOrderList: A(0) --> C(1) --> B(1) // MemoryOrderList: C(1) --> B(1) // InitOrderList: C(1) --> B(1) OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ; FreeLibrary( g_hDllB ) ; // 運行到這里時,DllB和DllC都從MemoryOrderList和InitOrderList中刪除了 // LoadOrderList: A(0) --> C(0) --> B(0) // MemoryOrderList: // InitOrderList: OutputDebugString( "==>DllA: Uninitialize end!\n" ) ; break; } returnTRUE; }
如果主程序是靜態鏈接DllA又如何呢?LdrUnloadDll同樣能判斷這種情況:如果進程正在關閉那么LdrUnloadDll直接返回。我也構建了一個運行正確的例子TestUnload2來說明這種情況:
TestUnload2主程序: int main(int argc, char* argv[]) { // 此時DllA,DllB,DllC均已load // LoadOrderList: A(-1) --> C(-1) --> B(1), 括號內的代表LoadCount // MemoryOrderList: A(-1) --> C(-1) --> B(1) // InitOrderList: C(-1) --> A(-1) --> B(1) return 0; } DllA: BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: OutputDebugString( "==>DllA: Initialize!\n" ) ; // 這里用LoadLibrary是安全的 g_hDllB = LoadLibrary( "DllB.dll" ) ; if (NULL == g_hDllB) returnFALSE ; break ; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break ; case DLL_PROCESS_DETACH: // 運行到這里時,DllB已經被卸載,因為它是InitOrderList中最后一項 // 這里的卸載指的是調用了Init routine,發出了DLL_PROCESS_DETACH通知,而不是指unmap內存中的映像 OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ; // 這里不應該再調用DllB的函數!!! // 盡管DllB已經被卸載,但這里調用FreeLibrary并無危險 // 因為LdrUnloadDll判斷出進程正在Shutdown,所以它什么也沒做,直接返回 FreeLibrary( g_hDllB ) ; OutputDebugString( "==>DllA: Uninitialize end!\n" ) ; break; } returnTRUE; }
在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寫的測試程序在關閉時的堆棧:
PDFREA_1!CPDFReaderOCXApp::ExitInstance+0x1d PDFREA_1!DllMain+0x1bb PDFREA_1!_DllMainCRTStartup+0x80 ntdll!LdrpCallInitRoutine+0x14 ntdll!LdrUnloadDll+0x29a KERNEL32!FreeLibrary+0x3b ole32!CClassCache::CDllPathEntry::CFinishObject::Finish+0x2b ole32!CClassCache::CFinishComposite::Finish+0x19 ole32!CClassCache::FreeUnused+0x192 ole32!CoFreeUnusedLibraries+0x35 MFCO42D!AfxOleTerm+0x7b MFCO42D!AfxOleTermOrFreeLib+0x12 MFC42D!AfxWinTerm+0xa9 MFC42D!AfxWinMain+0x103 ReaderContainerMFC!WinMain+0x18 ReaderContainerMFC!WinMainCRTStartup+0x1b3 KERNEL32!BaseProcessStart+0x3d
可以看到OCX被FreeLibrary顯式地釋放,搶在Plugin被進程釋放之前,所以不會出錯。
下面是關閉IE時的堆棧:
CPDFReaderOCXApp::ExitInstance() line 44 DllMain(HINSTANCE__ * 0x04e10000, unsigned long 0, void * 0x00000001) line 139 _DllMainCRTStartup(void * 0x04e10000, unsigned long 0, void * 0x00000001) line 273 + 17 bytes NTDLL! LdrShutdownProcess + 238 bytes 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執行之前就已經執行了!