DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子
? ? ? ? 有了前面兩節的基礎,我們現在切入正題:研究下DllMain為什么會因為不當操作導致死鎖的問題。首先我們看一段比較經典的“DllMain中死鎖”代碼。(轉載請指明出于breaksoftware的csdn博客)
//主線程中
HMODULE h = LoadLibraryA(strDllName.c_str());
?
// DLL中代碼
static DWORD WINAPI ThreadCreateInDllMain(LPVOID) {return 0;
}BOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{DWORD tid = GetCurrentThreadId();switch (ul_reason_for_call) {case DLL_PROCESS_ATTACH: {printf("DLL DllWithoutDisableThreadLibraryCalls_A:\tProcess attach (tid = %d)\n", tid);HANDLE hThread = CreateThread(NULL, 0, ThreadCreateInDllMain, NULL, 0, NULL);WaitForSingleObject(hThread, INFINITE);CloseHandle(hThread);}break;case DLL_PROCESS_DETACH:case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:break;}return TRUE;
}
? ? ? ? 簡要說下DLL中邏輯:設計該段代碼的同學希望在DLL第一次被映射到進程內存空間時,創建一個工作線程,該工作線程內容可能很簡單。為了盡可能簡單,我們讓這個工作線程直接返回0。這樣從邏輯和效率上看,都不會因為我們的工作線程寫的有問題而導致死鎖。然后我們在DllMain中等待這個線程結束才從返回。
? ? ? ? 粗略看這個問題,我們很難看出這個邏輯會導致死鎖。但是事實就是這樣發生了。我們跑一下程序,發現程序輸出一下結果
? ? ? ? 后就停住了,光標在閃動,貌似還是在等待我們輸入。可是我們怎么敲擊鍵盤都沒有用:它死鎖了。
? ? ? ? 我是在VS2005中調試該程序,于是我們可以Debug->Break All來凍結所有線程。
? ? ? ? 我們先查看主線程(3096)的堆棧
? ? ? ? ? ?堆棧不長,我全部列出來
| 17 | ntdll.dll!_KiFastSystemCallRet@0() |
| 16 | ntdll.dll!_NtWaitForSingleObject@12() |
| 15 | kernel32.dll!_WaitForSingleObjectEx@12() |
| 14 | kernel32.dll!_WaitForSingleObject@8() |
| 13 | DllWithoutDisableThreadLibraryCalls_A.dll!DllMain(HINSTANCE__ * hModule=0x10000000, unsigned long ul_reason_for_call=1, void * lpReserved=0x00000000) |
| 12 | DllWithoutDisableThreadLibraryCalls_A.dll!__DllMainCRTStartup(void * hDllHandle=0x10000000, unsigned long dwReason=1, void * lpreserved=0x00000000) |
| 11 | DllWithoutDisableThreadLibraryCalls_A.dll!_DllMainCRTStartup(void * hDllHandle=0x10000000, unsigned long dwReason=1, void * lpreserved=0x00000000) |
| 10 | ntdll.dll!_LdrpCallInitRoutine@16() |
| 9 | ntdll.dll!_LdrpRunInitializeRoutines@4() |
| 8 | ntdll.dll!_LdrpLoadDll@24() |
| 7 | ntdll.dll!_LdrLoadDll@16() |
| 6 | kernel32.dll!_LoadLibraryExW@12() |
| 5 | kernel32.dll!_LoadLibraryExA@12() |
| 4 | kernel32.dll!_LoadLibraryA@4() |
| 3 | DllMainSerial.exe!wmain(int argc=3, wchar_t * * argv=0x003b7000) |
| 2 | DllMainSerial.exe!__tmainCRTStartup() |
| 1 | DllMainSerial.exe!wmainCRTStartup() |
| 0 | kernel32.dll!_BaseProcessStart@4() |
? ? ? 我們看下這個堆棧。大致我們可以將我們程序分為4段:
? ? ? ? 0 啟動啟動我們程序
? ? ? ? 1~6 我們加載Dll。
? ? ? ? 7~10 系統為我們準備DLL的加載。
? ? ? ? 11~17 DLL內部代碼執行。
? ? ? ? 我們關注一下14~17這段對WaitForSingleObject的調用邏輯。15、16步這個過程顯示了Kernel32中的WaitForSingleObjectEx在底層是調用了NtDll中的NtWaitForSingleObject。在NtWaitForSingleObject內部,即17步,我們看到的“_KiFastSystemCallRet@0”。這兒要說明下,這個并不是意味著我們程序執行到這個函數。我們看下這個函數的代碼
? ? ? ? KiFastSystemCallRet函數是內核態(Ring0層)邏輯回到用戶態(Ring3層)的著陸點。與之相對應的KiFastSystemCall函數是用戶態進入內核態必要的調用方法。因為內核態代碼我們是無法查看的,所以動態斷點只能設置到KiFastSystemCallRet開始處。所以實際死鎖是因為NtWaitForSingleObject在底層調用了KiFastSystemCall進入內核,在內核態中死鎖的。
? ? ? ? 我們在《DllMain中不當操作導致死鎖問題的分析--死鎖介紹》中介紹過,死鎖存在的條件是相互等待。主線程中,我們發現其等待的是工作線程結束。那么工作線程在等待主線程什么呢?我們看下工作線程的調用堆棧
? ? ? ? 我們對這個堆棧進行編號
| 6 | ntdll.dll!_KiFastSystemCallRet@0() |
| 5 | ntdll.dll!_NtWaitForSingleObject@12()? + 0xc bytes |
| 4 | ntdll.dll!_RtlpWaitForCriticalSection@4()? + 0x8c bytes |
| 3 | ntdll.dll!_RtlEnterCriticalSection@4()? + 0x46 bytes |
| 2 | ntdll.dll!__LdrpInitialize@12() ?+ 0xb4bf bytes |
| 1 | ntdll.dll!_KiUserApcDispatcher@20()? + 0x7 bytes |
| 0 | ntdll.dll!_RtlAllocateHeap@12()? + 0x9b48 bytes |
? ? ? ?我們看到倒數兩步(5、6)和主線程中最后兩步(16、17)是相同的,即工作線程也是在進入內核態后死鎖的。我們知道主線程在等工作線程結束,那么工作線程在等什么呢?我們追溯棧,請關注“ntdll.dll!__LdrpInitialize@12()?+ 0xb4bf bytes”處的代碼?
? ? ? ??我們看到,是因為_RtlEnterCriticalSection在底層調用了NtWaitForSingleObject。那么我們關注下_RtlEnterCriticalSection的參數_LdrpLoaderLock,它是什么?我們借助下IDA查看下LdrpInitialize反編譯代碼
……
v4 = *(_DWORD *)(*MK_FP(__FS__, 0x18) + 0x30);
v3 = *MK_FP(__FS__,0x18);……*(_DWORD *)(v4 + 0xa0) = &LdrpLoaderLock;if ( !(unsigned __int8)RtlTryEnterCriticalSection(&LdrpLoaderLock) ){……RtlEnterCriticalSection(&LdrpLoaderLock);}……if ( *(_DWORD *)(v4 + 0xc) ){……LdrpInitializeThread(a1);}else{
……v17 = LdrpInitializeProcess(a1, a2, &v11, v14, v15);
……}
……
? ? ? ? 由RtlTryEnterCriticalSection 可知LdrpLoaderLock是_RTL_CRITICAL_SECTION類型。在嘗試進入臨界區之前,LdrpLoaderLock將被保存到某個結構體變量v4的某個字段(偏移0xA0)中。那么v4是什么類型呢?這兒可能要科普下windows x86操作系統的一些知識:
? ? ? ? 在windows系統中每個用戶態線程都有一個記錄其執行環境的結構體TEB(Thread Environment Block)。TEB結構體中第一個字段是一個TIB(ThreadInformation Block)結構體,該結構體中保存著異常登記鏈表等信息。在x86系統中,段寄存器FS總是指向TEB結構。于是FS:[0]指向TEB起始字段,也就是指向TIB結構體。我們用Windbg查看下TEB的結構體,該結構體很大,我只列出我們目前關心的字段
lkd> dt _TEB
nt!_TEB+0x000 NtTib : _NT_TIB+0x01c EnvironmentPointer : Ptr32 Void+0x020 ClientId : _CLIENT_ID
……
? ? ? ? NtTib就是TIB結構體對象名。 我們再看下TIB結構體
lkd> dt _NT_TIB
nt!_NT_TIB+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD+0x004 StackBase : Ptr32 Void+0x008 StackLimit : Ptr32 Void+0x00c SubSystemTib : Ptr32 Void+0x010 FiberData : Ptr32 Void+0x010 Version : Uint4B+0x014 ArbitraryUserPointer : Ptr32 Void+0x018 Self : Ptr32 _NT_TIB
? ? ? ? 該結構體其他字段不解釋,我們只看最后一個字段(FS:[18])指向_NT_TIB結構體的指針Self。正如其名,該字段指向的是TIB結構體在進程空間中的虛擬地址。為什么要指向自己?那我們是否可以直接使用FS:[0]地址?不可以。舉個例子:我用windbg掛載到我電腦上一個運行中的calc(計算器)。我們查看fs:[0]指向空間保存的值,7ffdb000是TIB的Self字段。
? ? ? ? 我們查看TIB結構體去匹配該地址指向的空間的。
? ? ? ? 可以看到7ffdb000所指向的空間的各字段的值和FS:[0]指向的空間的值一致。但是如果我們這樣輸入就會失敗
? ? ? ? 介紹完這些后,我們再回到IDA反匯編的代碼中。v4 = *(_DWORD*)(*MK_FP(__FS__, 0x18) + 0x30);這段中MK_FP不是一個函數,是一個宏。它的作用是在基址上加上偏移得出一個地址。于是MK_FP(__FS__, 0x18)就是FS:[0x18],即TIB的Self字段。在該地址再加上0x30得到的地址已經超過了TIB空間,于是我們繼續查看TEB結構體
? ? ? ? 發現0x30偏移的是PEB(Process Environment Block)。
lkd> dt _PEB
nt!_PEB+0x000 InheritedAddressSpace : UChar+0x001 ReadImageFileExecOptions : UChar
……
+0x09c GdiDCAttributeList : Uint4B+0x0a0 LoaderLock : Ptr32 Void+0x0a4 OSMajorVersion : Uint4B
? ? ? ? 可以發現該結構體偏移0xa0處是一個名字為LoaderLock的變量。
? ? ? ? 《windows核心編程》中有關于DllMain序列化執行的講解,大致意思是:線程在調用DllMain之前,要先獲取鎖,等DllMain執行完再解開這個鎖。這樣不同線程加載DLL就可以實現序列化操作。而在微軟官方文檔《Best Practices for Creating DLLs》中也有對這個說法的佐證
The DllMain entry-point function. This function is called by the loader when it loads or unloads a DLL. The loader serializes calls to DllMain so that only a single DllMain function is run at a time .
? ? ? ??
? ? ? ? 其中還有段關于這個鎖的介紹
The loader lock. This is a process-wide synchronization primitive that the loader uses to ensure serialized loading of DLLs. Any function that must read or modify the per-process library-loader data structures must acquire this lock before performing such an operation. The loader lock is recursive, which means that it can be acquired again by the same thread.
? ? ? ? 在該文中多處對這個鎖的說明值暗示這個鎖是PEB中的LoaderLock。
? ? ? ? 那么剛才為什么要*(_DWORD *)(v4 + 0xa0) = &LdrpLoaderLock;?因為該LdrpLoaderLock是進程內共享的變量。這樣每個線程在執行初期,會先進入該臨界區,從而實現在進程內DllMain的執行是序列化的。于是我們得出以下結論:
? ? ? ? 進程內所有線程共用了同一個臨界區來序列化DllMain的執行。
? ? ? ? 結合《DllMain中不當操作導致死鎖問題的分析--進程對DllMain函數的調用規律的研究和分析》中介紹的規律
? ? ? ??二 線程創建后會調用已經加載了的DLL的DllMain,且調用原因是DLL_THREAD_ATTACH。
? ? ??我們發現
HANDLE hThread = CreateThread(NULL, 0, ThreadCreateInDllMain, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
? ? ? ? 主線程進入臨界區去調用DllMain時進入了臨界區,而工作線程也要進入臨界區去執行DllMain。但是此時臨界區被主線程占用,工作線程便進入等待狀態。而主線程卻等待工作線程退出才退出臨界區。于是這就是死鎖產生的原因。
總結
以上是生活随笔為你收集整理的DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DllMain中不当操作导致死锁问题的分
- 下一篇: DllMain中不当操作导致死锁问题的分