DllMain中不当操作导致死锁问题的分析--进程对DllMain函数的调用规律的研究和分析
? ? ? ? 不知道大家是否思考過一個過程:系統試圖運行我們寫的程序,它是怎么知道程序起始位置的?很多同學想到,我們在編寫程序時有個函數,類似Main這樣的名字。是的!這就是系統給我們提供的控制程序最開始的地方(注意這兒是提供給我們的,而實際有比這個還要靠前的main)。于是看到DllMain就可以想到它是干嘛的了:Dll的入口點函數。那何時調用這個函數的呢?以及各種調用場景都傳給了它什么參數呢?(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 進程對DLL的載入卸載,以及新線程的創建和退出都會導致對DllMain的調用。于是,我們設計了如下流程
? ? ? ? 為了盡可能排除一些因素對我們實驗的影響,所有線程函數公用一個簡單的例程函數
static DWORD WINAPI ThreadRoutine(LPVOID lpParam) {DWORD dwTID = GetCurrentThreadId();PrintLog("Thread%s %u\n", (LPSTR)lpParam, dwTID );Sleep(15000);PrintLog("\nThread%s Will Exit\n", (LPSTR)lpParam );return 0;
}
? ? ? ? DllMain函數也是非常簡單,兩個DLL的DllMain函數99.99%是相同的,只是在最后輸出所在DLL時列出了各自的DLL名字,以Dll1為例
BOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{string strReason;DWORD TID = GetCurrentThreadId();switch (ul_reason_for_call) {case DLL_PROCESS_ATTACH:{strReason = "DLL_PROCESS_ATTACH";}break;case DLL_PROCESS_DETACH:{strReason = "DLL_PROCESS_DETACH";}break;case DLL_THREAD_ATTACH:{strReason = "DLL_THREAD_ATTACH";}break;case DLL_THREAD_DETACH:{strReason = "DLL_THREAD_DETACH";}break;default:{strReason = "default";}break;}PrintLog("Dll1 TID:%u %s\n", TID, strReason.c_str() );return TRUE;
}
? ? ? ? 現在我們說下我設計這個流程的考慮:
? ? ? ? ?0 1 ?這個過程是為了查看Dll加載后,DllMain被調用是否受之前創建的線程影響。如果受到影響,我們應該能看到Dll1中輸出的信息中包含有線程A TID的記錄。反之則沒有記錄。?
? ? ? ? 2 這個過程是為了驗證創建新線程,對之前加載的Dll的DllMain調用情況。如果Dll1的DllMain輸出了線程B TID記錄,那么說明新線程創建會讓之前加載Dll的DllMain。反之說明創建新線程不會調用之前加載DLL的DllMain。
? ? ? ? 3 是為了再次驗證0,1這個過程得出的結論。
? ? ? ? 4 是為了再次驗證2這個過程得出的結論。
? ? ? ? 5 創建的線程是為了之后驗證線程正常退出和強制關閉之間的影響。
? ? ? ? 61,62 是為了驗證FreeLibrary是否會對之前對此DLL調用DllMain的線程存在影響。也就是想查看之前在創建線程時對Dll調用DllMain的線程,是否會發現要FreeLibrary了,從而對該Dll再調用DllMain做某些處理(比如清理)。該過程導致DllMain中輸出的信息包括那些線程TID的記錄,則說明存在影響(其他線程調用DllMain),否則說明不存在影響(其他線程不調用DllMain)。
? ? ? ? 6 驗證通過強制關閉線程對DllMain調用的影響。
? ? ? ? 7 8 9 驗證對不同DLL的DllMain調用情況可能存在不同的線程,在退出時,是否會調用DllMain,以及它們對DllMain的調用規律。
? ? ? ? 10 101 102 103 104等是通過不同方式驗證進程退出對DllMain是否存在調用,以及調用的規律。
? ? ? ? 我們先在主線程中用 1 2 3 4 5 6 7 8 9 10 這個流程,其結果是
| ? | MainTid:1056 | 主線程ID是1056 |
| 0 | CreatThread A ThreadA 3156 | A線程ID是3156 |
| 1 | LoadLibraryA Dll1 Dll1 TID:1056 DLL_PROCESS_ATTACH | Dll1加載了,它是主線程(1056)加載的。調用原因是DLL_PROCESS_ATTACH。而它的加載,并不會導致之前創建的A線程對其調用DllMain。 |
| 2 | CreatThread B Dll1 TID:4784 DLL_THREAD_ATTACH ThreadB 4784 | B線程(4784)在執行到線程函數之前,會去調用之前加載了但還沒有卸載的Dll1的DllMain函數。調用原因是DLL_THREAD_ATTACH,而不是之前的DLL_PROCESS_ATTACH。 |
| 3 | LoadLibraryA Dll2 Dll2 TID:1056 DLL_PROCESS_ATTACH | Dll2加載了,調用其DllMain是主線程。調用原因是DLL_PROCESS_ATTACH。加載后,并不會導致線程A、B去調用其DllMain。 |
| 4 | CreatThread C Dll1 TID:4052 DLL_THREAD_ATTACH Dll2 TID:4052 DLL_THREAD_ATTACH ThreadC 4052 | C線程(4052)在執行其線程函數之前,會去調用之前在主線程中加載了但還沒有卸載的DLL的DllMain函數,調用原因是DLL_THREAD_ATTACH。 |
| 5 | CreatThread D Dll1 TID:3440 DLL_THREAD_ATTACH Dll2 TID:3440 DLL_THREAD_ATTACH ThreadD 3440 | 同上。 |
| 6 | TerminateThread D | 強制關閉線程,不會導致任何DllMain的調用。 |
| 7 | ThreadA Will Exit Dll2 TID:3156 DLL_THREAD_DETACH Dll1 TID:3156 DLL_THREAD_DETACH | 線程A退出之前,會調用之前加載了但還沒有卸載的所有DLL的DllMain。注意,此處調用是線程A(3156),而不是主線程(1056)。調用原因是DLL_THREAD_DETACH。 |
| 8 | ThreadB Will Exit Dll2 TID:4784 DLL_THREAD_DETACH Dll1 TID:4784 DLL_THREAD_DETACH | 同上。 |
| 9 | ThreadC Will Exit Dll2 TID:4052 DLL_THREAD_DETACH Dll1 TID:4052 DLL_THREAD_DETACH | 同上。 |
| 10 | Proceess Exit Dll2 TID:1056 DLL_PROCESS_DETACH Dll1 TID:1056 DLL_PROCESS_DETACH | 主線程退出前,會調用所有加載了但還沒有卸載的DLL的DllMain。調用原因是DLL_PROCESS_DETACH。 |
? ? ? ? 為了排除主線程對我們環境的影響我們看下在子線程中執行以上流程的結果(之后我們對流程的修改,都將建立在子線程執行流程的基礎之上)
| ? | MainTid:5536 | 執行的線程ID是5536 |
| 0 | CreatThread A ThreadA 5684 | A線程ID是5684 |
| 1 | LoadLibraryA Dll1 Dll1 TID:5536 DLL_PROCESS_ATTACH | Dll1加載了,它是執行線程(5536)加載的。調用原因是DLL_PROCESS_ATTACH。而它的加載,并不會導致之前創建的A線程對其調用DllMain。 |
| 2 | CreatThread B Dll1 TID:4716 DLL_THREAD_ATTACH ThreadB 4716 | B線程(4716)在執行到線程函數之前,會去調用之前加載了但還沒有卸載的Dll1的DllMain函數。調用原因是DLL_THREAD_ATTACH,而不是之前的DLL_PROCESS_ATTACH。 |
| 3 | LoadLibraryA Dll2 Dll2 TID:5536 DLL_PROCESS_ATTACH | Dll2加載了,調用其DllMain是執行線程(5536)。調用原因是DLL_PROCESS_ATTACH。加載后,并不會導致線程A、B去調用其DllMain。 |
| 4 | CreatThread C Dll1 TID:2620 DLL_THREAD_ATTACH Dll2 TID:2620 DLL_THREAD_ATTACH ThreadC 2620 | C線程(2620)在執行其線程函數之前,會去調用之前在執行線程中加載了但還沒有卸載的DLL的DllMain函數,調用原因是DLL_THREAD_ATTACH。 |
| 5 | CreatThread D Dll1 TID:1016 DLL_THREAD_ATTACH Dll2 TID:1016 DLL_THREAD_ATTACH ThreadD 1016 | 同上。 |
| 6 | TerminateThread D | 強制關閉線程,不會導致任何DllMain的調用。 |
| 7 | ThreadA Will Exit Dll2 TID:5684 DLL_THREAD_DETACH Dll1 TID:5684 DLL_THREAD_DETACH | 線程A退出之前,會調用之前加載了但還沒有卸載的所有DLL的DllMain。注意,此處調用是線程A(5684),而不是執行線程(5536)。調用原因是DLL_THREAD_DETACH。 |
| 8 | ThreadB Will Exit Dll2 TID:4716 DLL_THREAD_DETACH Dll1 TID:4716 DLL_THREAD_DETACH | 同上。 |
| 9 | ThreadC Will Exit Dll2 TID:2620 DLL_THREAD_DETACH Dll1 TID:2620 DLL_THREAD_DETACH | 同上。 |
| 10 | Dll2 TID:5536 DLL_THREAD_DETACH Dll1 TID:5536 DLL_THREAD_DETACH Proceess Exit Dll2 TID:3904 DLL_PROCESS_DETACH Dll1 TID:3904 DLL_PROCESS_DETACH | 執行線程(5536)在退出時調用了它加載了但還沒有卸載的兩個DLL的DllMain,調用原因是DLL_THREAD_DETACH。 主線程退出前,會調用所有之前加載了但還沒有卸載的DLL的DllMain。調用原因是DLL_PROCESS_DETACH。 |
? ? ? ? 看了如此一串后,我想很多人都會有點暈,現在我總結一下:
? ? ? ? 一 Dll的加載不會導致之前創建的線程調用其DllMain函數。
? ? ? ? 二 線程創建后會調用已經加載了的DLL的DllMain,且調用原因是DLL_THREAD_ATTACH。(DisableThreadLibraryCalls會導致該過程不被調用,之后會介紹)
? ? ? ? 三 TerminateThread方式終止線程是不會讓該線程去調用該進程中加載的Dll的DllMain。
? ? ? ? 四 線程正常退出時,會調用進程中已經加載過的的DLL的DllMain,且調用原因是DLL_THREAD_DETACH。(不準確,之后糾正)
? ? ? ? 五 進程正常退出時,會調用該進程中已經加載過的的DLL的DllMain,且調用原因是DLL_PROCESS_DETACH。(不準確,之后糾正)
? ? ? ? 六 加載DLL進入進程空間時(和哪個線程LoadLibrary無關),加載它的線程會調用DllMain,且調用原因是DLL_PROCESS_ATTACH。
? ? ? ? 我們將過程6替換為過程61,并在子線程中執行,結果大部分相似,我把不一樣的地方列出來(執行線程TID是4752)
| 61 | Dll1 TID:4752 DLL_PROCESS_DETACH | 執行線程(4752)中卸載了Dll1,則執行線程(4752)調用該DLL的DllMain,且原因是DLL_PROCESS_DETACH。 |
| 6 | TerminateThread D | ? |
| 7 | ThreadA Will Exit Dll2 TID:3688 DLL_THREAD_DETACH | 線程A不會對已經卸載了的Dll1調用其DllMain。 |
| 8 | ThreadB Will Exit Dll2 TID:1872 DLL_THREAD_DETACH | 同上。 |
| 9 | ThreadC Will Exit Dll2 TID:5600 DLL_THREAD_DETACH | 同上。 |
| 10 | Dll2 TID:4752 DLL_THREAD_DETACH Proceess Exit Dll2 TID:2364 DLL_PROCESS_DETACH | 同上。 進程退出時,對尚未卸載的DLL調用其DllMain,且原因是DLL_PROCESS_DETACH。 |
? ? ? ? 基于以上結果,我們將以上四五兩點結論再嚴謹點
? ? ? ? 四 線程正常退出時,會調用進程中還沒卸載的DLL的DllMain,且調用原因是DLL_THREAD_DETACH。
? ? ? ? 五 進程正常退出時,會調用(不一定是主線程)該進程中還沒卸載的DLL的DllMain,且調用原因是DLL_PROCESS_DETACH。
? ? ? ? 并得出以下結論
? ? ? ? 七 DLL從進程空間中卸載出去前,會被卸載其的線程調用其DllMain,且調用原因是DLL_PROCESS_DETACH。
? ? ? ? 如果仔細看過我試驗結果的同學,應該看到一個現象:線程A不會對Dll1調用DllMain(DLL_THREAD_ATTACH),而在線程A退出時,卻會調用DLL1的DllMain(DLL_THREAD_DETACH)。這種不同步的現象是不是讓你內心感覺很疑惑?你說windows為什么要這么設計呢?我不明白。《windows核心編程》也有對該現象的一個描述:雖然當系統將該線程連接到該DLL的時候,不會向該DLL發送DLL_THREAD_ATTACH通知。但是當系統將該線程與DLL解除連接的時候,卻會向該DLL發送DLL_THREAD_DETACH通知。由于這個原因,我們在進行與線程相關的清理時必須極其小心。幸運的是,在大多數程序中,調用Loadlibrary的線程與調用Freelibrary的線程是同一個線程。
? ? ? ? 現在我們再將過程61換成6,并依次用101(TerminateProcess)、102(ExitProcess)、103(TerminateThread)、104(ExitThread)替換10。我列一下不同點
| 101 | The thread 'Win32 Thread' (0x142c) has exited with code -1 (0xffffffff). The program '[6128] CallDllMain.exe: Native' has exited with code -1 (0xffffffff). | 執行線程(0x142c)和進程退出時未對任何加載的DLL調用DllMain。 沒有對主線程退出的捕獲。 |
| 102 | The thread 'Win32 Thread' (0x1214) has exited with code -1 (0xffffffff). Dll2 TID:4660 DLL_PROCESS_DETACH Dll1 TID:4660 DLL_PROCESS_DETACH The program '[2576] CallDllMain.exe: Native' has exited with code -1 (0xffffffff). | 主進程(0x1214) 提前意外關閉,未對任何加載的DLL調用DllMain。 執行線程(4660)退出時對加載了的DLL調用了其DllMain的DLL_PROCESS_DETACH。 |
| 103 | The thread 'Win32 Thread' (0x81c) has exited with code -1 (0xffffffff). Proceess Exit Dll2 TID:2356 DLL_PROCESS_DETACH Dll1 TID:2356 DLL_PROCESS_DETACH The program '[5860] CallDllMain.exe: Native' has exited with code 0 (0x0). | 執行線程(0x81c)退出時未對任何加載的DLL調用DllMain。 主進程(2356)退出時對加載了的DLL調用了其DllMain的DLL_PROCESS_DETACH。 |
| 104 | Dll2 TID:5600 DLL_THREAD_DETACH Dll1 TID:5600 DLL_THREAD_DETACH The thread 'Win32 Thread' (0x15e0) has exited with code -1 (0xffffffff). Proceess Exit Dll2 TID:632 DLL_PROCESS_DETACH Dll1 TID:632 DLL_PROCESS_DETACH The program '[284] CallDllMain.exe: Native' has exited with code 0 (0x0). | 執行線程(5600)退出時對加載的DLL調用了DllMain,且原因是DLL_THREAD_DETACH。 主進程(632)退出時對加載了的DLL調用了其DllMain的DLL_PROCESS_DETACH。 |
? ? ? ? 從以上我們可以看出Terminate(101、103)類型函數比Exit(102、104)類型函數暴力。
? ? ? ? 102例子中我們看到主線程退出后,子線程還在正常工作的場景,可以想象,可能是ExitProcess是直接TerminateThread主線程了。總結如下:
? ? ? ? 八 TerminateProcess 將導致線程和進程在退出時不對未卸載的DLL進行DllMain調用。
? ? ? ? 九 ExitProcess將導致主線程意外退出,子線程對未卸載的DLL進行了DllMain調用,且調用原因是DLL_PROCESS_DETACH。(《windows核心編程》上是說,調用ExitProcess函數的線程將負責執行DllMain函數的代碼。(DLL_PROCESS_DETACH))
? ? ? ? 十?ExitThread是最和平的退出方式,它會讓線程退出前對未卸載的DLL調用DllMain。
? ? ? ? 我們再考慮下DisableThreadLibraryCalls函數對DllMain函數的調用的影響。我們在Dll1的DllMain中加入DisableThreadLibraryCalls(hModule);我們觀察下結果
| ? | MainTid:7760 | ? |
| 0 | CreatThread A ThreadA 7992 | ? |
| 1 | LoadLibraryA Dll1 TID:7760 DLL_PROCESS_ATTACH | 加載DLL1,執行線程調用其DllMain,原因是DLL_PROCESS_ATTACH。 |
| 2 | CreatThread B ThreadB 6684 | 線程B創建不會對DLL1調用DllMain了。因為DLL1中調用了DisableThreadLibraryCalls。 |
| 3 | LoadLibraryA Dll2 Dll2 TID:7760 DLL_PROCESS_ATTACH | 加載DLL2。執行線程調用其DllMain,原因是DLL_PROCESS_ATTACH。 |
| 4 | CreatThread C Dll2 TID:8168 DLL_THREAD_ATTACH ThreadC 8168 | 線程C創建不會對DLL1調用DllMain了。但是會對沒有調用過DisableThreadLibraryCalls的DLL2調用DllMain。 |
| 5 | CreatThread D Dll2 TID:1848 DLL_THREAD_ATTACH ThreadD 1848 | 同上 |
| 6 | TerminateThread D | ? |
| 7 | ThreadA Will Exit Dll2 TID:7992 DLL_THREAD_DETACH | 線程A退出,不會對DLL1調用DllMain了。但是會對沒有調用過DisableThreadLibraryCalls的DLL2調用DllMain。 |
| 8 | ThreadB Will Exit Dll2 TID:6684 DLL_THREAD_DETACH | 同上 |
| 9 | ThreadC Will Exit Dll2 TID:8168 DLL_THREAD_DETACH | 同上 |
| 10 | Dll2 TID:7760 DLL_THREAD_DETACH Proceess Exit Dll2 TID:8096 DLL_PROCESS_DETACH Dll1 TID:8096 DLL_PROCESS_DETACH | 執行線程(7760)出前不會對DLL1調用DllMain了。 進程退出前,主線程會對DLL1和DLL2調用DllMain。 |
? ? ? ? 通過以上我們可以再得出一個結論
? ? ? ? 十一 線程的創建和退出不會對調用了DisableThreadLibraryCalls的DLL調用DllMain。
? ? ? ? 最后,我們考慮下LoadLibrary和Freelibrary對DllMain的影響。我將在兩個線程中嘗試多次LoadLibrary同一個Dll,多次Freelibrary同一個Dll。
PrintLog("LoadLibraryA1\n");
HMODULE hDll1 = LoadLibraryA("DLL1");
WAIT();
PrintLog("LoadLibraryA2\n");
hDll1 = LoadLibraryA("DLL1");
WAIT();
PrintLog("LoadLibraryA3\n");
hDll1 = LoadLibraryA("DLL1");
WAIT();
CreateThread( NULL, NULL, ThreadFun, NULL, 0, NULL );
Sleep(35000);
PrintLog("FreeLibrary1\n");
FreeLibrary(hDll1);
WAIT();
PrintLog("FreeLibrary2\n");
FreeLibrary(hDll1);
WAIT();
PrintLog("FreeLibrary3\n");
FreeLibrary(hDll1);
WAIT();
其結果是
LoadLibraryA1
Dll1 TID:4620 DLL_PROCESS_ATTACH
LoadLibraryA2
LoadLibraryA3
Dll1 TID:5560 DLL_THREAD_ATTACH 子線程創建時調用的
MainTid:5560
……
FreeLibrary1
FreeLibrary2
FreeLibrary3
Dll1 TID:4620 DLL_PROCESS_DETACH
子線程創建時調用的
MainTid:5560
……
FreeLibrary1
FreeLibrary2
FreeLibrary3
Dll1 TID:4620 DLL_PROCESS_DETACH
? ? ? ? 我們發現第一句LoadLibrary對DllMain產生了調用DLL_PROCESS_ATTACH,而第二三句LoadLibrary不會對DllMain產生任何調用(《windows核心編程》系統不會讓進程的主線程用DLL_THREAD_ATTACH值調用DLLMain函數。系統不會讓用DLL_PROCESS_ATTACH來調用該DLL的DllMain函數的線程不會得到DLL_THREAD_ATTACH通知);第一二次FreeLibrary對DllMain沒有產生調用,而第三次FreeLibrary對DllMain產生了DLL_PROCESS_DETACH調用。
? ? ? ? 可以見得,在一個線程中對DLL產生了DllMain調用后,就不會因為Loadlibrary再發生DllMain的調用。
? ? ? ? 我們前兩次FreeLibrary不會對DllMain進行調用,而第三次就是DLL_PROCESS_DETACH。同樣這個線程中LoadLibraryA也被調用了三次。可以想象LoadLibraryA和FreeLibrary之間存在一個計數器的關系(LoadLibraryA加計數器,FreeLibrary減計數器)。正如《windows核心編程》上所說:當系統第一次將一個DLL映射到進程的地址空間中時……如果之后一個線程在調用Loadlibrary(Ex)來載入一個已經被映射到進程的地址空間的DLL,那么操作系統只不過是遞增該DLL的使用計數,而不會再次用DLL_PROCESS_ATTACH來調用DllMain函數。
? ? ? ? 本文中介紹了經過幾輪實驗,得出了11條規律。我們之后研究DllMain導致的死鎖,將用到這些規律。
總結
以上是生活随笔為你收集整理的DllMain中不当操作导致死锁问题的分析--进程对DllMain函数的调用规律的研究和分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DllMain中不当操作导致死锁问题的分
- 下一篇: DllMain中不当操作导致死锁问题的分