前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Windows中Loader Lock引起的死锁问题

Windows中Loader Lock引起的死锁问题

作者头像
河边一枝柳
发布2021-08-06 14:59:39
1.1K0
发布2021-08-06 14:59:39
举报

在程序构建中,往往让程序模块化。常见的就是实现为动态链接库。然后在主程序启动的时候隐式或者显示的去加载动态链接库。在Windows中,如果不恰当的编写动态链接库的DllMain函数,将会引起意想不到的Bug哦,比如典型的Loader Lock死锁问题,相信做过Windows开发的人不少碰到过这样的坑。

1. 背景介绍

当主程序在启动的时候,隐式或者显示的加载动态链接库的时候,调用动态链接库的DllMain,或者当创建线程的时候,线程启动过程中隐式的调用动态链接库的DllMain。然而为了多个线程顺序的调用DllMain,在微软内部在调用DllMain的时候使用了一个锁,叫做Loader Lock,这个锁作用于整个进程。

比如,当前程序中使用LoadLibrary第一次加载动态链接库,那么在调用动态链接库的时候,顺序如下:

既然有个隐藏的Loader Lock锁,那么在编写DllMain的时候就需要格外小心了,举一个Winodws核心编程书中的20.2.5节的一个死锁例子:

代码语言:javascript
复制
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 space
      hThread = CreateThread(NULL, 0, SomeFuction, NULL, 0, &dwThreadId);
      WaitForSingleObject(hThread, INFINITE);
      CloseHandle(hThread);
      break;
    case DLL_THREAD_ATTACH:
      // A thread is being created
      break;
    case DLL_THREAD_DETACH:
      // A thread is exiting cleanly
      break;
    case DLL_PROCESS_DETACH:
      // The DLL is being unmapped from the process' address space
      break;
  }
  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的主进程上,首先查看哪些正在被占用的锁:

代码语言:javascript
复制
0:019> !locks


CritSec ntdll!LdrpLoaderLock+0 at 0000000077d17490
WaiterWoken        No
LockCount          12
RecursionCount     1
OwningThread       cb0
EntryCount         0
ContentionCount    d
*** Locked

可以看到锁被线程cb0(16进制)所占用,并且从LockCount来看,还有很多线程再请求Loader Lock。先根据"!thread"命令获取占用Loader Lock线程cb0的顺序号为5 (下面只列出了6个线程,其实有几十个线程):

代码语言:javascript
复制
0:019> !threads
Index  TID      TEB        StackBase      StackLimit      DeAlloc      StackSize      ThreadProc
0  0000000000000d4c  0x000007fffffdd000  0x0000000000130000  0x0000000000126000  0x0000000000030000  0x000000000000a000  0x0
1  0000000000000fc0  0x000007fffffdb000  0x0000000002490000  0x000000000248e000  0x0000000002390000  0x0000000000002000  0x0
2  0000000000000968  0x000007fffffae000  0x0000000002cc0000  0x0000000002cbe000  0x0000000002bc0000  0x0000000000002000  0x0
3  0000000000000914  0x000007fffffac000  0x0000000002dc0000  0x0000000002dbe000  0x0000000002cc0000  0x0000000000002000  0x0
4  0000000000000de4  0x000007fffffaa000  0x0000000002ec0000  0x0000000002ebc000  0x0000000002dc0000  0x0000000000004000  0x0
5  0000000000000cb0  0x000007fffffa8000  0x0000000002fc0000  0x0000000002f9a000  0x0000000002ec0000  0x0000000000026000  0x0

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

