在程序构建中,往往让程序模块化。常见的就是实现为动态链接库。然后在主程序启动的时候隐式或者显示的去加载动态链接库。在Windows中,如果不恰当的编写动态链接库的DllMain函数,将会引起意想不到的Bug哦,比如典型的Loader Lock死锁问题,相信做过Windows开发的人不少碰到过这样的坑。
1. 背景介绍
当主程序在启动的时候,隐式或者显示的加载动态链接库的时候,调用动态链接库的DllMain,或者当创建线程的时候,线程启动过程中隐式的调用动态链接库的DllMain。然而为了多个线程顺序的调用DllMain,在微软内部在调用DllMain的时候使用了一个锁,叫做Loader Lock,这个锁作用于整个进程。
比如,当前程序中使用LoadLibrary第一次加载动态链接库,那么在调用动态链接库的时候,顺序如下:

既然有个隐藏的Loader Lock锁,那么在编写DllMain的时候就需要格外小心了,举一个Winodws核心编程书中的20.2.5节的一个死锁例子:
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad){HANDLE hThread;DWORD dwThreadId;switch (fdwReason){case DLL_PROCESS_ATTACH:// The DLL is being mapped into the process' address spacehThread = CreateThread(NULL, 0, SomeFuction, NULL, 0, &dwThreadId);WaitForSingleObject(hThread, INFINITE);CloseHandle(hThread);break;case DLL_THREAD_ATTACH:// A thread is being createdbreak;case DLL_THREAD_DETACH:// A thread is exiting cleanlybreak;case DLL_PROCESS_DETACH:// The DLL is being unmapped from the process' address spacebreak;}return TRUE;}
从上述例子中可以看出, 当这个DLL库的DllMain首先收到DLL_PROCESS_ATTACH通知,此时会创建一个新的线程,系统用DLL_THREAD_ATTACH来再次通知新创建的线程调用DllMain。而之前的线程还在DllMain中还在等待新创建线程执行结束,但由于之前的线程又占有了Loader Lock,新创建的线程一直在等待Loader Lock,从而造成了死锁。
2. Windbg分析问题
在背景介绍中,明白了Loader Lock中会产生一些隐藏的Bug,那就让谨慎编写DllMain吧。而实际项目比上述的例子可能会复杂一些,但在理解了其原理后,对问题的分析也会更加接近真像了。下面本人简化一下一个实际项目中出问题的逻辑:

产品以Windows Service形式存在,在启动产品Service的时候,将先加载A.dll,而A.dll的DllMain中将会创建一个线程Thread2(如果这个线程在接收到清除Log的Event后,将会对Log进行清除)。接着加载B.dll,在B.dll的DllMain中,将会去检查log文件,如果其大于10M,则通知Thread2去清理log,并且等待Thread2将log清理完成(最多等待5分钟)。但是当log大于10M的时候,启动Service有时候会出现启动超时的情况。
于是用Windbg Attach到hang的主进程上,首先查看哪些正在被占用的锁:
0:019> !locksCritSec ntdll!LdrpLoaderLock+0 at 0000000077d17490WaiterWoken NoLockCount 12RecursionCount 1OwningThread cb0EntryCount 0ContentionCount d*** Locked
可以看到锁被线程cb0(16进制)所占用,并且从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的函数调用栈,其hang在xmodule3模块的DB_xxxxxxxxx函数中,这个函数中就是之前提到的,通知清理的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的线程的handle为"17c" (16进制)。查看其线程Id为5fc.890。
0:019> !handle 17c fHandle 17cType ThreadAttributes 0GrantedAccess 0x1fffff:Delete,ReadControl,WriteDac,WriteOwner,SynchTerminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonateHandleCount 4PointerCount 6Name <none>Object Specific InformationThread Id 5fc.890Priority 10Base Priority 0Start 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中不要实现太多逻辑,可以使用一个导出函数,在加载动态链接库之后,手动的调用导出的初始化函数。
最后,推荐看看Microsoft的文档:<<Dynamic-Link Library Best Practices>>.




