在前一篇文章《DllMain()限入死锁问题分析 (一)》里,介绍了DllMain()死锁问题的理象及初步的调试分析结果。本章我们将继续分析导致死锁的详细原因。
之前我们看到,主线程和子线程因为都要访问一个变量ntdll!PebLdr,才导致进入了死锁。
0:000> x ntdll!pebldr
779f7880 ntdll!PebLdr = <no type information>
用x命令,只能看到这个变量的地址,看不到它的类型信息。但既然它的名字里有Peb,我们就不妨看看它和Peb有什么关联。
0:000> !peb
PEB at 7ffd3000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: Yes
ImageBaseAddress: 00230000
Ldr 779f7880
Ldr.Initialized: Yes
Ldr.InInitializationOrderModuleList: 00251af8 . 00252c30
Ldr.InLoadOrderModuleList: 00251a58 . 00252c20
Ldr.InMemoryOrderModuleList: 00251a60 . 00252c28
!peb命令列出的信息里有一项叫Ldr的地址正好与PebLdr的地址完全吻合。看一下_PEB结构的定义。
0:000> dt _PEB 7ffd3000
LoadDll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
+0x003 SpareBool : 0x8 ''
+0x004 Mutant : 0xffffffff Void
+0x008 ImageBaseAddress : 0x00230000 Void
+0x00c Ldr : 0x779f7880 _PEB_LDR_DATA
其中果然有一个成员叫Ldr,类型是_PEB_LDR_DATA。再来看一下这个类型的定义。
0:000> dt _PEB_LDR_DATA 779f7880
LoadDll!_PEB_LDR_DATA
+0x000 Length : 0x30
+0x004 Initialized : 0x1 ''
+0x008 SsHandle : (null)
+0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x251a58 - 0x252c20 ]
+0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x251a60 - 0x252c28 ]
+0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x251af8 - 0x252c30 ]
+0x024 EntryInProgress : (null)
可以看到其中有三个Module List,根据它们的名字,可以想见它们存放的内容是一样的,只不过顺序不同。它们的类型都是_LIST_ENTRY,也就是每个都是一个双向链表。
0:000> dt _LIST_ENTRY
LoadDll!_LIST_ENTRY
+0x000 Flink : Ptr32 _LIST_ENTRY
+0x004 Blink : Ptr32 _LIST_ENTRY
以其中第一个链表InLoadOrderModuleList为例,根据它列出的首尾指针,可以用dd命令依次遍历链表中每一个结点,结果发现这个链表有五个节点,分别是:
00251a58 -> 00251ae8 -> 00251e10 -> 00251f28 -> 00252c20
同时我们用lm命令列出已经加载的模块列表,刚好也是5个。
0:000> lm
start end module name
00230000 00236000 LoadDll
71c20000 71cc3000 MSVCR90
75af0000 75b3a000 KERNELBASE
77350000 77424000 kernel32
77920000 77a5c000 ntdll
可以根据每个节点的数据字段的信息找到更详细的信息证明对应的数据与lm列出的信息是一一对应的。为节约篇幅,这里只列出针对第一个节点的分析。
0:002> dt _LDR_DATA_TABLE_ENTRY 00251a58
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x251ae8 - 0x779f788c ]
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x251af0 - 0x779f7894 ]
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x018 DllBase : 0x00230000 Void
+0x01c EntryPoint : 0x002312db Void
+0x020 SizeOfImage : 0x6000
+0x024 FullDllName : _UNICODE_STRING "C:\ForWinDbg\EXEFiles\LoadDll.exe"
+0x02c BaseDllName : _UNICODE_STRING "LoadDll.exe"
+0x034 Flags : 0x4000
+0x038 LoadCount : 0xffff
+0x03a TlsIndex : 0
+0x03c HashLinks : _LIST_ENTRY [ 0x779fa660 - 0x779fa660 ]
+0x03c SectionPointer : 0x779fa660 Void
+0x040 CheckSum : 0x779fa660
+0x044 TimeDateStamp : 0x5462e799
+0x044 LoadedImports : 0x5462e799 Void
+0x048 EntryPointActivationContext : (null)
+0x04c PatchInformation : (null)
+0x050 ForwarderLinks : _LIST_ENTRY [ 0x251aa8 - 0x251aa8 ]
+0x058 ServiceTagLinks : _LIST_ENTRY [ 0x251ab0 - 0x251ab0 ]
+0x060 StaticLinks : _LIST_ENTRY [ 0x252920 - 0x2527b8 ]
+0x068 ContextInformation : 0x77990538 Void
+0x06c OriginalBase : 0
+0x070 LoadTime : _LARGE_INTEGER 0x0
至此我们可以猜测,当LoadDll调用LoadLibrary加载DllMain.dll时,该API会修改上述三个列表,因此在LoadLibrary函数中需要进入关键区以避免与其它人同时访问这三个列表。翻一下上一篇里提及的LdrpFindOrMapDll函数的代码,它确实可以印证这一点。
779808dd a190789f77 mov eax,dword ptr [ntdll!PebLdr+0x10 (779f7890)]779808e2 894604 mov dword ptr [esi+4],eax
779808e5 c7068c789f77 mov dword ptr [esi],offset ntdll!PebLdr+0xc (779f788c)
779808eb 8930 mov dword ptr [eax],esi
779808ed 8b0d98789f77 mov ecx,dword ptr [ntdll!PebLdr+0x18 (779f7898)]
779808f3 893590789f77 mov dword ptr [ntdll!PebLdr+0x10 (779f7890)],esi
779808f9 8d4608 lea eax,[esi+8]
779808fc c70094789f77 mov dword ptr [eax],offset ntdll!PebLdr+0x14 (779f7894)
77980902 894804 mov dword ptr [eax+4],ecx
77980905 8901 mov dword ptr [ecx],eax
77980907 a398789f77 mov dword ptr [ntdll!PebLdr+0x18 (779f7898)],eax
再来看子线程又为什么要访问PebLdr。回到发生死锁时的现场,栈回溯如下。
005af624 77966a24 77952264 00000030 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
005af628 77952264 00000030 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc (FPO: [3,0,0])
005af68c 77952148 00000000 00000000 00000000 ntdll!RtlpWaitOnCriticalSection+0x13e (FPO: [Non-Fpo])
005af6b4 77983795 779f7340 77c6147b 7ffdd000 ntdll!RtlEnterCriticalSection+0x150 (FPO: [Non-Fpo])
005af748 77983636 005af7b8 77c614a7 00000000 ntdll!LdrpInitializeThread+0xc6 (FPO: [Non-Fpo])
005af794 77983663 005af7b8 77920000 00000000 ntdll!_LdrpInitialize+0x1ad (FPO: [Non-Fpo])
从LdrpInitializeThread函数中摘取关键的代码出来。
ntdll!LdrpInitializeThread+0xbc:
7798378b 6840739f77 push offset ntdll!LdrpLoaderLock (779f7340)
77983790 e80b40feff call ntdll!RtlEnterCriticalSection (779677a0)
77983795 895dfc mov dword ptr [ebp-4],ebx
77983798 a194789f77 mov eax,dword ptr [ntdll!PebLdr+0x14 (779f7894)] ; eax = InMemoryOrderModuleList[0]->FLink
7798379d 8945e4 mov dword ptr [ebp-1Ch],eax
779837a0 33db xor ebx,ebx
779837a2 43 inc ebx
779837a3 e9fd000000 jmp ntdll!LdrpInitializeThread+0xd4 (779838a5)
ntdll!LdrpInitializeThread+0xfb:
77983836 8b4e1c mov ecx,dword ptr [esi+1Ch]; EntryPoint of the module, for DLL, it's DLLMain
77983839 894ddc mov dword ptr [ebp-24h],ecx
: : :
ntdll!LdrpInitializeThread+0x14c:
7798388b 6a00 push 0
7798388d 6a02 push 2
7798388f ff7618 push dword ptr [esi+18h]; DllBase
77983892 ff75dc push dword ptr [ebp-24h]; EntryPoint, that is DLLMain()
77983895 e82a51ffff call ntdll!LdrpCallInitRoutine (779789c4) ; Call DllMain() function of the module
ntdll!LdrpInitializeThread+0xd4:
779838a5 3d94789f77 cmp eax,offset ntdll!PebLdr+0x14 (779f7894)
779838aa 0f8454ffffff je ntdll!LdrpInitializeThread+0x17d (77983804)
ntdll!LdrpInitializeThread+0xdf:; InLoadOrderModuleList[] always = InMemoryOrderModuleList[] - 8
779838b0 8d70f8 lea esi,[eax-8] ;esi = InLoadOrderModuleList[0]->FLink
779838b3 8975d8 mov dword ptr [ebp-28h],esi
779838b6 8b4de0 mov ecx,dword ptr [ebp-20h]
779838b9 8b4908 mov ecx,dword ptr [ecx+8]
779838bc 3b4e18 cmp ecx,dword ptr [esi+18h]; Check DllBase
779838bf 740f je ntdll!LdrpInitializeThread+0x164 (779838d0) ; skip current module
ntdll!LdrpInitializeThread+0xf0:
779838c1 8b5634 mov edx,dword ptr [esi+34h]
779838c4 f7c200000400 test edx,40000h ; Check Flags
779838ca 0f8466ffffff je ntdll!LdrpInitializeThread+0xfb (77983836)
ntdll!LdrpInitializeThread+0x164:
779838d0 8b00 mov eax,dword ptr [eax]; eax = next Flink of InMemoryOrderModuleList
779838d2 8945e4 mov dword ptr [ebp-1Ch],eax
779838d5 ebce jmp ntdll!LdrpInitializeThread+0xd4 (779838a5)
大致可以看出,这个函数的一个重要工作是遍历InLoadOrderModuleList,调用每个模块的DllMain()函数。不妨再深入的看一下LdrpCallInitRoutine的代码。
ntdll!LdrpCallInitRoutine:
779789c4 55 push ebp
779789c5 8bec mov ebp,esp
779789c7 56 push esi
779789c8 57 push edi
779789c9 53 push ebx
779789ca 8bf4 mov esi,esp
779789cc ff7514 push dword ptr [ebp+14h]
779789cf ff7510 push dword ptr [ebp+10h]
779789d2 ff750c push dword ptr [ebp+0Ch]
779789d5 ff5508 call dword ptr [ebp+8]
它只是个壳而已,就是调用一下DllMain函数,而参数就是外面传进来的。传进来的参数是这样的。
7798388b 6a00 push 0
7798388d 6a02 push 2
7798388f ff7618 push dword ptr [esi+18h]; DllBase
结合DllMain的原型</>
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
<p">头文件中相关宏的定义如下,其中2是DLL_THREAD_ATTACH。</>
#define DLL_PROCESS_CREATE -1
/* WinNT compatible dll entry Op values */
#define DLL_PROCESS_ATTACH 1
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACH 3
#define DLL_PROCESS_DETACH 0
MSDN中有关DLLMain的帮助中对DLL_THREAD_ATTACH的描述如下。
The current process is creating a new thread. When this occurs, the system calls the entry-point function of all DLLs currently attached to the process. The call is made in the context of the new thread. DLLs can use this opportunity to initialize a TLS slot for the thread.
这样我们就理解了为什么在子线程中也需要进入关键区:对于每个新建的子线程,都需要根据PebLdr中的已加载模块列表遍历所有已经加载的模块,并以DLL_THREAD_ATTACH为参数调用每个模块的DLLMain()函数。所以在子线程初始化时也需要进入关键区。
下一节我们将继续深入分析DllMain()函数和PebLdr。