首页
社区
课程
招聘
[原创]调试DLL卸载时的死锁
发表于: 2019-11-10 22:18 3045

[原创]调试DLL卸载时的死锁

2019-11-10 22:18
3045

前言

最近我们的程序在退出时会卡住,调查发现是在卸载dll时死锁了。大概流程是这样的:我们的dll在加载的时候会创建一个工作线程,在卸载的时候,会设置退出标志并等待之前开启的工作线程结束。为了研究这个经典的死锁问题,写了一个模拟程序,用到的dump文件及示例代码参考附件。

这也是几年前在项目中遇到的一个问题,我对之前的笔记进行了整理重新发布于此。

关键代码

主程序 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号线程结束,故死锁。

注意:即使在DllMain()里调用DisableThreadLibraryCalls(hModule);也不管用,具体参考《windows核心编程》中的相关分析。

在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



如果您喜欢此类文章,还想阅读更多,请关注我的微信公众号:编程难


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 1
支持
分享
最新回复 (4)
雪    币: 144
活跃值: (38)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
这个死锁很早在开发dll时就发现了。后来我看dllmain的定义,我就明白了,其实就是一个逻辑悖论。dllmain触发的条件包含进程或者线程退出事件,每当新的线程创建或者结束,都会调用dllmain函数。如果你在dllmain函数等待创建的线程结束,那么另外一个线程就会触发dllmain函数代码,但是之前的那个被阻塞了。dllmain函数的特点是不可重入,也就是之前触发的dllmain事件执行完毕后,才能执行下一个dllmain函数事件。dllmain函数本身就是一个锁。在这个锁被打开之前,是不能重入的。这其实与windows的调试设计有关。在debug模式的时候,线程创建,线程载入,模块创建,模块载入,这些事件处理在同一单元。
2019-11-10 23:25
0
雪    币: 144
活跃值: (38)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
准确的表述为,在dll入口函数执行返回前(进程创建或者结束,线程创建或者接受),不能用于等待其他线程结束。
2019-11-10 23:29
0
雪    币: 4709
活跃值: (1575)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
4
学习,mark
2019-11-11 01:59
0
雪    币: 8519
活跃值: (9122)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
5
zylyy 准确的表述为,在dll入口函数执行返回前(进程创建或者结束,线程创建或者接受),不能用于等待其他线程结束。
对,操作系统有个LoaderLock
2019-11-11 12:35
0
游客
登录 | 注册 方可回帖
返回
//