前言
最近我们的程序在退出时会卡住,调查发现是在卸载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)