[原]调试实战——使用windbg调试DLL卸载时的死锁
前言
最近我们的程序在退出时会卡住,调查发现是在卸载dll
时死锁了。大概流程是这样的:我们的dll
在加载的时候会创建一个工作线程,在卸载的时候,会设置退出标志并等待之前开启的工作线程结束。为了研究这个经典的死锁问题,写了一个模拟程序,用到的dump
文件及示例代码参考附件。
{% note info %}
这也是几年前在项目中遇到的一个问题,我对之前的笔记进行了整理重新发布于此。
{% endnote %}
关键代码
主程序 WaitDllUnloadExe
//WaitDllUnloadExe.cpp
#include "stdafx.h"
#include "windows.h"
int _tmain(int argc, _TCHAR* argv[])
{
HMODULE module = LoadLibraryA(".\\DllUnload.dll");
Sleep(5000);
FreeLibrary(module);
return 0;
}
DLL程序 DllUnload
// dllmain.cpp
#include "stdafx.h"
#include "process.h"
HANDLE g_hThread;
bool g_quit = false;
unsigned __stdcall procThread(void *)
{
while ( !g_quit )
{
OutputDebugStringA("procThread running.\n");
Sleep(100);
}
OutputDebugStringA("==========================procThread quitting.\n");
return 0;
}
unsigned __stdcall quitDemoProc(void *)
{
int idx = 0;
while ( idx++ < 5 )
{
OutputDebugStringA("quitDemoProc running!!!!!!!!.\n");
Sleep(100);
}
OutputDebugStringA("--------------------------------------------------quitDemoProc quitting.\n");
return 0;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
g_hThread = (HANDLE)_beginthreadex(NULL, 0, &procThread, NULL, 0, NULL);
CloseHandle((HANDLE)_beginthreadex(NULL, 0, &quitDemoProc, NULL, 0, NULL));
}
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
{
OutputDebugStringA("------------DLL_THREAD_DETACH called.\n");
}
break;
case DLL_PROCESS_DETACH:
{
OutputDebugStringA("------------DLL_PROCESS_DETACH begin wait...\n");
g_quit = true;
WaitForSingleObject(g_hThread, INFINITE);
OutputDebugStringA("------------DLL_PROCESS_DETACH end wait...\n");
}
break;
}
return TRUE;
}
分析
使用windbg
打开dump
文件。然后使用~kvn
列出所有线程的调用栈。
0 Id: 1918.1924 Suspend: 1 Teb: 7efdd000 Unfrozen
# ChildEBP RetAddr Args to Child
00 004af6f4 76150816 00000038 00000000 00000000 ntdll!NtWaitForSingleObject+0x15 (FPO: [3,0,0])
01 004af760 76781194 00000038 ffffffff 00000000 KERNELBASE!WaitForSingleObjectEx+0x98 (FPO: [Non-Fpo])
02 004af778 76781148 00000038 ffffffff 00000000 kernel32!WaitForSingleObjectExImplementation+0x75 (FPO: [Non-Fpo])
*** WARNING: Unable to verify checksum for DllUnload.dll
03 004af78c 6d0c15eb 00000038 ffffffff 00000000 kernel32!WaitForSingleObject+0x12 (FPO: [Non-Fpo])
04 004af86c 6d0c1e2b 6d0b0000 00000000 00000000 DllUnload!DllMain+0xdb (FPO: [Non-Fpo]) (CONV: stdcall) [c:\users\bianchengnan\documents\visual studio 2012\projects\waitdllunloadexe\dllunload\dllmain.cpp @ 55]
05 004af8b0 6d0c1d4f 6d0b0000 00000000 00000000 DllUnload!__DllMainCRTStartup+0xcb (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt_bld\self_x86\crt\src\crtdll.c @ 508]
06 004af8c4 77139930 6d0b0000 00000000 00000000 DllUnload!_DllMainCRTStartup+0x1f (FPO: [Non-Fpo]) (CONV: stdcall) [f:\dd\vctools\crt_bld\self_x86\crt\src\crtdll.c @ 472]
07 004af8e4 77160000 6d0c10f0 6d0b0000 00000000 ntdll!LdrpCallInitRoutine+0x14
08 004af96c 77141221 6d0b0000 004af990 750227be ntdll!LdrpUnloadDll+0x375 (FPO: [Non-Fpo])
09 004af9b0 76151da7 6d0b0000 7efde000 004afaa4 ntdll!LdrUnloadDll+0x4a (FPO: [Non-Fpo])
*** WARNING: Unable to verify checksum for WaitDllUnloadExe.exe
0a 004af9c0 003a1425 6d0b0000 00000000 00000000 KERNELBASE!FreeLibrary+0x15 (FPO: [Non-Fpo])
0b 004afaa4 003a1989 00000001 0059a650 0059cf30 WaitDllUnloadExe!wmain+0x55 (FPO: [Non-Fpo]) (CONV: cdecl) [c:\users\bianchengnan\documents\visual studio 2012\projects\waitdllunloadexe\waitdllunloadexe\waitdllunloadexe.cpp @ 13]
0c 004afaf4 003a1b7d 004afb08 767833ca 7efde000 WaitDllUnloadExe!__tmainCRTStartup+0x199 (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 533]
0d 004afafc 767833ca 7efde000 004afb48 77139ed2 WaitDllUnloadExe!wmainCRTStartup+0xd (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 377]
0e 004afb08 77139ed2 7efde000 75022546 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
0f 004afb48 77139ea5 003a107d 7efde000 00000000 ntdll!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo])
10 004afb60 00000000 003a107d 7efde000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
1 Id: 1918.594 Suspend: 1 Teb: 7efda000 Unfrozen
# ChildEBP RetAddr Args to Child
00 0090fc68 77138dd4 00000040 00000000 00000000 ntdll!NtWaitForSingleObject+0x15 (FPO: [3,0,0])
01 0090fccc 77138cb8 00000000 00000000 0059c5b8 ntdll!RtlpWaitOnCriticalSection+0x13e (FPO: [Non-Fpo])
02 0090fcf4 7715d349 772020c0 75d82382 00000000 ntdll!RtlEnterCriticalSection+0x150 (FPO: [Non-Fpo])
03 0090fd8c 7715d5c2 00000000 00000000 0090fdac ntdll!LdrShutdownThread+0x50 (FPO: [Non-Fpo])
04 0090fd9c 0f78e099 00000000 0059ec48 0090fde8 ntdll!RtlExitUserThread+0x2a (FPO: [Non-Fpo])
05 0090fdac 0f78e007 00000000 d910e7ee 00000000 MSVCR110D!_endthreadex+0x39 (FPO: [Non-Fpo])
06 0090fde8 0f78e1d1 0059ec48 0090fe00 767833ca MSVCR110D!_beginthreadex+0x1a7 (FPO: [Non-Fpo])
07 0090fdf4 767833ca 0059ec48 0090fe40 77139ed2 MSVCR110D!_endthreadex+0x171 (FPO: [Non-Fpo])
08 0090fe00 77139ed2 0059c5b8 75d8204e 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
09 0090fe40 77139ea5 0f78e120 0059c5b8 00000000 ntdll!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo])
0a 0090fe58 00000000 0f78e120 0059c5b8 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
# 2 Id: 1918.1960 Suspend: 1 Teb: 7efd7000 Unfrozen
# ChildEBP RetAddr Args to Child
00 00a4f904 7719f826 75ec273a 00000000 00000000 ntdll!DbgBreakPoint (FPO: [0,0,0])
01 00a4f934 767833ca 00000000 00a4f980 77139ed2 ntdll!DbgUiRemoteBreakin+0x3c (FPO: [Non-Fpo])
02 00a4f940 77139ed2 00000000 75ec278e 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
03 00a4f980 77139ea5 7719f7ea 00000000 00000000 ntdll!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo])
04 00a4f998 00000000 7719f7ea 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
0号线程
是主线程(线程id
为1924
),1号线程
是子线程(线程id
为594
),2号线程
(线程id
为1960
)是windbg
插入的远程线程,用来中断到调试器。
0号线程
在调用WaitForSingleObject
时陷入了等待,我们来看它等什么。输入!handle 0x38 f
!handle 0x38 f
Handle 00000038
Type Thread
Attributes 0
GrantedAccess 0x1fffff:
Delete,ReadControl,WriteDac,WriteOwner,Synch
Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
HandleCount 5
PointerCount 8
Name <none>
Object specific information
Thread Id 1918.594
Priority 10
Base Priority 0
原来0号线程
在等线程id
为594
的线程 。我们代码里确实有WaitForSingleObject(g_hThread, INFINITE);
,我们再来看看1号线程
。从调用栈看来,1号线程
已经在调用_endthreadex()
准备关闭了,在关闭的过程中进入了一个关键段,并调用ntdll!NtWaitForSingleObject()
进入等待。等待的句柄为0x40
。输入!handle 0x40 f
查看句柄的相关信息。
0:002> !handle 0x40 f
Handle 00000040
Type Event
Attributes 0
GrantedAccess 0x100003:
Synch
QueryState,ModifyState
HandleCount 2
PointerCount 4
Name <none>
Object specific information
Event Type Auto Reset
Event is Waiting
我们发现句柄0x40
对应的对象是Event
,暂时先不管。使用万能死锁调试命令!cs -l
看看(因为从调用堆栈来看1号线程
是调用RtlEnterCriticalSection
而死锁的。)
0:002> !cs -l
-----------------------------------------
DebugInfo = 0x77204360
Critical section = 0x772020c0 (ntdll!LdrpLoaderLock+0x0)
LOCKED
LockCount = 0x1
WaiterWoken = No
OwningThread = 0x00001924
RecursionCount = 0x1
LockSemaphore = 0x40
SpinCount = 0x00000000
从输出结果可知,有一个锁住的关键段,被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号线程
结束,故死锁。
{% note warning %}
注意:即使在DllMain()
里调用DisableThreadLibraryCalls(hModule);
也不管用,具体参考《windows核心编程》中的相关分析。
{% endnote %}
在winnt.h
里找到了CriticalSection
的定义,如下
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
ULONG_PTR SpinCount; // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
总结
不要在
DllMain()
里等待线程结束。使用
!cs -l
调试关键段死锁,真香。
参考资料
- 《格蠹汇编》
- 《windows核心编程》第八章(
CriticalSection
相关知识,尤其是8.4.1
节) 第二十章(dll
相关知识,尤其是20.2.5
节)的相关内容。 - Dynamic-Link Library Best Practices (Windows)
[原]调试实战——使用windbg调试DLL卸载时的死锁的更多相关文章
- [原]调试实战——使用windbg调试崩溃在ole32!CStdMarshal::DisconnectSrvIPIDs
原调试debugwindbg崩溃crash 前言 最近程序会不定期崩溃,很是头疼!今晚终于忍无可忍,下决心要干掉它!从之前的几个相关的dump可以猜到是有接口未释放导致的问题,但没有确认到底是哪个接口 ...
- [原]调试实战——使用windbg调试TerminateThread导致的死锁
原调试debugwindbg死锁deadlock 前言 项目里的一个升级程序偶尔会死锁,查看dump后发现是死在了ShellExecuteExW里.经验少,不知道为什么,于是在高端调试论坛里发帖求助, ...
- [原]调试实战——使用windbg调试崩溃在ComFriendlyWaitMtaThreadProc
原调试debugwindbgcrash崩溃COM 前言 这是几年前在项目中遇到的一个崩溃问题,崩溃在了ComFriendlyWaitMtaThreadProc()里,没有源码.耗费了我很大精力,最终通 ...
- [原]调试实战——使用windbg调试excel启动时死锁
原调试debugwindbg死锁deadlock 前言 这是几年前在项目中遇到的一个死锁问题,在博客园发布过.我对之前的笔记进行了整理重新发布于此. 本文假设小伙伴们知道一些基本概念,比如什么是.du ...
- .NET高级调试系列-Windbg调试入门篇
Windbg是.NET高级调试领域中不可或缺的一个工具和利器,也是日常我们分析解决问题的必备.准备近期写2篇精华文章,集中给大家分享一下如果通过Windbg进行.NET高级调试. 今天我们来一篇入门的 ...
- (转)解决WinDbg调试Dump文件不同环境mscordacwks.dll版本问题
解决WinDbg调试Dump文件不同环境mscordacwks.dll版本问题 开发人员提交一个dump文件(Windows Server 2008 R2),当前调试环境Windows Serve ...
- Windbg调试命令详解
作者:张佩][原文:http://www.yiiyee.cn/Blog] 1. 概述 用户成功安装微软Windows调试工具集后,能够在安装目录下发现四个调试器程序,分别是:cdb.exe.ntsd. ...
- Windbg调试命令详解(1)
转载注明>> [作者:张佩][镜像:http://www.yiiyee.cn/Blog] 1. 概述 用户成功安装微软Windows调试工具集后,能够在安装目录下发现四个调试器程序,分别是 ...
- Windbg调试命令详解(2)
转载注明>> [作者:张佩][原文:http://blog.csdn.net/blog_index] 2. 符号与源码 符号与源码是调试过程中的重要因素,它们使得枯燥生硬的调试内容更容易 ...
随机推荐
- 实验吧-杂项-你知道他是谁吗?(转盘密码、NTFS数据流检测及导出)
刚看到的时候听懵,没注意到重点,其实很多时候题目中的细节就是给我们线索的,所以审题和思考是很重要的. 在没做到点上的是,也做了一点努力,没有效果,科普一下这个人((*^▽^*))图片上是托马斯.杰斐逊 ...
- 吴裕雄 Bootstrap 前端框架开发——Bootstrap 字体图标(Glyphicons):glyphicon glyphicon-list-alt
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name ...
- 吴裕雄 Bootstrap 前端框架开发——Bootstrap 字体图标(Glyphicons):glyphicon glyphicon-adjust
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name ...
- junit基础学习之-多线程测试(6)
步骤: 1.定义单个TestRunner 2.重载单个TestRunner的runTest() 3.定义TestRunner数组,并添加多个TestRunner 4.MultiThreadedTest ...
- Python 内置类型 dict, list,线程安全吗
近段时间发现一个 Python 连接数据库的连接是线程不安全的,结果惹得我哪哪儿都怀疑变量的多线程是否安全的问题,今天终于找到了正确答案,那就是 Python 内置类型 dict,list ,tupl ...
- 基于Ambari Server部署HDP集群实战案例
基于Ambari Server部署HDP集群实战案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.部署Ambari Server端 博主推荐阅读: https://www.c ...
- 洛谷 P1435 回文字串
题目传送门 解题思路: 就是求一个字符串的最长回文子序列的长度,然后用整个的长度减去最长回文子序列的长度 AC代码: #include<iostream> #include<cstd ...
- 面试官,不要再问我“Java虚拟机类加载机制”了(转载)
关于Java虚拟机类加载机制往往有两方面的 面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断 ...
- 春节宅家火了短视频,手游 APP 成最大赢家!
春节历来是APP运营者翘首以盼的火热期,但2020年的春节有些特殊, 新型冠状病毒的爆发,牵动着全国亿万人民的心.响应号召不出门,宅在家里玩手机,于是打游戏.看新闻.追剧等成为大家打发时间.疏解内心压 ...
- VS 2017 没有工具栏中没有Report Viewer的解决方案
安装 控件 Install-Package Microsoft.ReportingServices.ReportViewerControl.WinForms -Pre “工具”>“Nuget包管 ...