65.9K
CodeProject 正在变化。 阅读更多。
Home

API 挂钩揭秘第 3 部分和第 4 部分 - 线程死锁检测器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (16投票s)

2005 年 2 月 7 日

7分钟阅读

viewsIcon

90597

downloadIcon

3111

关于 API Hooking 的最后一篇文章,其中包含一个可工作的线程死锁检测器示例。

引言

这是构建线程死锁检测器的第三、第四(也是最后)部分。请参阅前两篇文章以了解正在发生的事情,地址为 API Hooking 的一个(可工作的)实现(第二部分)

事实上,我添加了一个名为 SetThreadName 的小型库,它允许您设置线程名称并获取同步对象的有意义的名称。该库在 Release 版本中不做任何事情,您甚至不需要它。

备注:即使没有这个 DLL,此软件也能正常工作,您无需重新编译任何内容,除非您希望您的线程和对象具有名称。

线程命名技巧允许在 Visual Debugger 中支持线程名称(在 Debug\Thread 菜单下,显示的是实际名称而不是 0x0000C340),因此如果您使用它,即使您不使用我的软件,也能带来 100% 的好处。对于自动对象命名,使用了以下算法:

  • 第一是进程时钟值,这样创建对象的函数仍然会创建不同的对象名称。
  • 第二是进程 ID,因此您可以多次运行应用程序,它仍然会有一个唯一的名称。
  • 第三个字符串是对象类型。(Mutex、Event、Semaphore 等)
  • 第四个字符串是创建对象的文件的名称。
  • 第五个数字是创建对象的行号。

第三部分:线程死锁检测器

背景

如果您已阅读我的前几篇文章,那么您应该知道什么是 API Hooking,以及如何使用它来监视任何应用程序中发生的情况(第一部分)。您还应该知道对于线程死锁检测器,我们关心什么,以及获取所有必需信息的技巧(第二部分)。

想法

现在我们可以获取对任何同步和线程函数的每次调用,我们面临一个问题。我们如何知道目标应用程序何时死锁或未死锁?我在这里使用的算法相当简单。这是一个死锁的例子

Let's take 2 threads (A and B), and 2 objects (o0 and o1).
Thread B locks o0, and then locks o1.
In parallel, Thread A locks o1, and then locks o0.

If o0 and o1 are free (ready to be locked), then they are 4 possibles cases:
 1) Thread B is executed, and not interrupted, then thread A is executed.
 2) Thread A is executed, and not interrupted, then thread B is executed.
 3) Thread B is executed, but gets interrupted before locking o1, then thread A 
    is executed, and waits for o0. Thread B is then executed, and waits for o1.
 4) Thread A is executed, but gets interrupted before locking o0, then thread B 
    is executed, locks o0 and waits for o1. Thread A is then executed, 
    and waits for o0.

很明显,情况 1 和 2 是没问题的。然而,当达到情况 3 或 4 时,应用程序就会死锁。为了避免这种情况,服务器(死锁检测器)使用其 CSyncObjectCThread 类来监视每个对象和线程。然后,每个对象都会跟踪谁拥有它(线程列表)。类似地,每个线程都有一个等待对象列表和锁定对象列表。现在让我们看看算法如何找到死锁。

    Case 3:
Time 
  0          Thread B locks o0      (o0 now has Thread B in its list
                                     and Thread B have o0 in its Locked list)
  1          Thread B is interrupted
  2          Thread A locks o1      (o1 now has Thread A in its list
                                     and Thread A have o1 in its Locked list)
  3          Thread A tries to lock o0
               (as o0 is already locked, we look inside it to find who got it.
                we find Thread B, so then we check if thread B is waiting for
                any object current thread (thread A) may have. In that case,
                the waiting list of thread B is empty, so we add o0 to our 
                waiting list)
  4          Thread A is interrupted
  5          Thread B tries to lock o1
               (as o1 is already locked, we look inside it to find who'got it.
                we find Thread A, so then we check if thread A is waiting for
                any object current thread (thread B) may have. Thread A is 
                waiting for o0 but o0 is in our locked list => deadlock)

该算法相当简单但开箱即用。

实现

我们需要一个名为 ThreadDLD 的服务器(多么美妙的名字,不是吗?)。它的目的是:

  • 启动被调试进程(可以是任何应用程序,有或没有源代码)。
  • 将其注入间谍 DLL,并使其感染被调试进程。
  • 接收线程监控函数。
  • 接收来自客户端的任何 API 嗅探。
  • 解析嗅探,并将其显示为日志。
  • 分析嗅探并发现错误(死锁)。

