日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子

發布時間:2023/11/27 生活经验 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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)的堆棧

? ? ? ? ? ?堆棧不長,我全部列出來

17ntdll.dll!_KiFastSystemCallRet@0()
16ntdll.dll!_NtWaitForSingleObject@12()
15kernel32.dll!_WaitForSingleObjectEx@12()
14kernel32.dll!_WaitForSingleObject@8()
13DllWithoutDisableThreadLibraryCalls_A.dll!DllMain(HINSTANCE__ * hModule=0x10000000, unsigned long ul_reason_for_call=1, void * lpReserved=0x00000000)
12DllWithoutDisableThreadLibraryCalls_A.dll!__DllMainCRTStartup(void * hDllHandle=0x10000000, unsigned long dwReason=1, void * lpreserved=0x00000000)
11DllWithoutDisableThreadLibraryCalls_A.dll!_DllMainCRTStartup(void * hDllHandle=0x10000000, unsigned long dwReason=1, void * lpreserved=0x00000000)
10ntdll.dll!_LdrpCallInitRoutine@16()
9ntdll.dll!_LdrpRunInitializeRoutines@4()
8ntdll.dll!_LdrpLoadDll@24()
7ntdll.dll!_LdrLoadDll@16()
6kernel32.dll!_LoadLibraryExW@12()
5kernel32.dll!_LoadLibraryExA@12()
4kernel32.dll!_LoadLibraryA@4()
3DllMainSerial.exe!wmain(int argc=3, wchar_t * * argv=0x003b7000)
2DllMainSerial.exe!__tmainCRTStartup()
1DllMainSerial.exe!wmainCRTStartup()
0kernel32.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中不當操作導致死鎖問題的分析--死鎖介紹》中介紹過,死鎖存在的條件是相互等待。主線程中,我們發現其等待的是工作線程結束。那么工作線程在等待主線程什么呢?我們看下工作線程的調用堆棧

? ? ? ? 我們對這個堆棧進行編號

6ntdll.dll!_KiFastSystemCallRet@0()
5ntdll.dll!_NtWaitForSingleObject@12()? + 0xc bytes
4ntdll.dll!_RtlpWaitForCriticalSection@4()? + 0x8c bytes
3ntdll.dll!_RtlEnterCriticalSection@4()? + 0x46 bytes
2ntdll.dll!__LdrpInitialize@12() ?+ 0xb4bf bytes
1ntdll.dll!_KiUserApcDispatcher@20()? + 0x7 bytes
0ntdll.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中死锁的关键隐藏因子的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。