DllMain()限入死锁问题分析 (二)

本文深入探讨了DllMain()函数中出现的死锁问题,通过调试工具分析了导致死锁的原因,涉及到PebLdr结构、模块加载列表以及线程初始化过程。文章揭示了主线程和子线程因访问PebLdr中的模块列表导致的并发冲突,解释了Ldr初始化线程和调用DllMain()函数的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前一篇文章《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。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值