代码语言:javascript
复制
0:019> ~5kv
Child-SP          RetAddr           : Args to Child                                                           : Call Site
00000000`02fbd558 000007fe`fdd81203 : 00000000`02fbd618 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtDelayExecution+0xa
00000000`02fbd560 00000000`63151a35 : 00000000`00000008 00000000`00000000 00000000`00000000 00000000`00000000 : KERNELBASE!SleepEx+0xab
00000000`02fbd600 00000000`6327299d : 00000000`00000000 00000000`00000000 00000000`00000010 00000000`002e2770 : xmodule3!DB_xxxxxxxxx+0x105
00000000`02fbd650 00000000`007fab85 : 00000000`00000001 00000000`00000004 00000000`00000268 00000000`02fbe3a8 : xmodule2!LM_xxxxx+0x18d
00000000`02fbe3e0 00000000`0082848d : 00000000`00000001 00000000`00000001 00000000`00000000 000012eb`e9b70b34 : xmodule1!ENG_xx+0x605
00000000`02fbee10 00000000`77c1b108 : 00000000`002cbb00 00000000`00000000 00000000`00000000 00000000`00297bf4 : xmodule1!ENG_xxx+0x2065d
00000000`02fbee50 00000000`77c0787a : 00000000`00000000 00000000`002cbb00 00000000`02fbef60 00000000`00000000 : ntdll!LdrpRunInitializeRoutines+0x1fe
00000000`02fbf020 00000000`77c07b5e : 00000000`00000000 00000000`0012fc38 00000000`02fbf2c0 000007fe`fdd8da2d : ntdll!LdrpLoadDll+0x231
00000000`02fbf230 000007fe`fdd89059 : 00000000`00000000 00000000`00000000 00000000`0012fc38 00000000`00000046 : ntdll!LdrLoadDll+0x9a
00000000`02fbf2a0 00000001`40003b05 : 00000000`00000000 00000000`0012fc38 00000001`4000e3d8 00000000`00000000 : KERNELBASE!LoadLibraryExW+0x22e
00000000`02fbf310 00000000`757237d7 : 00000000`0096d840 00000000`0096d840 00000000`00000000 00000000`00000000 : XXXSvc+0x3b05
00000000`02fbff00 00000000`75723894 : 00000000`757d95c0 00000000`0096d840 00000000`00000000 00000000`00000000 : MSVCR80!endthreadex+0x47
00000000`02fbff30 00000000`779d652d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : MSVCR80!endthreadex+0x104
00000000`02fbff60 00000000`77c0c541 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd
00000000`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。

代码语言:javascript
复制
0:019> !handle 17c f
Handle 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           <none>
  Object Specific Information
    Thread Id   5fc.890
    Priority    10
    Base Priority 0
    Start Address 75723810 MSVCR80!endthreadex

同之前的方法查看清理log的线程的函数栈,在"ntdll!RtlpWaitOnCriticalSection"中的参数"00000000`77d17490"刚好为Loader Lock。终于真想大白了~~~

代码语言:javascript
复制
0:019> ~6kv
Child-SP          RetAddr           : Args to Child                                                           : Call Site
00000000`0321f858 00000000`77c2e518 : 00000000`00000000 00000000`00000194 000007ff`fffa62c8 00000000`77c0c4fa : ntdll!ZwWaitForSingleObject+0xa
00000000`0321f860 00000000`77c2e40b : 00000000`00000001 000007ff`fffdf000 00000000`77be0000 00000000`77d17490 : ntdll!RtlpWaitOnCriticalSection+0xe8
00000000`0321f910 00000000`77c0c5dd : 00000000`00000000 000007ff`fffa6000 000007ff`fffa62c8 00000000`00000000 : ntdll!RtlEnterCriticalSection+0xd1
00000000`0321f940 00000000`77c0c44f : 000007ff`fffdf000 00000000`00000000 000007ff`fffa6000 00000000`00000000 : ntdll!LdrpInitializeThread+0x8d
00000000`0321fa40 00000000`77c0c34e : 00000000`0321fb00 00000000`00000000 000007ff`fffdf000 00000000`00000000 : ntdll!LdrpInitialize+0x9f
00000000`0321fab0 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!LdrInitializeThunk+0xe

在知道问题的根源后,解决这个问题也显得不是特别困难了。那么通过这个指导我们,尽量在DllMain中不要实现太多逻辑,可以使用一个导出函数,在加载动态链接库之后,手动的调用导出的初始化函数。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一个程序员的修炼之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档