前言
项目里的一个升级程序偶尔会死锁,查看dump后发现是死在了ShellExecuteExW里。经验少,不知道为什么,于是在高端调试论坛里发帖求助,链接如下http://advdbg.org/forums/6520/ShowPost.aspx
根据张银奎老师的描述可知,应该是拥有关键段的线程意外结束了。仔细检查项目中的代码,发现程序中有使用TerminateThread()来强制杀线程的代码。很可疑,于是写了一个测试程序,还原了这个问题。
这也是几年前在项目中遇到的一个问题,我对之前的笔记进行了整理重新发布于此。
问题重现
重现方法
主程序会加载一个DLL,并调用该DLL的导出函数创建一个线程,然后调用TerminateThread()强制杀死这个线程,然后调用RunProcess()(内部封装了对ShellExecuteEx()的调用)执行一个新进程,会卡死在ShellExecuteEx()。为了让问题更容易重现,特地在DllMain()的参数ul_reason_for_call为DLL_THREAD_DETACH时,强制睡眠了5秒。
代码摘录
主工程 testTerminateThread
1  | //testTerminateThread.cpp  | 
DLL工程 testDll
1  | // DllMain.cpp  | 
1  | // testDll.cpp  | 
问题分析
运行测试程序前先打开DbgView监视调试信息,然后运行测试程序。

从日志可知,我们启动的测试线程的线程id为0x1400。
当程序hang住后,使用windbg附加。附加成功后,先运行~*kvn查看线程及每个线程的的调用栈信息。发现只有一个0号线程(1号线程是windbg附加到进程时产生的)。
1  | 0:001> ~*kvn  | 
通过调用栈,我们发现程序卡在了ShellExecuteExW里。
运行!cs -l看下输出结果:
1  | 0:001> !cs -l  | 
注意OwningThread的值0x00001400 正是我们生成的测试线程,与我们在DbgView里看到的线程id一致。但是该线程已经被我们杀死了,它在被杀死前获得了进程加载锁0x77637340 (ntdll!LdrpLoaderLock+0x0)。
至此,真相大白。
总结
- 不要随便用
TerminateThread来强行杀死线程! windbg真是windows下的调试神器。!cs -l可以帮助我们快速的查找到死锁的关键段。
参考资料
- 《软件调试》
 - 《格蠹汇编》
 - 《windows核心编程(第 5 版)》尤其是第20章
 - Dynamic-Link Library Best Practices