Windows中Loader Lock引起的死锁问题

在程序开发中,常见的做法是将程序模块化,通常实现为动态链接库(dll)。在主程序启动时,可以通过隐式或显式的方式加载这些动态链接库。然而,在windows系统中,如果动态链接库的dllmain函数编写不当,可能会导致一些意想不到的bug,例如典型的loader lock死锁问题。这是一个许多windows开发者可能遇到的陷阱。

背景介绍

当主程序启动时,无论是通过隐式还是显式的方式加载动态链接库,都会调用动态链接库的DllMain函数。同样,当创建新线程时,线程启动过程中也会隐式地调用DllMain。为了确保多个线程按顺序调用DllMain,微软在内部使用了一个称为Loader Lock的锁,这个锁作用于整个进程。

例如,当使用LoadLibrary首次加载动态链接库时,调用顺序如下:

Windows中Loader Lock引起的死锁问题

由于存在这个隐式的Loader Lock,在编写DllMain时需要特别小心。以下是《Windows核心编程》书中20.2.5节的一个死锁示例:

BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad){  HANDLE hThread;  DWORD dwThreadId;  switch (fdwReason){    case DLL_PROCESS_ATTACH:      // DLL被映射到进程的地址空间      hThread = CreateThread(NULL, 0, SomeFuction, NULL, 0, &dwThreadId);      WaitForSingleObject(hThread, INFINITE);      CloseHandle(hThread);      break;    case DLL_THREAD_ATTACH:      // 创建新线程      break;    case DLL_THREAD_DETACH:      // 线程正常退出      break;    case DLL_PROCESS_DETACH:      // DLL从进程的地址空间中卸载      break;  }  return TRUE;}

从上述示例可以看出,当DLL接收到DLL_PROCESS_ATTACH通知时,会创建一个新线程。系统会用DLL_THREAD_ATTACH通知新创建的线程调用DllMain。然而,由于之前的线程还在DllMain中等待新创建的线程执行完毕,并且占用了Loader Lock,新创建的线程将无法获得Loader Lock,从而导致死锁。

Windbg分析问题

在背景介绍中,我们了解到Loader Lock可能会导致一些隐含的Bug,因此在编写DllMain时需要格外小心。实际项目中的情况可能比上述示例更为复杂,但理解了原理后,问题的分析会更接近真相。以下是简化的一个实际项目中出现问题的逻辑:

Windows中Loader Lock引起的死锁问题

产品以Windows服务的形式存在,启动服务时首先加载A.dll。在A.dll的DllMain中创建一个线程Thread2(如果该线程接收到清理Log的Event,将会对Log进行清理)。然后加载B.dll,在B.dll的DllMain中检查log文件,如果其大小超过10M,则通知Thread2清理log,并等待Thread2完成清理(最多等待5分钟)。然而,当log文件超过10M时,启动服务有时会出现启动超时的情况。

于是,我们使用Windbg附加到hang的主进程上,首先查看哪些锁被占用:

0:019> !locksCritSec ntdll!LdrpLoaderLock+0 at 0000000077d17490WaiterWoken        NoLockCount          12RecursionCount     1OwningThread       cb0EntryCount         0ContentionCount    d*** Locked

可以看到,锁被线程cb0(十六进制)占用,并且从LockCount来看,还有许多线程在请求Loader Lock。使用”!thread”命令获取占用Loader Lock的线程cb0的顺序号为5(下面只列出了6个线程,实际上有几十个线程):

0:019> !threadsIndex  TID      TEB        StackBase      StackLimit      DeAlloc      StackSize      ThreadProc0  0000000000000d4c  0x000007fffffdd000  0x0000000000130000  0x0000000000126000  0x0000000000030000  0x000000000000a000  0x01  0000000000000fc0  0x000007fffffdb000  0x0000000002490000  0x000000000248e000  0x0000000002390000  0x0000000000002000  0x02  0000000000000968  0x000007fffffae000  0x0000000002cc0000  0x0000000002cbe000  0x0000000002bc0000  0x0000000000002000  0x03  0000000000000914  0x000007fffffac000  0x0000000002dc0000  0x0000000002dbe000  0x0000000002cc0000  0x0000000000002000  0x04  0000000000000de4  0x000007fffffaa000  0x0000000002ec0000  0x0000000002ebc000  0x0000000002dc0000  0x0000000000004000  0x05  0000000000000cb0  0x000007fffffa8000  0x0000000002fc0000  0x0000000002f9a000  0x0000000002ec0000  0x0000000000026000  0x0

然后查看线程cb0的函数调用栈,它在xmodule3模块的DB_xxxxxxxxx函数中hang。这个函数就是之前提到的,通知清理log的线程并等待其完成清理(最多等待5分钟),这个线程正在等待。

0:019> ~5kvChild-SP          RetAddr           : Args to Child                                                           : Call Site00000000`02fbd558 000007fe`fdd81203 : 00000000`02fbd618 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtDelayExecution+0xa00000000`02fbd560 00000000`63151a35 : 00000000`00000008 00000000`00000000 00000000`00000000 00000000`00000000 : KERNELBASE!SleepEx+0xab00000000`02fbd600 00000000`6327299d : 00000000`00000000 00000000`00000000 00000000`00000010 00000000`002e2770 : xmodule3!DB_xxxxxxxxx+0x10500000000`02fbd650 00000000`007fab85 : 00000000`00000001 00000000`00000004 00000000`00000268 00000000`02fbe3a8 : xmodule2!LM_xxxxx+0x18d00000000`02fbe3e0 00000000`0082848d : 00000000`00000001 00000000`00000001 00000000`00000000 000012eb`e9b70b34 : xmodule1!ENG_xx+0x60500000000`02fbee10 00000000`77c1b108 : 00000000`002cbb00 00000000`00000000 00000000`00000000 00000000`00297bf4 : xmodule1!ENG_xxx+0x2065d00000000`02fbee50 00000000`77c0787a : 00000000`00000000 00000000`002cbb00 00000000`02fbef60 00000000`00000000 : ntdll!LdrpRunInitializeRoutines+0x1fe00000000`02fbf020 00000000`77c07b5e : 00000000`00000000 00000000`0012fc38 00000000`02fbf2c0 000007fe`fdd8da2d : ntdll!LdrpLoadDll+0x23100000000`02fbf230 000007fe`fdd89059 : 00000000`00000000 00000000`00000000 00000000`0012fc38 00000000`00000046 : ntdll!LdrLoadDll+0x9a00000000`02fbf2a0 00000001`40003b05 : 00000000`00000000 00000000`0012fc38 00000001`4000e3d8 00000000`00000000 : KERNELBASE!LoadLibraryExW+0x22e00000000`02fbf310 00000000`757237d7 : 00000000`0096d840 00000000`0096d840 00000000`00000000 00000000`00000000 : XXXSvc+0x3b0500000000`02fbff00 00000000`75723894 : 00000000`757d95c0 00000000`0096d840 00000000`00000000 00000000`00000000 : MSVCR80!endthreadex+0x4700000000`02fbff30 00000000`779d652d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : MSVCR80!endthreadex+0x10400000000`02fbff60 00000000`77c0c541 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd00000000`02fbff90 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d

从上述信息可以看出,线程cb0一直在等待清理log的线程完成清理。那么,清理log的线程到底发生了什么呢?首先,log中记录了清理log线程的句柄为”17c”(十六进制)。查看其线程Id为5fc.890。

0:019> !handle 17c fHandle 17c  Type           Thread  Attributes     0  GrantedAccess  0x1fffff:         Delete,ReadControl,WriteDac,WriteOwner,Synch         Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate  HandleCount    4  PointerCount   6  Name             Object Specific Information    Thread Id   5fc.890    Priority    10    Base Priority 0    Start Address 75723810 MSVCR80!endthreadex

使用相同的方法查看清理log线程的函数调用栈,发现它在”ntdll!RtlpWaitOnCriticalSection”中,参数”00000000`77d17490″恰好是Loader Lock。终于真相大白了~~

0:019> ~6kvChild-SP          RetAddr           : Args to Child                                                           : Call Site00000000`0321f858 00000000`77c2e518 : 00000000`00000000 00000000`00000194 000007ff`fffa62c8 00000000`77c0c4fa : ntdll!ZwWaitForSingleObject+0xa00000000`0321f860 00000000`77c2e40b : 00000000`00000001 000007ff`fffdf000 00000000`77be0000 00000000`77d17490 : ntdll!RtlpWaitOnCriticalSection+0xe800000000`0321f910 00000000`77c0c5dd : 00000000`00000000 000007ff`fffa6000 000007ff`fffa62c8 00000000`00000000 : ntdll!RtlEnterCriticalSection+0xd100000000`0321f940 00000000`77c0c44f : 000007ff`fffdf000 00000000`00000000 000007ff`fffa6000 00000000`00000000 : ntdll!LdrpInitializeThread+0x8d00000000`0321fa40 00000000`77c0c34e : 00000000`0321fb00 00000000`00000000 000007ff`fffdf000 00000000`00000000 : ntdll!LdrpInitialize+0x9f00000000`0321fab0 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!LdrInitializeThunk+0xe

了解问题的根源后,解决这个问题也变得相对简单。通过这个案例,我们得到的启示是,尽量避免在DllMain中实现过多的逻辑。可以使用一个导出函数,在加载动态链接库之后,手动调用这个导出的初始化函数。

以上就是Windows中Loader Lock引起的死锁问题的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/165474.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
如何开发Workerman插件?
上一篇 2026年8月26日 20:09:35
下一篇 2026年8月26日 20:13:33

相关推荐

发表回复

登录后才能评论
关注微信