在程序开发中,常见的做法是将程序模块化,通常实现为动态链接库(dll)。在主程序启动时,可以通过隐式或显式的方式加载这些动态链接库。然而,在windows系统中,如果动态链接库的dllmain函数编写不当,可能会导致一些意想不到的bug,例如典型的loader lock死锁问题。这是一个许多windows开发者可能遇到的陷阱。
背景介绍
当主程序启动时,无论是通过隐式还是显式的方式加载动态链接库,都会调用动态链接库的DllMain函数。同样,当创建新线程时,线程启动过程中也会隐式地调用DllMain。为了确保多个线程按顺序调用DllMain,微软在内部使用了一个称为Loader Lock的锁,这个锁作用于整个进程。
例如,当使用LoadLibrary首次加载动态链接库时,调用顺序如下:

由于存在这个隐式的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服务的形式存在,启动服务时首先加载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
微信扫一扫
支付宝扫一扫