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






4.86/5 (16投票s)
2005 年 2 月 7 日
7分钟阅读

90597

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 时,应用程序就会死锁。为了避免这种情况,服务器(死锁检测器)使用其 CSyncObject
和 CThread
类来监视每个对象和线程。然后,每个对象都会跟踪谁拥有它(线程列表)。类似地,每个线程都有一个等待对象列表和锁定对象列表。现在让我们看看算法如何找到死锁。
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 文件行读取代码,但您仍然可以手动浏览它。
- 为什么这样一个“精彩”的功能……还没有。好的,随时实现它。请在此处发布您的修改以做出贡献。