当互斥锁所有者线程死机时调试死锁?

逆向工程 艾达 风袋
2021-06-12 10:19:49

介绍

我正面临着前所未有的特殊僵局。

我试图从 3 天开始调试这个死锁,但找不到如何以正确的方式修复它。希望有人能帮助我。

我们没有应用程序的源代码,因此必须通过修补 asm 代码来修复此死锁(尽管修补二进制文件没有问题)。

场景

该应用程序是一个 x86 老式在线游戏,它使用 Directx 9 来渲染图形 (d3d9.dll)。根据我的分析,有 3 个线程与此死锁相关。要命名,线程 #1 是主线程(渲染每一帧),线程 #2 似乎是一个 d3d9 工作线程,它将一些异步资源加载排队等待线程 #1 处理和提取,然后我们有神秘的线程#3,它与游戏的一个特性有关,它基本上向服务器发出http请求并下载纹理以即时显示在游戏中;每次必须绘制此纹理时,主线程都会创建此线程 #3 来处理 I/O 以下载纹理,该 I/O 将传递给线程 #2(d3d9 工作线程),最终由线程进行异步处理#1(游戏循环)。

在线程#3 将纹理资源提供给线程#2 之后,线程#3 自行关闭。这个线程 #3 是从 crt!_beginthread 创建的,然后用 crt!_endthread 关闭。

僵局

总结:线程#1(游戏循环)、线程#2(d3d9工作线程)、线程#3(从http下载纹理)。

当(出于某种未知原因)线程 #3 结束时,没有释放线程 #2 正在等待的锁,就会出现问题。线程#1 反过来等待线程#2 获取要绘制的资源。

这与我遇到的所有其他死锁不同,因为当死锁发生时,似乎导致死锁的线程不再存在。我调试过的所有其他死锁都可以直接找到问题的根源,因为在隔离死锁中的线程时,我只需要分析每个线程的调用堆栈即可准确了解发生了什么。这里的大问题是,由于线程 #3 已死,我没有它的调用堆栈来查看它创建死锁的时刻。所以最大的问题是:如果我什至看不到调用堆栈,我怎么能找到这个线程 #3 中发生的事情?

发生死锁后的一些 WinDbg 分析输出

0:001> !locks

CritSec +935a060 at 0935a060
WaiterWoken        No
LockCount          2
RecursionCount     1
OwningThread       34fc
EntryCount         0
ContentionCount    21a
*** Locked

Scanned 27 critical sections

0:001> !失控

  User Mode Time   Thread       Time
   0:2888      0 days 0:00:15.234
  11:2210      0 days 0:00:02.796
  20:15f0      0 days 0:00:01.656
  17:584       0 days 0:00:00.453
  21:2860      0 days 0:00:00.140
  13:8cc       0 days 0:00:00.031
   7:1a70      0 days 0:00:00.031
  23:373c      0 days 0:00:00.015
  14:2fe0      0 days 0:00:00.015
  22:1bf4      0 days 0:00:00.000
  19:3a50      0 days 0:00:00.000
  18:2980      0 days 0:00:00.000
  16:1e0c      0 days 0:00:00.000
  15:2768      0 days 0:00:00.000
  12:3154      0 days 0:00:00.000
  10:2cfc      0 days 0:00:00.000
   9:1e40      0 days 0:00:00.000
   8:1ea8      0 days 0:00:00.000
   5:2b64      0 days 0:00:00.000
   4:338c      0 days 0:00:00.000
   1:3be8      0 days 0:00:00.000

0:001> ~0 KB

 # ChildEBP RetAddr  Args to Child              
