调试DLL卸载时的死锁

转载原文:https://zhuanlan.zhihu.com/p/90591425
在使用Windbg实战的时候发现系统的pdb始终无法下载下来,发现是微软弃用了pdb从符号服务器下载的模式。具体怎么解决方案还没找到。不过windbg preview 的确很好用,集成了windbg的所有功能,界面也更友好一些。

Dll死锁根本原因是因为FreeLibrary的时候会触发DllMain的DLL_PROCESS_DETACH,而线程退出的时候会触发DllMain的DLL_THREAD_ATTACH,由于对DllMain()的调用需要序列化,需要等待0号线程释放锁后,其它线程才能调用。而0号线程又在无限等待1号线程结束,故死锁。
DLL_THREAD_DETACH:在线程退出的时候触发(退出的过程中触发,此时线程清理工作并没有结束,wait是不会受信的)
DLL_PROCESS_DETACH:Dll从进程地址空间退出的时候调用,FreeLibrary
但是DLL_PROCESS_DETACH和DLL_THREAD_DETAC的调用并没有先后顺序,但是调用是序列化的,也就是说只有当某一个调用完成之后才会调用另外一个。DLL_PROCESS_DETACH返回之后即模块调用结束,后续的DLL_THREAD_DETACH将不会收到,所以不能再DLL_PROCESS_DETACH里面等待线程结束,否则死锁。

在这里插入图片描述
前言
最近我们的程序在退出时会卡住,调查发现是在卸载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号线程是windbg插入的远程线程(线程id为1960)。

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核心编程》上讲过关键段的结构,其中的LockSemaphoreEvent类型的,具体参考第八章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调试关键段死锁,真香。

1. DLL_PROCESS_ATTACH:(主线程调用)
当DLL被进程 <<第一次>> 调用时,导致DllMain函数被调用,

同时ul_reason_for_call的值为DLL_PROCESS_ATTACH,

如果同一个进程后来再次调用此DLL时,操作系统只会增加DLL的使用次数,

不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。

2.DLL_PROCESS_DETACH:(主线程调用)
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的ul_reason_for_call值是DLL_PROCESS_DETACH。
★如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。

3.DLL_THREAD_ATTACH:(创建线程调用)
当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,

并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。

新创建的线程负责执行这次的DLL的DllMain函数,

只有当所有的DLL都处理完这一通知后,系统才允许线程开始执行它的线程函数。

4.DLL_THREAD_DETACH:(创建线程调用)
如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),

系统查看当前映射到进程空间中的所有DLL文件映像,

并用DLL_THREAD_DETACH来调用DllMain函数,

通知所有的DLL去执行线程级的清理工作。
★注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,
系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。

DllMain调用是多线程有时序的(同一时刻只能一个线程调用)

入口参数参考博客:https://www.cnblogs.com/jack-jia-moonew/p/4220696.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值