[原]调试实战——使用windbg调试DLL卸载时的死锁

原调试debugwindbg死锁deadlock

前言

最近我们的程序在退出时会卡住,调查发现是在卸载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号线程是主线程(线程id1924),1号线程是子线程(线程id594),2号线程线程id1960)是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号线程在等线程id594的线程 。我们代码里确实有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号线程线程id0x00001924)拥有。而且这个死锁的关键段的成员LockSemaphore正是1号线程正在等待的句柄值。突然想起来《windows核心编程》上讲过关键段的结构,其中的LockSemaphoreEvent类型的,具体参考第八章8.4节。

至此,终于真相大白了,0号线程DllMain()内(ul_reason_for_callDLL_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)

原文地址:https://www.cnblogs.com/bianchengnan/p/12158745.html

时间: 2024-11-12 17:34:43

[原]调试实战——使用windbg调试DLL卸载时的死锁的相关文章

[原]调试实战——使用windbg调试TerminateThread导致的死锁

原调试debugwindbg死锁deadlock 前言 项目里的一个升级程序偶尔会死锁,查看dump后发现是死在了ShellExecuteExW里.经验少,不知道为什么,于是在高端调试论坛里发帖求助,链接如下http://advdbg.org/forums/6520/ShowPost.aspx 根据张银奎老师的描述可知,应该是拥有关键段的线程意外结束了.仔细检查项目中的代码,发现程序中有使用TerminateThread()来强制杀线程的代码.很可疑,于是写了一个测试程序,还原了这个问题. {%

WinDbg调试CPU占用高的问题 试验+实战 《第七篇》

一.High CPU试验 1.示例代码 static void Main(string[] args) { Console.Clear(); Console.WriteLine("到命令行下,切换到windbg目录,执行adplus -hang -pn highcpu.exe -o c:\\dumps"); Console.WriteLine("如果要停止,按Ctrl+C结束程序"); Console.WriteLine("================

Windbg调试命令详解

发表于2013 年 8 月 23 日由张佩 转载注明>> [作者:张佩][原文:http://www.yiiyee.cn/Blog] 1. 概述 用户成功安装微软Windows调试工具集后,能够在安装目录下发现四个调试器程序,分别是:cdb.exe.ntsd.exe.kd.exe和Windbg.exe.其中cdb.exe和ntsd.exe只能调试用户程序,Kd.exe主要用于内核调试,有时候也用于用户态调试,上述三者的一个共同特点是,都只有控制台界面,以命令行形式工作. Windbg.exe在

调试SQLSERVER (二)使用Windbg调试SQLSERVER的环境设置

调试SQLSERVER (二)使用Windbg调试SQLSERVER的环境设置 调试SQLSERVER (一)生成dump文件的方法调试SQLSERVER (三)使用Windbg调试SQLSERVER的一些命令 大家知道在Windows里面,调试可以分为两个领域: 1.内核态调试 2.用户态调试 一般的程序都是运行在用户态,包括SQLSERVER,SQLServer 会依赖于操作系统的Win32/Win64 API去调用I/O或者其他他需要的服务 用户态程序调试和内核态程序调试是不太一样的,即使

Windbg 调试 SOS 版本问题

程序发生了崩溃,我抓了一个mini Dump,Mini dump 有一个优点就是非常的小.比full dump 要小很多. 0:020> .loadby sos clr //首先加载sos 0:020> !threads The version of SOS does not match the version of CLR you are debugging. Please load the matching version of SOS for the version of CLR you

WinDbg 调试.net程序

WinDbg支持以下三种类型的命令: ·        常规命令,用来调试进程 ·        点命令,用来控制调试器 ·        扩展命令,可以添加叫WinDbg的自定义命令,一般由扩展dll提供这些命令 PDB文件 PDB文件是由链接器产生的程序数据库文件.私有PDB文件包含私有和公有符号,源代码行,类型,本地和全局变量信息.公有PDB文件不包含类型,本地变量和源代码行信息,且只包含共有成员的调试信息. Dump文件 利用Dump工具,你可以获得进程的快照信息.一个mini-dump

调试SQLSERVER (三)使用Windbg调试SQLSERVER的一些命令

调试SQLSERVER (三)使用Windbg调试SQLSERVER的一些命令 调试SQLSERVER (一)生成dump文件的方法调试SQLSERVER (二)使用Windbg调试SQLSERVER的环境设置 windbg命令分为标准命令.元命令.扩展命令 标准命令提供最基本的调试功能,不区分大小写.如:bp g dt dv k等 元命令提供标准命令没有提供的功能,也内建在调试引擎中,以.开头.如.sympath .reload等 扩展命令用于扩展某一方面的调试功能,实现在动态加载的扩展模块中

WinDbg调试高内存的.Net进程Dump

WinDbg的学习路径,艰难曲折,多次研究进展不多,今日有所进展,记录下来. 微软官方帮助文档非常全面:https://msdn.microsoft.com/zh-cn/library/windows/hardware/ff551063(v=vs.85).aspx 问题发现在服务器上,服务器为WinServer2012 R2 x64.其中一个Windows服务,内存高达7G.但此服务,无什么操作,仅仅定时获取数据,更新数据.使用的EntityFramework.用任务管理器,抓包下来,查看.Du

WinDbg调试.NET

WinDbg调试.NET程序入门 俗话说:万事开头难! 自从来到新公司遇到性能问题后,需要想办法解决这个问题,但是一直没有合适的性能分析工具,然后找到StevenChennet 大神帮忙,他用WinDbg工具远程帮我分析了一个 dump文件,但是只看到键盘 “啪啪啪”,得到了结果,却不是很清楚WinDbg神奇具体如何使用的.结果,第二天,性能问题又来了,总不能每次劳烦大神驾到,所以不得不自己开始学习WinDbg,这里记录一个入门过程. 1,首先,下载并安装WinDbg程序 从下面的地址打开:ht