00 0019f640 75f48869 000006c0 00000000 0019f688 ntdll!NtWaitForSingleObject+0xc
01 0019f6b4 75f487c2 000006c0 000003e8 00000000 KERNELBASE!WaitForSingleObjectEx+0x99
02 0019f6c8 68bbac92 000006c0 000003e8 0935a040 KERNELBASE!WaitForSingleObject+0x12
03 0019f6dc 68b7d6e4 88760870 0935a040 015c6e38 d3d9!CBatchFilterI::WaitForBatchWorkerThread+0x23
04 0019f6ec 68c403d1 04f0de60 68c403b0 c9e02d57 d3d9!CBatchFilterI::FlushBatchWorkerThread+0xc
05 0019f700 68b78522 0935a040 00000000 00011001 d3d9!CBatchFilterI::LHBatchFlush1+0x21
06 0019f718 68b99daa 04f0de60 68b54020 04f0dd00 d3d9!Flush+0x36
07 0019f9bc 68b6a661 04f0de60 04f0b634 04f08ac0 d3d9!DdBltLH+0x45d8a
08 0019fa94 68be9fcc 00000000 00000000 00000000 d3d9!CSwapChain::PresentMain+0x3a7
09 0019fabc 68be9e57 00000000 00000000 00000000 d3d9!CBaseDevice::PresentMain+0x68
0a 0019faf4 10109099 04f08ac0 00000000 00000000 d3d9!CBaseDevice::Present+0x57
0b 0019fc10 10107a15 04f08ac0 00000000 00000000 DoNPatch!fIDirect3Device9::fPresent+0x2e9
0c 0019fc58 005495e0 00000001 03440be8 03448a70 DoNPatch!NKD_IDirect3DDevice_Present+0x5
0d 0019fc7c 00549367 00000000 03440be8 00000000 SD_Asgard!loc_5494D7+0x109
0e 0019fcc4 0054b7a1 0105a000 03440be8 00b200b0 SD_Asgard!loc_549367
0f 0019fef4 005bb824 00400000 00000000 01503b2d SD_Asgard!loc_54B784+0x1d
10 0019ff80 76d28744 00302000 76d28720 34573170 SD_Asgard!loc_5BB812+0x12
11 0019ff94 76f8582d 00302000 03e96be8 00000000 KERNEL32!BaseThreadInitThunk+0x24
12 0019ffdc 76f857fd ffffffff 76fa6389 00000000 ntdll!__RtlUserThreadStart+0x2f
13 0019ffec 00000000 005cc46f 00302000 00000000 ntdll!_RtlUserThreadStart+0x1b

0:001> ~11 KB

 # ChildEBP RetAddr  Args to Child              
00 04c2fe58 76f4c07a 0935a064 00000000 00000000 ntdll!NtWaitForAlertByThreadId+0xc
01 04c2fe78 76f4bfbe 00000000 00000000 ffffffff ntdll!RtlpWaitOnAddressWithTimeout+0x33
02 04c2febc 76f4beb5 00000004 00000000 00000000 ntdll!RtlpWaitOnAddress+0xa5
03 04c2fefc 76f6b3f1 0935a040 0935a040 00000004 ntdll!RtlpWaitOnCriticalSection+0xb7
04 04c2ff1c 76f6b315 0935a040 04c2ff38 68b7d1e8 ntdll!RtlpEnterCriticalSectionContended+0xd1
05 04c2ff28 68b7d1e8 0935a060 0941a324 04c2ff60 ntdll!RtlEnterCriticalSection+0x45
06 04c2ff38 68b80753 00000001 0935a040 00000001 d3d9!CBatchFilterI::AcquireSynchronization+0x28
07 04c2ff60 68c42021 0941a320 00000001 68c41760 d3d9!CBatchFilterI::ProcessBatch+0x14b
08 04c2ff78 68c4176d 04c2ff94 76d28744 0935a040 d3d9!CBatchFilterI::WorkerThread+0x2d
09 04c2ff80 76d28744 0935a040 76d28720 308c3170 d3d9!CBatchFilterI::LHBatchWorkerThread+0xd
0a 04c2ff94 76f8582d 0935a040 07326be8 00000000 KERNEL32!BaseThreadInitThunk+0x24
0b 04c2ffdc 76f857fd ffffffff 76fa6389 00000000 ntdll!__RtlUserThreadStart+0x2f
0c 04c2ffec 00000000 68c41760 0935a040 00000000 ntdll!_RtlUserThreadStart+0x1b

结论

在上面的输出中,拥有锁定临界区 (34fc) 的线程 id 是线程 #3(发出 http 请求),它没有出现在 !runaway 列表中。在 !runaway 列表中,线程号 0 是 #1(游戏循环),线程号 11 是 #2(d3d9 批处理器)。如果您需要我可以收集的任何其他数据,请索取。在本次分析中,我使用了 IDA Pro 6.9 和 WinDbg,但我可以使用其他工具(如果可用)。

最后,对长篇大论深表歉意,并提前致谢。

1个回答

这比 RE 更像是 SW 开发问题,但您可以尝试使用!htrace来找出互斥锁最初分配的位置(创建堆栈跟踪)。

或者,尝试找出为什么线程 #3 不释放锁就退出。这可能有点棘手,但如果您可以重现这两个场景(释放和不释放锁),差异调试可能有助于找出代码路径的分歧点。