本文转自:http://msdn.microsoft.com/zh-cn/magazine/cc163395.aspx
Bugslayer
等待链遍历
John Robbins
代码下载位置: Bugslayer2007_07.exe (175 KB)
Browse the Code Online
在近几年专门从事开发工具编写的生涯中,我总是关注新的操作系统版本,看是否有可用于解决困难的开发问题的内容。例如,Windows
® XP 引入了向量化异常处理,它允许您截取异常处理链用于记录和诊断。在《MSDN
® 杂志》
2006 年第 11 期 中,我针对自己所做的使用该功能为基于 Microsoft .NET 的应用程序编写异常记录器这一尝试进行了论述。
对于 Windows Vista™,Microsoft 没有让人失望。事实上,Windows Vista 有一个令人非常关注的称为等待链遍历 (WCT) 的新 API,可用于确定进程何时会发生死锁以及死锁原因。 这听起来太让我激动了,所以我自然要全身心投入。WCT 可以准确报告发生死锁的同步对象,这是一个好消息。但它仅报告有限的同步基元集,这是一个坏消息。即使存在此限制,它仍然是非常有用的 API,是您希望调试自己的工具包中包含的内容。
在此专栏中,我想讨论一下 WCT API 的用法和限制。作为此专栏的一部分,我将为您提供一种用于找出 WCT 支持的所有死锁的工具。因为我一直使用 .NET 编写工具,所以我还会向您展示令人生畏的交互操作的细节以及如何从 .NET Framework 使用 WCT API。
等待链遍历基础知识
图 1 列出了 WCT API 为死锁报告支持的同步基元。尽管还有其他几个同步基元(如事件)可能发生死锁,但该列表已涵盖了大多数应用程序的绝大部分问题区域。如果您专注于 .NET,则可能希望知道某个 Sync 块是否受支持。如果以前没有听说过 Sync 块,那是因为您不直接使用它们,Sync 块是用于实现锁定关键字以及 Monitor 类的内部结构。由于公共语言运行库 (CLR) 提供 sync 块的实现,所以这些块对于本机 WCT API 是未知的。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
同步基元 | 说明 |
---|---|
ALPC | 用于远程过程调用的 Windows 内部机制 |
COM | 跨线程和进程的 COM 调用 |
关键节 | 轻型、每进程、本机同步类型 |
Mutex | 通常用于协调多个进程同步的基于句柄的同步类型 |
SendMessage | 向其他线程和窗口块发送消息直至消息被处理 |
进程和线程句柄 | 等待进程和线程 |
WCT API 通过查看阻止调用中是否有正在等待的线程来发挥作用。如果阻止调用中存在线程,WCT API 将查找线程阻止的对象,并确定(如果适用)对象名称以及该对象所属的线程或进程。例如,如果线程 A 正在进程 B 上等待完成执行的操作,而进程 B 调用 SendMessage 来处理 A,则 WCT 即可检测到死锁并进行报告。
我所发现的 WCT API 的唯一问题是使用 WaitForMultipleObjects 时不会报告死锁,因此降低了它在许多应用程序中的有用性。例如,.NET 同步将 WaitForMultipleObjects 用于所有同步协作。但 WCT 并不报告线程死锁,而是报告线程受阻。幸运的是,WCT API 能够报告等待次数,由此,结合使用 LockWatcher(此专栏文章代码下载的一部分)之类的工具进行检查和比较,您可以手动判定某一特定线程是否发生了死锁。
WCT 是一个典型的 Win32
® API,这意味着必须抓取不透明句柄,将该句柄传递给记录数据的方法并在完成后调用 CloseHandle 的一种形式。Platform SDK 文档显示了初始化和使用 WCT API 所需的所有内容。对于初始化,存在一个小问题。您总是希望从中调用 RegisterWaitChainCOMCallback 函数以注册 CoGetCallState 和 CoGetActivationState COM 函数,以便 WCT 能够报告 COM 所有权信息,由于并未记录这些函数,您需要从 ole32.dll 通过 GetProcAddress 获得这些函数。个人认为,在您对 WCT API 进行初始化以查找 COM 死锁时,WCT API 应自动处理这种细小问题。
要初始化 WCT,请调用从 advapi32.dll 导出的 OpenThreadWaitChainSession 函数:
初始化完成后,无论您何时要确定应用程序中受阻的线程,都可以调用 GetThreadWaitChain:
虽然您可以初始化 WCT API 以异步执行所有操作,但几乎所有使用 WCT API 的用户都使用同步版本。除了线程 ID 以外,GetThreadWaitChain 的 Flag 参数可设置您希望接收的数据的类型。默认情况下,Flag 参数仅查看指定进程内部的线程。如果希望从其他进程获取信息,您需要指定三个标记:WCT_OUT_OF_PROC_COM_FLAG、WCT_OUT_OF_PROC_CS_FLAG 和 WCT_OUT_OF_PROC_FLAG。您可以使用涵盖这三个标记的汇总标记 WCTP_GETINFO_ALL_FLAGS 轻松实现上述操作。
GetThreadWaitChain 返回的关键信息是一个 WAITCHAIN_NODE_INFO 结构数组,此数组描述了线程状态以及当前受阻内容。此外,GetThreadWaitChain 还会返回根据 WCT 代码内部的分析引擎判定的线程是否发生死锁的信息。大体上,WCT API 相对简单,虽然它不会告知您所有死锁,但它所报告的内容是很精确的。
当线程受阻时,WCT API 会在 WAITCHAIN_NODE_INFO 结构数组中报告该线程及死锁原因。例如,在发生死锁的部分,如果线程 A 拥有关键部分 X 并希望获得关键部分 Y,而线程 B 拥有关键部分 Y 并希望获得 X,则会在数组中按以下形式报告死锁:
请注意,不会报告关键部分地址。但是对其他对象(如互斥锁)而言,如果对象有名称,则 WCT API 将报告该名称。
为充分利用 WCT API,您总是希望命名句柄,以便查看导致死锁的准确原因。当然,如果您正在命名句柄,则这些句柄此时在进程外部是可见的,这会导致严重的同步问题。例如,假设您将一个互斥锁命名为 MyHappyMutex。您正在运行 A 和 B 两个进程,并且它们都在等待使用同一个互斥锁名。如果进程 A 释放此斥锁,则进程 A 和进程 B 都可获取它。因此,您可能会遇到调试方面的严重挑战。如果要命名句柄并确保名称对进程是唯一的,则您会希望在名称上附加进程 ID 或其他某些能够保证对进程唯一的字符串。
当命名句柄对调试有所帮助时,一些用户可能会担心安全性攻击问题,因为在句柄全局化后,其他进程即可打开此句柄。在某些情形下,这可能是一个值得关注的问题;如果您对前景感到忧心,至少应考虑在调试版本中命名句柄。
WAITCHAIN_NODE_INFO 结构缺陷
虽然 WCT API 是面向本机代码的,但我有更多的分析构想,曾计划编写一个能够从 .NET 代码使用 WCT API 的工具。此外,由于无需过多担心内存问题,编写 .NET 应用程序会比较轻松。因此,我原以为自己可以较快地编写出此程序。我一点不曾想到会遇上某些实在令人挠头的互操作问题。
图 2 显示了本机 WAITCHAIN_NODE_INFO 结构。仔细观察此图,您会发现该结构中包含两个重叠的共同体。对大多数结构而言,P/Invoke 是没有问题的,但只要有 C++ 共同体存在,您就知道自己遇到了一些严重问题。Windows 的几乎所有结构和 API 在 pinvoke.net 网站都有定义,但我有种不安的感觉,面对这种讨厌的本机结构,自己孤立无援。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
typedef struct _WAITCHAIN_NODE_INFO { WCT_OBJECT_TYPE ObjectType; WCT_OBJECT_STATUS ObjectStatus; union { struct { WCHAR ObjectName[WCT_OBJNAME_LENGTH]; LARGE_INTEGER Timeout; // Not implemented in v1 BOOL Alertable; // Not implemented in v1 } LockObject; struct { DWORD ProcessId; DWORD ThreadId; DWORD WaitTime; DWORD ContextSwitches; } ThreadObject; }; } WAITCHAIN_NODE_INFO, *PWAITCHAIN_NODE_INFO;
我的第一希望是,通过在 .NET 端定义两个独立的结构,可能会将问题解决掉。在其他的项目中,我能够使用不同的 .NET 端声明来声明不同的 DllImportAttribute P/Invoke 方法以轻松定义结构。我可以使用 WCT 代码假设出可能的内部实现细节,但定义 .NET 结构将是一件繁琐的事情。对死锁示例运行本机等待链遍历示例 (
msdn2.microsoft.com/ms681418.aspx) 很快就让我知道,返回的死锁信息中有太大差异,我尝试的第一个解决方案不能解决问题。
这意味着我需要借助 LayoutKind.Explicit 的功能定义结构内部的各偏移量。一个返回 WAITCHAIN_NODE_INFO 结构大小的快速本机示例显示该大小在 32 位和 64 位 Windows 中都是 280 个字节,这样就不必再定义单独的结构,也不必以编程方式确定要根据操作系统使用哪个结构。
我有一个轻松查找结构偏移量的小窍门,就是使用 Debugging Tools for Windows 程序包中的 WinDBG,您可从
microsoft.com/whdc/devtools/debugging 下载此工具包。由于 WinDBG 是一个仅限本机的调试器,所以您需要对使用了有问题的结构的本机 C++ 应用程序进行调试。要显示某个类型,可使用 DT 命令,使用该命令可方便地默认显示所有字段偏移量。对于 WAITCHAIN_NODE_INFO,我会希望将 –r 1 和 –v 选项传递给 DT 命令。前者会告知 WinDBG 递归展开结构一级,后者会转储有关结构的所有信息。
图 3 显示了在 Windows Vista x64 上执行该命令的结果。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
0:000> dt -v -r 1 NodeInfoArray[0] Local var [AddrFlags 90 AddrOff 0000000000000050 Reg/Val rsp (7)] @ 0x12d970 [0] [16] struct _WAITCHAIN_NODE_INFO, 4 elements, 0x118 bytes +0x000 ObjectType : Enum _WCT_OBJECT_TYPE, 11 total enums 8 ( WctThreadType ) +0x004 ObjectStatus : Enum _WCT_OBJECT_STATUS, 11 total enums 3 ( WctStatusBlocked ) +0x008 LockObject : struct _WAITCHAIN_NODE_INFO::<unnamed-tag>::<unnamed-tag>, 3 elements, 0x110 bytes +0x000 ObjectName : [128] “۰” +0x100 Timeout : union _LARGE_INTEGER, 4 elements, 0x8 bytes 0x0 +0x000 LowPart : 0 +0x004 HighPart : 0 +0x000 u : struct <unnamed-tag>, 2 elements, 0x8 bytes +0x000 QuadPart : 0 +0x108 Alertable : 0 +0x008 ThreadObject : struct _WAITCHAIN_NODE_INFO::<unnamed-tag>::<unnamed-tag>, 4 elements, 0x10 bytes +0x000 ProcessId : 0x6f0 +0x004 ThreadId : 0x6f4 +0x008 WaitTime : 0x3005ce +0x00c ContextSwitches : 0x2fb
第一条有用的信息是
图 3 的输出内容中第三行的 0x118,这是结构的大小。正如您所期望的,前两个枚举的偏移量分别是 0x0 和 0x4。两个共同体 LockObject 和 ThreadObject 的初始偏移量是 0x8。以 LockObject 联合体为例,Unicode 字符数组的偏移量是 0x8 并且要用 0x100 个字节。这就意味着 Timeout 下半部分开始于 0x108,Timeout 上半部分开始于 0x110。最后,Alertable 字段为 0x114。看到模式后,ThreadObject 共同体的偏移量就无足轻重了。
使用 FieldOffset 属性对我的 WAITCHAIN_NODE_INFO 结构的 .NET 版本进行编码后,我遇到了第一个严重的障碍。LockObject 共同体以 128 位 Unicode 字符数组 ObjectName 作为第一项。无论尝试如何定义该字段,我每次使用自己的代码访问定义的结构时,总会出现 TypeLoadException。因于类型安全原因以及与垃圾收集有关的问题,指向引用类型的字段不能与其他字段重叠,而由于数组和字符串均为引用类型,那么在这种情况下,二者都是不可用的。
不安全
当我正打算放弃时,我想起了不安全关键字和 C# 中引入的一种与 .NET Framework 2.0 相关的固定关键字的新用法。我可以将结构定义为不安全的,并对 ObjectName 字段应用固定的字符数组定义;这将允许结构内部存在一个不安全的内联数组。它类似结构中的数组,但实际上,该字段在通过指针对其进行访问时结束;然后,我就可以使用带有字符指针的 String 构造函数并访问实际值。
定义我的结构并添加 /unsafe 编译器开关后,我很高兴地看到我的测试应用程序完全按照本机测试示例报告了关键部分死锁。扩展我的测试应用程序,使一个线程获取某个关键部分,并在等待同一关键部分的另一线程上对窗口调用 SendMessage,这样做也行得通。对 Explorer 进程上的块添加死锁测试也显示正常。
在认为一切都正常后,我添加了一个在已命名的互斥锁上发生死锁的测试。Platform SDK WCT 示例按以下方式显示该互斥锁的名称:
\Session\1 指示我的交互式登录会话,BaseNamedObjects 是内核对象表。在测试应用程序中,我已将互斥锁命名为 Deadlock Mutex A。但是,我的结构的 P/Invoke 杰作显示的名称却只是“\Sessi”。
我认为这是由于结构大小不正确导致的,就对 Marshal.SizeOf 进行检查,结果 .NET 端报告它认为适当的结构大小为 280 字节。我也渐渐了解到,如果结构大小不正确,那么先前对于关键部分所做的测试将无法进行,因为本机 GetThreadWaitChain 函数将跨越数组的所有 .NET 内存。此外,当我浏览这些先前死锁的数组时,我不会看到 C# 代码中数组内的正确数据。
对于这一点,我有点迷惑,因为关键部分死锁测试表明我的结构大小正确,而已命名的互斥锁却表明该结构可能太小。我首先想到的就是我需要查看本机 GetThreadWaitChain 函数所写的具体内容,因此我在已命名的互斥锁死锁上的 WinDBG 下运行了 .NET 应用程序。在 ADVAPI32!GetThreadWaitChain 上设置断点、查找数组参数并退出函数,结果显示本机端正在向数组复制完整的互斥锁名称。但是,该数据当然不会返回到我的 C# 代码中!
不幸的是,我需要在本机代码中查看 .NET 对数组执行的操作。当然,由于我运行的是 Windows Vista 的 x64 版本,这就意味着不支持混合调试 — 在混合调试中,您可以同时单步执行应用程序的托管端或本机端。在仅包含一个二进制程序的简单情形下,将平台设置为 x86,即可强制将应用程序设为 32 位,然后,您就可以在 64 位的 Windows Vista 中执行混合调试了。但是,对于拥有多个程序集的实际应用程序来说,在真正的 32 位计算机上执行混合调试要快得多,并且不会更改版本。在我编写本专栏时,我收到了有关在 x64 系统上执行混合调试的几个问题,现在我可以告诉大家这是有可能的。在本专栏的结尾,我将介绍在运行完整孔径的 x64 .NET Framework 代码时查看本机端和托管端的一些技巧。
在研究了大量的汇编语言后,我获得了灵感。因为在固定数组中我只能看到“\Sessi”,所以我想知道当其他字段位于相同位置时,.NET 互操作代码是否是仅封送数据。看起来似乎是线程共同体部分(长度为 12 字节)决定了名称共同体部分的长度。为了测试我的推测,我抽取了额外的 Int32 字段,并假设其在结构布局中从 0x18 开始。您可以想象当我运行应用程序后又得到“\Session”字符串时,我是多么地震惊!
结果表明,当固定关键字用于创建字符数组时,封送拆收器与通过 C# 编译器生成的代码进行交互的方式存在缺陷(此缺陷应在 Visual Studio“Orcas”版本中得以修复)。为了解决此问题,我需要修改结构以便可对其直接复制,也就是说,组成结构的类型必须全部具有与托管状态下相同的本机表示形式。可直接复制类型的列表包括 Byte、SByte、Int16、UInt16、Int32、UInt32、Int64、UInt64、IntPtr、UIntPtr、Single 和 Double。不过,您将注意到列表中明显缺少 Char。实际上,Char 不可直接复制,因为在本机代码中它可以有多种表示形式(一个或两个字节)。为了解决我的问题,我将固定数组的字段类型更改为 ushort,而不是 char;然后在将此数组转换为 String 的代码中,我首先将所得 ushort* 转换为 String 构造函数所需要的 char*。
图 4 显示了正常运行的最终结构。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
[StructLayout ( LayoutKind.Explicit, Size=280 )] internal unsafe struct WAITCHAIN_NODE_INFO { [FieldOffset ( 0x0 )] public WCT_OBJECT_TYPE ObjectType; [FieldOffset ( 0x4 )] public WCT_OBJECT_STATUS ObjectStatus; // The name union. [FieldOffset ( 0x8 )] private fixed ushort RealObjectName [ WCT_OBJNAME_LENGTH ]; [FieldOffset ( 0x108 )] public Int32 TimeOutLowPart; [FieldOffset ( 0x10C )] public Int32 TimeOutHiPart; [FieldOffset ( 0x110 )] public Int32 Alertable; // The thread union. [FieldOffset ( 0x8 )] public Int32 ProcessId; [FieldOffset ( 0xC )] public Int32 ThreadId; [FieldOffset ( 0x10 )] public Int32 WaitTime; [FieldOffset ( 0x14 )] public Int32 ContextSwitches; // Does the work to get the ObjectName field. public String ObjectName ( ) { fixed ( WAITCHAIN_NODE_INFO* p = &this ) { return (p->RealObjectName [ 0 ] != ‘\0’) ? new String ( (char*)p->RealObjectName ) : String.Empty; } } }
LockWatcher
解决 WAITCHAIN_NODE_INFO 结构后,我就能够完成 LockWatcher 应用程序了。现在,您可以使用它查找应用程序中的死锁。要查找单个进程或一组特定进程中的死锁,只需在命令行中传递进程 ID 值。如果没有指定任何进程 ID,则 LockWatcher 将对计算机上的所有进程进行报告。如果要查看所有进程,您将希望从具有已提升权限的命令窗口中启动 LockWatcher,以便 LockWatcher 具有完全权限来检查其他已提升进程中的数据。
默认情况下,LockWatcher 仅显示被阻止的线程和任何通过 WCT API 报告的被阻止的项。要查看等待时间和上下文开关信息,请传递 –a 命令行选项。LockWatcher 的一种卓越功能是 –t <seconds> 选项,其中 LockWatcher 将按照指定的时间间隔不断地检查死锁的过程。
当 LockWatcher 报告死锁时,您将看到以下输出内容(此输出内容是关键部分死锁的一个示例):
对于第二个线程,您可以在线程 5492 等待线程 424 拥有的关键部分时读取输出。线程 424 将等待关键部分。
正如我先前所述,当使用 WaitForMultipleObjects 时,WCT API 不报告死锁。以下是死锁的输出,其中线程 5652 拥有 Deadlock Mutex A 并已调用了 WaitForMultipleObjects 以等待另外两个线程句柄。
通过查看输出内容,您可以拼凑部分死锁,但不能拼凑出所有信息。我使用 LockWatcher 的目的是从 WCT API 返回的数据之上构建算法以标识死锁(如果使用了 WaitForMultipleObjects)。根据前面示例的工作情况,非常容易看出我必须完成的工作。我的下一步是迅速完成 .NET 示例,该示例死锁了两个 Mutex 类实例并具有以下输出内容:
正如您所看到的那样,WCT API 没有报告任何有用的内容,因此除非我打算开始执行代码注入、堆栈遍历和参数解密,否则我能够进行的操作不是很多。即使 WCT API 受限,WCT API 仍有助于查找更多的死锁,其数量远远多于您没有 WCT API 时可以查找的死锁。为了查看 LockWatcher 报告不同死锁的方式,我将在本机 C++ 中编写的正确命名的 DEAD 程序作为测试程序包括在其中。
x64 上的混合调试
先前,我讨论了如何为 x86 平台重新编译应用程序以允许混合托管和本机调试。不过,在我探索 WAITCHAIN_NODE_INFO 结构的 P/Invoke 的艰难过程中,我对以 x64 二进制程序运行的 .NET 应用程序执行了一点混合调试。由于该方案不是 Microsoft 支持的方案,因此我想自己应该告诉您我使用的技巧,这些技巧使我在查看两端时没有抓狂。
托管调试并不经过标准 Windows 调试 API (
msdn2.microsoft.com/ms679276.aspx),而是使用依赖于向量异常处理的方案。如果翻找 CLR 调试 API (
msdn2.microsoft.com/ms404520.aspx) 中的状况,您可以看到它们使用常见的单步执行和断点异常作为标准调试器。因为托管调试不经过 Windows 调试 API,所以您不会遇到将本机调试器附加到已在调试器下运行的应用程序时遇到的错误。因此,在 Visual Studio
® 中启动调试托管应用程序后,您可以将本机调试器附加到托管应用程序中。
虽然您可以尝试使用 Visual Studio 的另一个实例作为本机调试器,但这样几乎不能做任何事情,因为您将中断托管调试 API 触发的每个步骤。由于在 Visual Studio 中无法在出现异常后自动继续执行,所以您唯一的希望就是享受 WinDBG 带来的乐趣。
尽管 WinDBG 比较难以使用,但可爱的事件筛选使您可以忽略由 CLR 调试 API 不断触发的单步意外情况。将 WinDBG 附加到托管应用程序后,在遇到初始断点或加载程序断点时您将立即停止。
您要使用的第一个命令如下:
SXD (Set Exception Disabled) 命令将单步意外情况 (SSE) 设置为禁用,这表示您不会在出现单步意外情况时停止。由于 CLR 调试 API 使用单个步骤就可以控制一切,因此我设置了 first chance 命令,以使 WinDBG 自动继续运行并将其作为未经过处理的调试器传入调试对象。请记住,当在 64 位本机部分内部停止时您已设置了此命令,因为如果启用了此命令,您将不能够单步执行任何操作。要在需要时将单步执行转换回启用状态,请执行以下命令:
控制单步执行后,您需要规划如何处理 CLR 调试 API 的断点以及要在本机代码中设置的断点。在我尝试确定 P/Invoke 问题的情况时,我需要查看 advapi32.dll 中 GetThreadWaitChain 的参数。因此,我需要通过以下命令在该地址上设置断点:
要实现目的,我最需要的技巧是忽略所有来自 CLR 调试 API 的断点。幸运的是,WinDBG 可以像处理其他异常一样处理断点异常,因此,我可以继续使用 SX (Set Exception) 系列命令使 WinDBG 在遇到断点异常时执行命令。更幸运的是,WinDBG 支持称为调试器命令程序的基本控制流命令。使用 .if 和 .else 命令,您可以在调试器命令上执行条件逻辑。
要忽略所有断点异常(我设置断点的位置除外),在 x64 计算机上运行以下命令即可:
每个 BPE (Break Point Exception) 的 SXE (Set Exception Enabled) 语句在遇到第一个可能的异常时执行命令。该命令将检查指令寄存器 RIP 是否正在执行 GetThreadWaitChain 中的第一个指令。(对于 32 位计算机,将改用 EIP 作为指令寄存器。)如果正在执行,则调试器会通过将文本回显到屏幕报告停止操作。如果没有执行,则调试器将执行 GN (Go Not Handled) 命令以使调试对象拥有异常,从而不会与 CLR 调试 API 混淆。
虽然有点复杂,但这些步骤使得可以在 x64 系统上执行少量混合调试。请注意,对您进行的操作一定要非常谨慎,否则会很容易将托管应用程序搞得一团糟。我在此展示的技术极有可能不受 Microsoft 支持或宽恕。然而,当需要在 x64 系统上查看应用程序的本机端和托管端时,您至少可以有一点思路来执行操作。
总结
本专栏是一个多么漫长和奇妙的旅程呀!刚开始看起来简单的一小部分,通过 P/Invoke 和在 x64 环境下进行混合调试成为一个漫长的过程,并且该过程最终的结果能够检测应用程序中的部分死锁功能。根据对 Microsoft API 改进功能数年的跟踪记录,我觉得将来的 Service Pack 中在 WCT 中看到对 WaitForMultipleObjects 的支持很有希望。当我们获得这种支持后,我将更新 LockWatcher 以支持 WaitForMultipleObjects!
提示 78 如果您正在进行任何 ASP.NET 开发,您就会知道有时仅需要查看通信过程中的原始 HTTP 信息。在这些情况下,Fiddler (
www.fiddler2.com) 就派上用场了。您不仅可以查看每个字节,而且还可以使用真正绝妙的脚本 API 更改和调整数据。Fiddler 2 是最新的版本,它支持 HTTPS 截取,所以更应该将其作为必备品纳入您的工具包之中。
提示 79 Gregg Miskelly 在他的
博客中提出了一个聪明的调试技巧。在托管代码中,您没有对象的地址,因此,设置每实例断点是几乎不可能的。但是,Gregg 指出,如果您对实例使用该绝妙的 Make Object Id 技巧,您就可以通过将条件断点设置为 == 1# 来设置每实例断点。我已经使用过无数次了,屡试不爽!
将您想向 John 询问的问题和提出的意见发送至 slayer@microsoft.com.
John Robbins 是 Wintellect 的创始人之一,Wintellect 是一家软件咨询、教育和开发公司,专门研究 .NET 和 Windows。他的最新著作是《Debugging Microsoft .NET 2.0 Applications》(Microsoft Press,2006)
。您可以通过 www.wintellect.com 与 John 联系。