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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

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

發(fā)布時(shí)間:2023/11/27 生活经验 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

? ? ? ? 有了前面兩節(jié)的基礎(chǔ),我們現(xiàn)在切入正題:研究下DllMain為什么會(huì)因?yàn)椴划?dāng)操作導(dǎo)致死鎖的問題。首先我們看一段比較經(jīng)典的“DllMain中死鎖”代碼。(轉(zhuǎn)載請(qǐng)指明出于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;
}

? ? ? ? 簡(jiǎn)要說下DLL中邏輯:設(shè)計(jì)該段代碼的同學(xué)希望在DLL第一次被映射到進(jìn)程內(nèi)存空間時(shí),創(chuàng)建一個(gè)工作線程,該工作線程內(nèi)容可能很簡(jiǎn)單。為了盡可能簡(jiǎn)單,我們讓這個(gè)工作線程直接返回0。這樣從邏輯和效率上看,都不會(huì)因?yàn)槲覀兊墓ぷ骶€程寫的有問題而導(dǎo)致死鎖。然后我們?cè)贒llMain中等待這個(gè)線程結(jié)束才從返回。

? ? ? ? 粗略看這個(gè)問題,我們很難看出這個(gè)邏輯會(huì)導(dǎo)致死鎖。但是事實(shí)就是這樣發(fā)生了。我們跑一下程序,發(fā)現(xiàn)程序輸出一下結(jié)果

? ? ? ? 后就停住了,光標(biāo)在閃動(dòng),貌似還是在等待我們輸入。可是我們?cè)趺辞脫翩I盤都沒有用:它死鎖了。

? ? ? ? 我是在VS2005中調(diào)試該程序,于是我們可以Debug->Break All來凍結(jié)所有線程。

? ? ? ? 我們先查看主線程(3096)的堆棧

? ? ? ? ? ?堆棧不長(zhǎng),我全部列出來

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()

? ? ? 我們看下這個(gè)堆棧。大致我們可以將我們程序分為4段:

? ? ? ? 0 啟動(dòng)啟動(dòng)我們程序

? ? ? ? 1~6 我們加載Dll。

? ? ? ? 7~10 系統(tǒng)為我們準(zhǔn)備DLL的加載。

? ? ? ? 11~17 DLL內(nèi)部代碼執(zhí)行。

? ? ? ? 我們關(guān)注一下14~17這段對(duì)WaitForSingleObject的調(diào)用邏輯。15、16步這個(gè)過程顯示了Kernel32中的WaitForSingleObjectEx在底層是調(diào)用了NtDll中的NtWaitForSingleObject。在NtWaitForSingleObject內(nèi)部,即17步,我們看到的“_KiFastSystemCallRet@0”。這兒要說明下,這個(gè)并不是意味著我們程序執(zhí)行到這個(gè)函數(shù)。我們看下這個(gè)函數(shù)的代碼

? ? ? ? KiFastSystemCallRet函數(shù)是內(nèi)核態(tài)(Ring0層)邏輯回到用戶態(tài)(Ring3層)的著陸點(diǎn)。與之相對(duì)應(yīng)的KiFastSystemCall函數(shù)是用戶態(tài)進(jìn)入內(nèi)核態(tài)必要的調(diào)用方法。因?yàn)閮?nèi)核態(tài)代碼我們是無法查看的,所以動(dòng)態(tài)斷點(diǎn)只能設(shè)置到KiFastSystemCallRet開始處。所以實(shí)際死鎖是因?yàn)镹tWaitForSingleObject在底層調(diào)用了KiFastSystemCall進(jìn)入內(nèi)核,在內(nèi)核態(tài)中死鎖的。

? ? ? ? 我們?cè)凇禗llMain中不當(dāng)操作導(dǎo)致死鎖問題的分析--死鎖介紹》中介紹過,死鎖存在的條件是相互等待。主線程中,我們發(fā)現(xiàn)其等待的是工作線程結(jié)束。那么工作線程在等待主線程什么呢?我們看下工作線程的調(diào)用堆棧

? ? ? ? 我們對(duì)這個(gè)堆棧進(jìn)行編號(hào)

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

? ? ? ?我們看到倒數(shù)兩步(5、6)和主線程中最后兩步(16、17)是相同的,即工作線程也是在進(jìn)入內(nèi)核態(tài)后死鎖的。我們知道主線程在等工作線程結(jié)束,那么工作線程在等什么呢?我們追溯棧,請(qǐng)關(guān)注“ntdll.dll!__LdrpInitialize@12()?+ 0xb4bf bytes”處的代碼?

? ? ? ??我們看到,是因?yàn)開RtlEnterCriticalSection在底層調(diào)用了NtWaitForSingleObject。那么我們關(guān)注下_RtlEnterCriticalSection的參數(shù)_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類型。在嘗試進(jìn)入臨界區(qū)之前,LdrpLoaderLock將被保存到某個(gè)結(jié)構(gòu)體變量v4的某個(gè)字段(偏移0xA0)中。那么v4是什么類型呢?這兒可能要科普下windows x86操作系統(tǒng)的一些知識(shí):

