Loader Lock引起的一个Bug
在Windows中,让程序模块化实现的一种方式,就是让事实上现为动态链接库。
然后在主程序启动的时候隐式或者显示的去载入动态链接库。可是假设不恰当的编写动态链接库的DllMain函数,将会引起意想不到的Bug哦。比方典型的Loader Lock死锁问题。
这不,我们产品中就碰到了一个因为Loader Lock而引起的Bug....
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 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;
}
从上述样例中能够看出,当DllMain收到DLL_PROCESS_ATTACH通知的时候会创建一个新的线程,系统用DLL_THREAD_ATTACH来再次通知新创建的线程调用DllMain。而之前的线程还在DllMain中还在等待新创建线程运行结束,但因为之前的线程又占有了Loader Lock,新创建的线程一直在等待Loader Lock。从而造成了死锁。
2. Windbg分析问题
在背景介绍中,明确了Loader Lock中会产生一些隐藏的Bug,那就让慎重编写DllMain吧。在实际产品中。碰到的问题的复杂度肯定是超过了以上的样例的。以下本人简化一下我们产品中出问题的逻辑:
在产品以Windows Server形式存在。在启动产品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的主进程上,设置好产品的symbols。首先查看哪些正在被占用的锁:
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个线程,事实上有几十个线程):
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分钟)。这个线程正在等待。
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 : SpntSvc+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。
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。最终真想大白了~~~
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中不要实现太多逻辑。能够使用一个剥离的导出函数,在载入动态链接库之后,手动的调用导出的初始化函数。
最后。推荐看看Microsoft的文档<<Dynamic-Link Library Best Practices>>.
Loader Lock引起的一个Bug的更多相关文章
- 泛型实现中没有正确lock引用类型的一个隐藏bug分析
最近看到这篇文章dotNetDR_的回复,让我想起一个真实发生的案例,下面就简单说说这个关于lock引用类型的一个不容易发现的隐藏缺陷. 某类库中的代码,封装了很简单的一个通用类,用于线程安全地执行某 ...
- 给JDK提的一个bug(关于AbstractQueuedSynchronizer.ConditionObject)
1. 背景 之前读JUC的AQS源码,读到Condition部分,我当时也写了一篇源码阅读文章--(AbstractQueuedSynchronizer源码解读--续篇之Condition)[http ...
- CreateTimerQueueTimer在DllMain中调用导致的loader lock
开发一个COM组件在Windows 7上注册成功,但是Windows XP SP3版本却导致regsvr32.exe进程挂起.用WinDbg查看发现提示: Break- seconds... WARN ...
- Tomcat一个BUG造成CLOSE_WAIT
之前应该提过,我们线上架构整体重新架设了,应用层面使用的是Spring Boot,前段日子因为一些第三方的原因,略有些匆忙的提前开始线上的内测了.然后运维发现了个问题,服务器的HTTPS端口有大量的C ...
- MySQL关于exists的一个bug
今天碰到一个很奇怪的问题,关于exists的, 第一个语句如下: SELECT ) FROM APPLY t WHERE EXISTS ( SELECT r.APPLY_ID FROM RECORD ...
- 由一个bug引发的SQLite缓存一致性探索
问题 我们在生产环境中使用SQLite时中发现建表报“table xxx already exists”错误,但DB文件中并没有该表.后面才发现这个是SQLite在实现过程中的一个bug,而这个bug ...
- Win10系统菜单打不开问题的解决,难道是Win10的一个Bug ?
Win10左下角菜单打不开,好痛苦,点击右下角的时间也没反应,各种不爽,折磨了我好几天,重装又不忍心,实在费劲,一堆开发环境要安装,上网找了很多方法都不适用.今天偶然解决了,仔细想了下,难道是Win1 ...
- 你可能不知道的 NaN 以及 underscore 1.8.3 _.isNaN 的一个 BUG
这篇文章并不在我的 underscore 源码解读计划中,直到 @pod4g 同学回复了我的 issue(详见 https://github.com/hanzichi/underscore-analy ...
- 标准模板库(STL)的一个 bug
今天敲代码的时候遇到 STL 的一个 bug,与 C++ 的类中的 const 成员变量有关.什么,明明提供了默认的构造函数和复制构造函数,竟然还要类提供赋值运算符重载.怎么会这样? 测试代码 Tes ...
随机推荐
- redis 中 set 和 hset 有什么不同,什么时候使用 hset 什么时候使用set?
转载:https://blog.csdn.net/wab719591157/article/details/73379844 redis 中存数据时,到底什么时候用 hset 相比于 set 存数据 ...
- 【jeecg-mybatis版本】 mybatis+spring mvc 完美整合方案 查询,保存,更新,删除自动生成
Jeecg-Mybatis版本代码生成器演示视频 http://pan.baidu.com/share/link?shareid=243717&uk=2668473880 简要说明 JE ...
- angularjs也支持script形式的template
<script type="text/ng-template" id="name"> https://docs.angularjs.org/api/ ...
- 【Python】文件读写操作
Python的文件读写有点类似php的文件读写.php的文件读写已经在<[php]让记事本成为你调控变量的控制台>(点击打开链接)说过了,以下用一个小样例说明Python的文件读写. 在F ...
- vue css 模块化编程 CSS Modules Scoped
1.scoped https://vue-loader.vuejs.org/zh/guide/scoped-css.html 2.module https://vue-loader.vuejs.org ...
- 对2个hex(16进制)字符串进行异或操作
private static String hexXOR(String hex1, String hex2){ BigInteger i1 = new BigInteger(hex1, 16); Bi ...
- How to get the value of a form element : check box and radio button
Getting a radio element and it’s checked value Radio buttons in a form can be grouped together using ...
- Centos7 安装 docker-ce
本文参考官网地址:https://docs.docker.com/install/linux/docker-ce/centos/#os-requirements 1.卸载旧版本的docker $ su ...
- Java中的Random()函数 【转载】
今天在做Java练习的时候注意到了Java里面的一个随机函数——Random,刚开始只是知道这个函数具有随机取值的作用,于是上网搜索了资料一番,做了一下一些关于Random函数的总结: J ...
- vs code 问题:preLaunchTask“build”已终止,退出代码为 1。解决办法
菜单:任务-配置任务 改为如下: { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation ab ...