前言
项目里的一个升级程序偶尔会死锁,查看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