被调试进程在 CMainFrame::OnFileOpen 中以挂起状态启动。然后使用常规的 CreateRemoteThread 技巧将间谍 DLL ThreadSpy.DLL 注入其中。然后恢复被调试进程,服务器等待来自它的任何消息。然后,被调试进程发送带有线程监控函数地址的 StartMeUp 命令,并将任何被挂钩的命令(带有堆栈跟踪和时间戳)发送到服务器。服务器在 CThreadDLDView::ReceivedMessage 中等待来自被调试进程的任何 CommunicationObject。然后服务器解析消息并相应地记录。有四种日志记录模式,从简单模式到分析模式。它们都报告相同的信息,但视角不同。

  • 日志模式

    在此模式下,收到的消息被显示为未因子化,并且不智能。然而,这是最快的报告模式,在重现被调试进程中的死锁时应使用此模式。

  • 线程生命周期

    在此模式下,收到的消息从线程的角度显示。在此模式下不进行死锁检测。然而,此模式非常适合检查每个线程的行为。

  • 对象生命周期

    在此模式下,收到的消息从对象的角度显示。在此模式下不进行死锁检测。然而,此模式非常适合检查您正在监视的任何对象会发生什么。

  • 分析

    在此模式下,收到的消息将被分析,并在后台进行死锁检测。这是最慢的模式。然而,这是唯一会概述线程死锁和错误的模式。上述算法定义在 CThread::CheckLock 方法中。对象在 SyncObject.h 中声明。

第四部分:附加曲目

在被调试进程和服务器之间发送的每个 CommunicationObject 都包含被调试进程中的堆栈跟踪。此堆栈跟踪有助于发现死锁发生的位置。堆栈跟踪的问题主要是由于它们的含义不清(当错误发生在 0x00401345 时,我几乎肯定它并没有告诉你太多)。想法是使用 map 文件(如果可用),将堆栈跟踪中的地址映射到实际函数。我包含了一个 MapFileParser 来将地址反向解析为未修饰的函数名称。它不会给你行号,但无论如何它都比什么都没有好。(Map 文件也可以在 Release 版本中构建,没有任何风险,因为它们是单独的文件)。map 文件解析器会找到给定地址之前的函数。这对于 DLL 不起作用,因为无法知道 DLL 将如何映射(除非你像 Google 所说的“Mark Pietrek”那样自己指定)。

总结

我花了一个月的时间寻找这样的工具,因为没有可用的,所以这是我的。显然,这不是一个“专业”的软件。例如,它无法检测潜在的死锁,不像静态代码覆盖工具那样,它只会检测实际的死锁。我相信我可以添加此功能,因为我拥有所有必要的数据。此项目是用 ATL 和 WTL 构建的,所以我更鼓励您学习这些工具。我实现了一个拥有绘制的 CListViewCtrl,因为我找不到任何好的。实现位于 CThreadDLDView 中。无法保存日志,也无法读取日志。我保留了打印图标,但没有打印代码。如果您想升级/添加功能,请在下方回复几句。

我希望看到的是:

  • map 文件中的 function::line 号码(也可以生成包含行号的 map 文件,但那样 MapFileParser 会更复杂)。
  • 回复被调试进程以防止其死锁(因为我们在死锁真正发生在被调试进程之前就知道情况)。这可以通过使用共享内存区域来实现,而无需线程和延迟。

更新

  • 2005 年 4 月 1 日
    • 添加了导入 DLL 的映射(因此,您现在应该能够在 DLL 中定位死锁)。
    • 您可以使用该软件查看目标进程中加载了哪些模块。
    • 分析模式现在可以检测可能的死锁(是的,即使是那些只在客户端发生死锁的情况)。
    • 可以向被分析的程序传递参数。
    • 可以将分析保存到文件(是的,也可以导入到 Excel)。
  • 2005 年 2 月
    • 初始发布。

已知错误和/或问题

  • 在收集数据时屏幕闪烁很多。这不是一个问题,如果您不想看到收集数据的实时更新,请最小化服务器。
  • 仍然看不到死锁发生的行。我还没有完成 MAP 文件行读取代码,但您仍然可以手动浏览它。
  • 为什么这样一个“精彩”的功能……还没有。好的,随时实现它。请在此处发布您的修改以做出贡献。
© . All rights reserved.