前言
最近我们的程序在退出时会卡住,调查发现是在卸载dll
时死锁了。大概流程是这样的:我们的dll
在加载的时候会创建一个工作线程,在卸载的时候,会设置退出标志并等待之前开启的工作线程结束。为了研究这个经典的死锁问题,写了一个模拟程序,用到的dump
文件及示例代码参考附件。
这也是几年前在项目中遇到的一个问题,我对之前的笔记进行了整理重新发布于此。
关键代码
主程序 WaitDllUnloadExe
1 | //WaitDllUnloadExe.cpp |
DLL程序 DllUnload
1 | // dllmain.cpp |
分析
使用windbg
打开dump
文件。然后使用~kvn
列出所有线程的调用栈。
1 | 0 Id: 1918.1924 Suspend: 1 Teb: 7efdd000 Unfrozen |
0号线程
是主线程(线程id
为1924
),1号线程
是子线程(线程id
为594
),2号线程
(线程id
为1960
)是windbg
插入的远程线程,用来中断到调试器。
0号线程
在调用WaitForSingleObject
时陷入了等待,我们来看它等什么。输入!handle 0x38 f
1 | !handle 0x38 f |
原来0号线程
在等线程id
为594
的线程 。我们代码里确实有WaitForSingleObject(g_hThread, INFINITE);
,我们再来看看1号线程
。从调用栈看来,1号线程
已经在调用_endthreadex()
准备关闭了,在关闭的过程中进入了一个关键段,并调用ntdll!NtWaitForSingleObject()
进入等待。等待的句柄为0x40
。输入!handle 0x40 f
查看句柄的相关信息。
1 | 0:002> !handle 0x40 f |
我们发现句柄0x40
对应的对象是Event
,暂时先不管。使用万能死锁调试命令!cs -l
看看(因为从调用堆栈来看1号线程
是调用RtlEnterCriticalSection
而死锁的。)
1 | 0:002> !cs -l |
从输出结果可知,有一个锁住的关键段,被0号线程
(线程id
为0x00001924
)拥有。而且这个死锁的关键段的成员LockSemaphore
正是1号线程
正在等待的句柄值。突然想起来《windows核心编程》上讲过关键段的结构,其中的LockSemaphore
为Event
类型的,具体参考第八章8.4节。
至此,终于真相大白了,0号线程
在DllMain()
内(ul_reason_for_call
为DLL_PROCESS_DETACH
)等待1号线程
结束,而1号线程
在结束的时候同样要调用DllMain()
,并且ul_reason_for_call
参数为DLL_THREAD_DETACH
。由于对DllMain()
的调用需要序列化,需要等待0号线程
释放锁后,其它线程才能调用。而0号线程
又在无限等待1号线程
结束,故死锁。
注意:即使在DllMain()
里调用DisableThreadLibraryCalls(hModule);
也不管用,具体参考《windows核心编程》中的相关分析。
在winnt.h
里找到了CriticalSection
的定义,如下
1 | typedef struct _RTL_CRITICAL_SECTION { |
总结
不要在
DllMain()
里等待线程结束。使用
!cs -l
调试关键段死锁,真香。
参考资料
- 《格蠹汇编》
- 《windows核心编程》第八章(
CriticalSection
相关知识,尤其是8.4.1
节) 第二十章(dll
相关知识,尤其是20.2.5
节)的相关内容。 - Dynamic-Link Library Best Practices (Windows)