LoadLibrary深入案例詳解
LoadLibrary流程分析
在Windows開發(fā)中,我們都有過一個(gè)規(guī)定:在DllMain中不應(yīng)該處理過于復(fù)雜的事情,防止死鎖的發(fā)生。
那么,到底為什么DllMain中容易導(dǎo)致死鎖呢?下面我們來分析一下LoadLibrary的整個(gè)流程和原理。
1. 使用
我們來看一下LoadLibrary怎么使用的,由于這個(gè)函數(shù)底層是調(diào)用LoadLibraryEx我們看LoadLibraryEx的使用情況。
1.1 聲明
HMODULE WINAPI LoadLibraryEx( _In_ LPCTSTR lpFileName, _Reserved_ HANDLE hFile, _In_ DWORD dwFlags );
這里主要第三個(gè)參數(shù)使用起來有需要注意的地方:
- DONT_RESOLVE_DLL_REFERENCES : 這個(gè)標(biāo)志用于告訴系統(tǒng)將DLL映射到調(diào)用進(jìn)程的地址空間中,但是不調(diào)用DllMain并且不加載依賴Dll(只映射自己本身)。
- LOAD_LIBRARY_AS_DATAFILE : 這個(gè)標(biāo)志與DONT_RESOLVE_DLL_REFERENCES標(biāo)志相類似,因?yàn)橄到y(tǒng)只是將DLL映射到進(jìn)程的地址空間中,就像它是數(shù)據(jù)文件一樣。系統(tǒng)并不花費(fèi)額外的時(shí)間來準(zhǔn)備執(zhí)行文件中的任何代碼。
- LOAD_LIBRARY_SEARCH_USER_DIRS : 搜索路徑的使用使用AddDllDirectory和SetDllDirectory設(shè)置的路徑(保護(hù)Dll自己和依賴Dll)。
- LOAD_LIBRARY_SEARCH_SYSTEM32 : 從%windows%\system32加載Dll和其依賴項(xiàng)。
- LOAD_LIBRARY_SEARCH_APPLICATION_DIR : 應(yīng)用程序安裝路徑搜索Dll和其依賴項(xiàng)。
- LOAD_WITH_ALTERED_SEARCH_PATH : 按照如下目錄搜索:
- 進(jìn)程當(dāng)前目錄。
- Windows的系統(tǒng)目錄。
- 16 位Windows的系統(tǒng)目錄。
- Windows目錄。
- path環(huán)境變量目錄。
默認(rèn)情況下,LoadLibrary和LoadLibrary按照如下目錄搜索:
- 進(jìn)程當(dāng)前目錄。
- SetDllDirectory設(shè)置的文件夾路徑。
- Windows的系統(tǒng)目錄。
- 16 位Windows的系統(tǒng)目錄。
- Windows目錄。
- path環(huán)境變量目錄。
1.2 SetDllDirectoryW
這個(gè)函數(shù)的實(shí)現(xiàn)如下:
其中KernelBaseGetGlobalData返回的結(jié)果信息如下:
如下信息為:
0:003> dd KERNELBASE!KernelBaseGlobalData 75b155a0 00000000 00000000 00160014 7f9a1240 75b155b0 00280026 7f9a1260 00000000 00000000 75b155c0 00000000 00e60000 75b156c0 7fff0000 75b155d0 00c4b494 0000011a 00cc1914 0000012c 75b155e0 00cb9f34 00000253 00cc2658 00cc5e20 75b155f0 00000000 00e84380 0000000f ffffffff 75b15600 ffffffff 00000000 00000000 00000000 75b15610 020007d0 75950000 00000000 00000000 0:003> du 7f9a1240 7f9a1240 "C:\Windows" 0:003> du 7f9a1260 7f9a1260 "C:\Windows\system32"
2. LoadLibrary分析
下面來分析一下這個(gè)函數(shù)的執(zhí)行過程:
HMODULE __stdcall LoadLibraryW(LPCWSTR lpLibFileName) { return LoadLibraryExW(lpLibFileName, 0, 0); }
2.1 LoadLibraryExW
對于LoadLibraryW的主要流程如下:
HMODULE __stdcall LoadLibraryExW(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags) { //... SearchPath = BaseGetProcessDllPath( &dwFlags, (chFlags & LOAD_WITH_ALTERED_SEARCH_PATH) != 0 ? DllName.Buffer : 0, 0, (int)&dwFlags); //... ntStatus = LdrLoadDll(SearchPath, (PULONG)&lpLibFileName, &DllName, &hFile); }
通過BaseGetProcessDllPath獲取到的路徑為:
00072acc "C:\Users\xxx\Desktop;C:\Windows\" 00072b0c "system32;C:\Windows\system;C:\Wi" 00072b4c "ndows;.;C:\Windows\system32;C:\W" 00072b8c "indows;C:\Windows\System32\Wbem;" 00072bcc "C:\Windows\System32\WindowsPower" 00072c0c "Shell\v1.0\"
接下來就是使用LdrLoadDll價(jià)值dll了。
2.2 LdrLoadDll
NTSTATUS __stdcall LdrLoadDll(PWSTR SearchPath, PULONG LoadFlags, PUNICODE_STRING DllName, PVOID *BaseAddress) { //... if ( SearchPath ) { result = RtlInitUnicodeStringEx(&DestinationString, SearchPath); if ( result < 0 ) return result; NewSearchPath = &DestinationString; } else { NewSearchPath = &LdrpDefaultPath; } //... v7 = LdrpLoadDll(DllName, (int)NewSearchPath, v6, 1, 0, (int)&DllName); //... return v7; }
這里有一個(gè)默認(rèn)的加載路徑為LdrpDefaultPath,路徑信息如下:
0:000> dS LdrpDefaultPath 00061560 "C:\Users\xxx\Desktop;C:\Windows\" 000615a0 "system32;C:\Windows\system;C:\Wi" 000615e0 "ndows;.;C:\Windows\system32;C:\W" 00061620 "indows;C:\Windows\System32\Wbem;" 00061660 "C:\Windows\System32\WindowsPower" 000616a0 "Shell\v1.0\"
2.3 LdrpLoadDll
int __stdcall LdrpLoadDll(PCUNICODE_STRING Source, int a2, int a3, char a4, int a5, int a6) { //... if ( !LdrpInLdrInit ) RtlEnterCriticalSection(&LdrpLoaderLock); //... LdrpFindOrMapDll(*(PCUNICODE_STRING *)((char *)&v31 + 1), v29, a3, v27[0], (int)&v33, (int)&v31); //... if ( v21 & 0x1000000 ) v22 = LdrpCorProcessImports((void *)v21, v33); else v22 = LdrpProcessStaticImports(v33, v29); //... LdrpRunInitializeRoutines(0); //... if ( !LdrpInLdrInit ) RtlLeaveCriticalSection(&LdrpLoaderLock); }
這個(gè)函數(shù)的整理流程如下:
獲取加載鎖RtlEnterCriticalSection(&LdrpLoaderLock);嘗試加載dll: LdrpFindOrMapDll。處理導(dǎo)入表信息。運(yùn)行回調(diào)函數(shù)LdrpRunInitializeRoutines。釋放鎖RtlLeaveCriticalSection(&LdrpLoaderLock);。
這里值得注意的地方就是LdrpRunInitializeRoutines是調(diào)用DllMain函數(shù),調(diào)用堆棧信息如下:
# ChildEBP RetAddr Args to Child 00 0029d718 67f42c22 67ef0000 00000001 00000000 DllTest!DllMain 01 0029d75c 67f42def 67ef0000 00000001 00000000 DllTest!dllmain_dispatch+0xb2 02 0029d770 777b89d8 67ef0000 00000001 00000000 DllTest!_DllMainCRTStartup+0x1f 03 0029d790 777c5c41 67f3dcc5 67ef0000 00000001 ntdll!LdrpCallInitRoutine+0x14 04 0029d884 777c052e 00000000 73bd12d3 777a7c9a ntdll!LdrpRunInitializeRoutines+0x26f 05 0029d9f0 777c232c 0029da50 0029da1c 00000000 ntdll!LdrpLoadDll+0x4d1 06 0029da24 75b688ee 00072acc 0029da64 0029da50 ntdll!LdrLoadDll+0x92 07 0029da5c 763d3c12 00000000 00000000 00000001 KERNELBASE!LoadLibraryExW+0x15a
加載dll的時(shí)候,會獲取鎖LdrpLoaderLock;然而加載的時(shí)候又會調(diào)用LdrpRunInitializeRoutines進(jìn)入DllMain。
2.4 LdrpFindOrMapDll
加載和映射的dll的函數(shù)是LdrpFindOrMapDll,這個(gè)函數(shù)在加載Dll之前,需要判斷Dll是否被加載過, 已經(jīng)加載的dll,通過一個(gè)哈希表加載,哈希表如下:
LIST_ENTRY LdrpHashTable[LDR_HASH_TABLE_ENTRIES];
其實(shí)每個(gè)LdrpHashTable都是通過一個(gè)_LDR_DATA_TABLE_ENTRY的成員HashLinks鏈接起來,結(jié)構(gòu)如下:
0:000> dt ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY +0x008 InMemoryOrderLinks : _LIST_ENTRY +0x010 InInitializationOrderLinks : _LIST_ENTRY +0x018 DllBase : Ptr32 Void +0x01c EntryPoint : Ptr32 Void +0x020 SizeOfImage : Uint4B +0x024 FullDllName : _UNICODE_STRING +0x02c BaseDllName : _UNICODE_STRING +0x034 Flags : Uint4B +0x038 LoadCount : Uint2B +0x03a TlsIndex : Uint2B +0x03c HashLinks : _LIST_ENTRY //LdrpHashTable連接的列表結(jié)構(gòu) +0x03c SectionPointer : Ptr32 Void +0x040 CheckSum : Uint4B +0x044 TimeDateStamp : Uint4B +0x044 LoadedImports : Ptr32 Void +0x048 EntryPointActivationContext : Ptr32 _ACTIVATION_CONTEXT +0x04c PatchInformation : Ptr32 Void +0x050 ForwarderLinks : _LIST_ENTRY +0x058 ServiceTagLinks : _LIST_ENTRY +0x060 StaticLinks : _LIST_ENTRY +0x068 ContextInformation : Ptr32 Void +0x06c OriginalBase : Uint4B +0x070 LoadTime : _LARGE_INTEGER
對于每個(gè)加載的DLL,都會有兩種形式:
- dll名稱加載。
- dll全路徑加載。
在LdrpFindLoadedDllByName也會根據(jù)不同的加載來匹配不同的情況:
- 如果使用dll名稱加載,那么比較BaseDllName;使用RtlEqualUnicodeString.
- 如果使用dll全路徑加載,那么比較FullDllName;使用RtlEqualUnicodeString.
例如:
0:000> dt ntdll!_LDR_DATA_TABLE_ENTRY 006f6270 +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x6f6358 - 0x6f5eb8 ] +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x6f6360 - 0x6f5ec0 ] +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x6f6e38 - 0x6f6368 ] +0x018 DllBase : 0x75b30000 Void +0x01c EntryPoint : 0x75b43273 Void +0x020 SizeOfImage : 0x110000 +0x024 FullDllName : _UNICODE_STRING "C:\Windows\syswow64\kernel32.dll" //全路徑匹配這個(gè) +0x02c BaseDllName : _UNICODE_STRING "kernel32.dll" //Dll名稱匹配這個(gè) +0x034 Flags : 0x84004 +0x038 LoadCount : 0xffff +0x03a TlsIndex : 0 +0x03c HashLinks : _LIST_ENTRY [ 0x77c148a0 - 0x77c148a0 ] +0x03c SectionPointer : 0x77c148a0 Void +0x040 CheckSum : 0x77c148a0 +0x044 TimeDateStamp : 0x589c961f +0x044 LoadedImports : 0x589c961f Void +0x048 EntryPointActivationContext : (null) +0x04c PatchInformation : (null) +0x050 ForwarderLinks : _LIST_ENTRY [ 0x718580 - 0x718580 ] +0x058 ServiceTagLinks : _LIST_ENTRY [ 0x6f62c8 - 0x6f62c8 ] +0x060 StaticLinks : _LIST_ENTRY [ 0x6f63f0 - 0x6f62f0 ] +0x068 ContextInformation : 0x77b4ef40 Void +0x06c OriginalBase : 0x7dd60000 +0x070 LoadTime : _LARGE_INTEGER 0x01d48576`ee26eab0
如果,這里匹配成功了,那么Dll就不會再加載了。
3. 總結(jié)
3.1 同名DLL
一個(gè)進(jìn)程是否可以加載相同名字的Dll呢?按照上面分析有一個(gè)加載規(guī)則
在LdrpFindLoadedDllByName也會根據(jù)不同的加載來匹配不同的情況:
- 如果使用dll名稱加載,那么比較BaseDllName;使用RtlEqualUnicodeString.
- 如果使用dll全路徑加載,那么比較FullDllName;使用RtlEqualUnicodeString.
那么可以從這個(gè)規(guī)則來看,是看加載是的方式:
int main(int args, char* argv[]) { LoadLibraryW(L"C:\\DllTest.dll"); LoadLibraryW(L"DllTest.dll"); system("pause"); return 0; }
這種方式,第二個(gè)應(yīng)該LoadLibraryW(L"DllTest.dll")不會去匹配加載了,如下:
那么我們按照這種方式加載呢?
int main(int args, char* argv[]) { LoadLibraryW(L"DllTest.dll"); LoadLibraryW(L"C:\\DllTest.dll"); system("pause"); return 0; }
按照規(guī)則來說,應(yīng)該是可以加載的,如下:
這個(gè)中情況在wow64 情況下,有時(shí)候會踩到坑,例如,對于wowo64路徑,如果直接使用C:\Windows\system32\kernel32.dll路徑加載,那么LdrpFindLoadedDllByName一定會失敗,因?yàn)镕ullDllName為wow64使用的值:C:\Windows\syswow64\kernel32.dll.
3.2 死鎖
我們知道,在調(diào)用Dllmain的時(shí)候,會占用鎖RtlEnterCriticalSection(&LdrpLoaderLock),所有凡是在DllMain中調(diào)用可能獲取RtlEnterCriticalSection(&LdrpLoaderLock)的操作,就可能會導(dǎo)致死鎖,例如:
- 直接調(diào)用LoadLibrary(Ex)。
- 調(diào)用CoInitializeEx(在CoInitializeEx底層會調(diào)用LoadLibraryEx)。
- 調(diào)用CreateThread(因?yàn)镃reateThread會繼續(xù)調(diào)用DllMain,稍有不慎可能就會死鎖)。
- 調(diào)用GetModuleFileName、GetModuleHandle 等(底層獲取RtlEnterCriticalSection(&LdrpLoaderLock)鎖)。
到此這篇關(guān)于LoadLibrary深入案例詳解的文章就介紹到這了,更多相關(guān)LoadLibrary詳解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
VisualStudio2019配置OpenCV4.5.0的方法示例
這篇文章主要介紹了VisualStudio2019配置OpenCV4.5.0的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03C++11中移動(dòng)構(gòu)造函數(shù)案例代碼
C++11 標(biāo)準(zhǔn)中為了滿足用戶使用左值初始化同類對象時(shí)也通過移動(dòng)構(gòu)造函數(shù)完成的需求,新引入了 std::move() 函數(shù),它可以將左值強(qiáng)制轉(zhuǎn)換成對應(yīng)的右值,由此便可以使用移動(dòng)構(gòu)造函數(shù),對C++11移動(dòng)構(gòu)造函數(shù)相關(guān)知識感興趣的朋友一起看看吧2023-01-01C++利用鏈表實(shí)現(xiàn)圖書信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++利用鏈表實(shí)現(xiàn)圖書信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11Qt自定義Widget實(shí)現(xiàn)互斥效果詳解
在使用Qt時(shí),可能會遇到這種問題:多個(gè)控件互斥,類似于QRadiButton控件,但又不是單純的QRadioButton控件,互斥的可能是一個(gè)窗口,也可能是幾個(gè)按鈕,等等多種情況。本文將介紹利用Qt自定義Widget實(shí)現(xiàn)的互斥效果,需要的可以參考一下2022-01-01C++中main函數(shù)怎樣調(diào)用類內(nèi)函數(shù)
這篇文章主要介紹了C++中main函數(shù)怎樣調(diào)用類內(nèi)函數(shù)問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08