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>>.

时间: 2024-12-31 00:04:56

Loader Lock引起的一个Bug的相关文章

Ibatis2.3.4的一个bug

java.lang.ClassCastException: com.chat.upgrade.domain.ClientFile cannot be cast to java.lang.String 今天查一个对象转化成json串报错的问题,查了两个小时,最后问题的根源居然是ibatis. ibatis的语句如下: <typeAlias alias="Client" type="com.chat.upgrade.domain.ClientFile"/>

Win10系统菜单打不开问题的解决,难道是Win10的一个Bug ?

Win10左下角菜单打不开,好痛苦,点击右下角的时间也没反应,各种不爽,折磨了我好几天,重装又不忍心,实在费劲,一堆开发环境要安装,上网找了很多方法都不适用.今天偶然解决了,仔细想了下,难道是Win10的一个Bug? 1.问题和现象 右下角菜单点不开,下面的状态栏的右键也没有反应.时间日期也点不开,音频喇叭同样点不开....各种烦人,百度一堆都无果.... 说明:Win10是正式版,已激活:杀毒也全盘扫描过,因为电脑是开发和办公用,几乎不上其他网站,所以中毒的可能性几乎为0. 2.解决方法 晚上

Universal-Image-Loader的一个BUG

使用UIL的内置圆角图片的功能时,发现一个BUG,就是它会拉伸图片,造成图片失真.费了一下午的功夫,重写了RoundedBitmapDisplayer,总算解决这个问题. 代码如下: public class RoundedBitmapDisplayer implements BitmapDisplayer { protected final int cornerRadius; protected final int margin; public RoundedBitmapDisplayer(i

ubuntu12.04 software-center 的一个BUG

ubuntu software-center 软件中心今天突然发现打不开了,就是在启动的过程中启动一半就退出了,多次启动无果.首先想到的办法当然是最彻底的两句话 sudo apt-get purge software-center sudo apt-get install software-center 结果未果,启动起来还是首先一个窗体初始化 接着..就直接关闭了.然后查看它的输出信息,发现原来是py输出中文导致的,因为我们窗体上有很多中文字体的组件需要加载,而python处理的时候有一个使用

docker 1.0.0发布以及一个bug依赖apparmor_parser

6月10号docker 1.0稳定版本发布,找了台ubuntu的机器,装了下 ubuntu version:12.04 docker version:1.0.0 装docker的步骤可以看官方文档:https://docs.docker.com/installation/ubuntulinux/ 装好之后,运行docker -d尝试启动docker守护进程,报错如下: [0fcb4ed6] +job serveapi(tcp://127.0.0.1:2375) [0fcb4ed6] +job i

【Qt】无边框窗体中带有ActiveX组件时的一个BUG

无意中发现的一个BUG,Qt5.1.1正式版首先创建一个GUI工程,拖入一个QAxWidget控件(为了使ActiveX生效,需要在.pro文件中加入CONFIG += qaxcontainer)接着,为了让ActiveX有效,需要引入一个组件,我这里引入的是IE组件 [cpp] view plain copy MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->s

使用getDrawable时遇到的一个bug

做一个筛选菜单时候,用到了dongjunkun的DropDownMenu,github地址:https://github.com/dongjunkun/DropDownMenu 遇到几个问题: (1)最右面的上三角形.下三角形很难看,需要改成向上箭头向下箭头,而且靠近文件,在右边: (2)背景颜色需要改成白色: (3)下面的子菜单的文字在最左边,需要居中: (4)第一次进来Fragment的时候DropDownMenu的下拉选项没有选中任意一项 上面几个需求看起来很容易改,不就是改改布局什么的,

memory_limit的一个bug | 风雪之隅

原文:memory_limit的一个bug | 风雪之隅 27 Nov 09 memory_limit的一个bug 作者: Laruence( ) 本文地址: http://www.laruence.com/2009/11/27/1164.html 转载请注明出处 PHP 5.2x中, 由于错误的选用了zend_atoi, 导致memory_limit不能设置为超过4G的值. 今天同事分享给我一个问题(thans to yanmi), 一段代码(PHP 5.2.11 Linux/X86_64),

记用ajax的一个bug

遇到一个bug,IE8,只有第一次功能正确,后面都不生效.我看了一下,确实IE存在问题,调试发现根本不进入后台,于是我猜测是缓存的问题,于是加上cache:false,解决了问题. $.ajax( { url: "ParaSetting/OpenOrCloseParam", data: { Id: id }, cache:false, type: "get", success: function (data) { alert(data); if (data ===