? ? ? ? 在windows系統(tǒng)中每個(gè)用戶態(tài)線程都有一個(gè)記錄其執(zhí)行環(huán)境的結(jié)構(gòu)體TEB(Thread Environment Block)。TEB結(jié)構(gòu)體中第一個(gè)字段是一個(gè)TIB(ThreadInformation Block)結(jié)構(gòu)體,該結(jié)構(gòu)體中保存著異常登記鏈表等信息。在x86系統(tǒng)中,段寄存器FS總是指向TEB結(jié)構(gòu)。于是FS:[0]指向TEB起始字段,也就是指向TIB結(jié)構(gòu)體。我們用Windbg查看下TEB的結(jié)構(gòu)體,該結(jié)構(gòu)體很大,我只列出我們目前關(guān)心的字段

lkd> dt _TEB
nt!_TEB+0x000 NtTib            : _NT_TIB+0x01c EnvironmentPointer : Ptr32 Void+0x020 ClientId         : _CLIENT_ID
……

? ? ? ? NtTib就是TIB結(jié)構(gòu)體對(duì)象名。 我們?cè)倏聪耇IB結(jié)構(gòu)體

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

? ? ? ? 該結(jié)構(gòu)體其他字段不解釋,我們只看最后一個(gè)字段(FS:[18])指向_NT_TIB結(jié)構(gòu)體的指針Self。正如其名,該字段指向的是TIB結(jié)構(gòu)體在進(jìn)程空間中的虛擬地址。為什么要指向自己?那我們是否可以直接使用FS:[0]地址?不可以。舉個(gè)例子:我用windbg掛載到我電腦上一個(gè)運(yùn)行中的calc(計(jì)算器)。我們查看fs:[0]指向空間保存的值,7ffdb000是TIB的Self字段。


? ? ? ? 我們查看TIB結(jié)構(gòu)體去匹配該地址指向的空間的。

? ? ? ? 可以看到7ffdb000所指向的空間的各字段的值和FS:[0]指向的空間的值一致。但是如果我們這樣輸入就會(huì)失敗

? ? ? ? 介紹完這些后,我們?cè)倩氐絀DA反匯編的代碼中。v4 = *(_DWORD*)(*MK_FP(__FS__, 0x18) + 0x30);這段中MK_FP不是一個(gè)函數(shù),是一個(gè)宏。它的作用是在基址上加上偏移得出一個(gè)地址。于是MK_FP(__FS__, 0x18)就是FS:[0x18],即TIB的Self字段。在該地址再加上0x30得到的地址已經(jīng)超過了TIB空間,于是我們繼續(xù)查看TEB結(jié)構(gòu)體

? ? ? ? 發(fā)現(xiàn)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

? ? ? ? 可以發(fā)現(xiàn)該結(jié)構(gòu)體偏移0xa0處是一個(gè)名字為L(zhǎng)oaderLock的變量。

? ? ? ? 《windows核心編程》中有關(guān)于DllMain序列化執(zhí)行的講解,大致意思是:線程在調(diào)用DllMain之前,要先獲取鎖,等DllMain執(zhí)行完再解開這個(gè)鎖。這樣不同線程加載DLL就可以實(shí)現(xiàn)序列化操作。而在微軟官方文檔《Best Practices for Creating DLLs》中也有對(duì)這個(gè)說法的佐證

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 .


? ? ? ??

? ? ? ? 其中還有段關(guān)于這個(gè)鎖的介紹

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.

? ? ? ? 在該文中多處對(duì)這個(gè)鎖的說明值暗示這個(gè)鎖是PEB中的LoaderLock。

? ? ? ? 那么剛才為什么要*(_DWORD *)(v4 + 0xa0) = &LdrpLoaderLock;?因?yàn)樵揕drpLoaderLock是進(jìn)程內(nèi)共享的變量。這樣每個(gè)線程在執(zhí)行初期,會(huì)先進(jìn)入該臨界區(qū),從而實(shí)現(xiàn)在進(jìn)程內(nèi)DllMain的執(zhí)行是序列化的。于是我們得出以下結(jié)論:

? ? ? ? 進(jìn)程內(nèi)所有線程共用了同一個(gè)臨界區(qū)來序列化DllMain的執(zhí)行。

? ? ? ? 結(jié)合《DllMain中不當(dāng)操作導(dǎo)致死鎖問題的分析--進(jìn)程對(duì)DllMain函數(shù)的調(diào)用規(guī)律的研究和分析》中介紹的規(guī)律

? ? ? ??二 線程創(chuàng)建后會(huì)調(diào)用已經(jīng)加載了的DLL的DllMain,且調(diào)用原因是DLL_THREAD_ATTACH

? ? ??我們發(fā)現(xiàn)

HANDLE hThread = CreateThread(NULL, 0, ThreadCreateInDllMain, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);

? ? ? ? 主線程進(jìn)入臨界區(qū)去調(diào)用DllMain時(shí)進(jìn)入了臨界區(qū),而工作線程也要進(jìn)入臨界區(qū)去執(zhí)行DllMain。但是此時(shí)臨界區(qū)被主線程占用,工作線程便進(jìn)入等待狀態(tài)。而主線程卻等待工作線程退出才退出臨界區(qū)。于是這就是死鎖產(chǎn)生的原因。

總結(jié)

以上是生活随笔為你收集整理的DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。