.NET 对象间谍和 InvokeRemote
一个用于浏览任何正在运行的 .NET 应用程序中的公共和私有成员的工具(以及一个通用的 InvokeRemote 方法,它封装了代码注入)。
目录
引言
在软件开发过程中,一个好的调试器是无价的。同样,当发生运行时异常时,.NET 的异常信息(尤其是调用堆栈)提供了极大的帮助。然而,有时您会遇到一些问题,某些地方不对劲,为了缩小问题范围,您需要检查应用程序的某些内部状态和数据。使用调试器并非总是易事,因为很少有自然的地方可以放置断点。当应用程序部署后,情况会变得更加困难;唯一的选择通常是您在应用程序中留下了某些代码来转储相关的内部状态和数据。
工具 Managed Spy 和关于 Windows Forms Spy (wfspy) 的文章让我着迷于这样的想法:应该可以使用反射来读取任何正在运行的 .NET 应用程序中的公共和私有成员。此处提出的解决方案——与其他不同——不会止步于选定的控件(窗口);它提供了一个可浏览的字段和属性层次结构。而且,与 Managed Spy 不同,对象不必是可序列化的;整个对象浏览器被注入到被监视应用程序的地址空间中。另一方面,此解决方案不提供任何跟踪功能。
解决方案
上述问题解决方案实现为一个名为 .NET Object Spy 的工具。主窗口很小很简单,但易于使用。
将十字准线拖到另一个窗口,它将被框起来,主窗口中会显示主要属性。一旦松开,您就会看到这个菜单。
选择第一个选项,浏览器就会显示出来(在所选应用程序的地址空间中运行)。
您可以右键单击节点以弹出上下文菜单。这允许您刷新节点(包括子节点)。
窗口顶部的行显示当前所选对象的“路径”,例如 Controls[0].Size.Width
。您也可以键入此路径直接跳转到相应的节点。您甚至可以使用它来调用无参数方法,例如 Controls[0].GetHashCode()
或 FindForm().Location.X
。Copy 按钮将“路径”复制到剪贴板。这旨在帮助 ObjectSpyEvaluator,稍后将进行解释。
实现
该解决方案包含三个程序集,将在以下段落中详细介绍。
- InjectLib - 一个 C++ 库,它将代码注入封装为一个通用方法
Injector.InvokeRemote
。 - ObjectSpy - 一个包含主窗口的 C# 可执行文件。
- ObjectSpyLib - 一个包含浏览器(将被注入到被监视应用程序中)的 C# 库。
InjectLib (InvokeRemote)
此解决方案的一个衍生是具有此签名的通用方法(C# 语法)。
object Injector.InvokeRemote(IntPtr hWnd, string assemblyFile,
string typeName, string methodName, object[] args)
这以一种通用方式封装了注入过程,可用于其他目的。hWnd
参数标识了代码应注入的窗口(以及进程)。assemblyFile
指定了应注入的代码,typeName
指定了这个程序集中的一个类,而 methodName
指定了这个类上的一个静态方法。该方法使用提供的参数调用,并且可以选择性地具有返回值。
有不同的方法可以将代码注入到另一个进程中(请参阅 CodeProject 文章 Three Ways to Inject Your Code into Another Process)。此解决方案基于大多数其他工具使用的方法:Windows 挂钩(有关 Windows 挂钩的详细信息,请参阅 Using Hooks)。InvokeRemote
执行的步骤是:
- 使用 Win32 API 函数
SetWindowsHookEx
创建一个 Windows 消息挂钩。 - 将参数序列化到共享内存。
- 向挂钩发送自定义消息,请求其服务。
- 挂钩(即注入到目标进程中的代码)现在:
- 从共享内存中反序列化参数。
- 加载请求的程序集。
- 使用指定的参数调用请求的方法。
- 将任何返回值序列化到共享内存。
- 移除挂钩。
- 从共享内存中反序列化返回值并将其返回给调用者。
SetWindowsHookEx
需要挂钩函数的地址。该函数必须从库中导出,而 C# 无法做到这一点。因此,该库是使用 Visual Studio 2005 的 C++/CLI 支持创建的混合模式 C++ 库。也就是说,该库同时包含原生代码和托管代码。
两个进程之间共享的内存仅仅是库中创建的一个数据段,也就是说,它有一个固定大小。对于这个库来说,这意味着 InvokeRemote
参数的序列化版本必须适合 2000 字节(显然,它们也必须是可序列化的)。返回值也适用相同的限制。另一种方法可能是内存映射文件(请参阅 MSDN 库中的 这篇文章)。
挂钩函数引起了几个有趣的问题。
问题 #1:为了对序列化/反序列化进行良好的封装,消息挂钩的参数被封装在 InjectLib 库本身声明的可序列化 RequestMessage
类中。尽管如此,上面步骤 4a 中的反序列化失败了,并抱怨找不到 InjectLib 库——显然忽略了它当时正在运行它!搜索互联网表明,如果使用 LoadFrom
加载库,可能会出现这种情况。虽然是 Windows 本身在注入时加载库,但建议的解决方案解决了问题:订阅 AppDomain.CurrentDomain.AssemblyResolve
事件。当 CLR 找不到程序集时会发生此事件,并允许您自行返回程序集。
问题 #2:当消息挂钩尝试加载 InvokeRemote
调用中指定的程序集(上面步骤 4b)时,它找不到。这可以通过使用 Assembly.LoadFrom
而不是 Assembly.Load
来解决。消息挂钩会追加 InjectLib 的路径,假设请求的程序集位于相同位置。否则,请求的程序集将必须放置在 GAC 或被监视应用程序的文件夹中。一个类似于问题 #1 的解决方案可能也可以使用。
ObjectSpy
ObjectSpy 是包含主窗口的可执行文件。它用 C# 编写,并进行多次 Win32 API 调用来查找窗口句柄和提取信息等(有关在 C# 中声明 API 调用,pinvoke.net 非常有用)。十字准线方法的实现高度借鉴了 Robert Kuster 的 关于向另一个进程注入代码的三种方法的文章中的 WinSpy 演示项目。除了这些 API 调用之外,代码非常直接,最终调用 InjectLib 的 InvokeRemote
方法。
Injector.InvokeRemote(hWnd, "ObjectSpyLib.dll",
"Bds.ObjectSpy.ObjectSpyForm", "ShowBrowser", new object[] { hWnd });
ObjectSpyLib
这个 C# 库包含被注入到被监视应用程序中的浏览器窗体。入口点是 ObjectSpy 向 InvokeRemote
指定的静态方法。
public static void ShowBrowser(IntPtr hWnd)
该方法创建一个窗体实例并调用其 Show
方法。窗口句柄被“转换”为一个 Control
,使用 Control.FromHandle
。该控件构成了对象层次结构的根。从那里开始,其余的都是对 reflection 命名空间的运用。
在代表非空对象的树的每个节点下方,在节点第一次展开时(排除了索引器,因为它们需要参数),会添加公共和私有字段以及对象的属性。此外,如果一个对象实现了 IEnumerable
,则会添加枚举的对象。每当在树中选择一个节点时,相应的对象将用作窗口右侧 PropertyGrid
控件的 SelectedObject
。
值得注意的一点是,泛型类型名称会被损坏。例如,从 reflection 类返回的泛型 List
类的类型名称是“List`1”。反引号后的数字表示类型参数的数量。ObjectSpyLib 以递归方式取消损坏,并以 C# 语法(即使用小于号和大于号)显示。
ObjectSpyEvaluator
除了浏览器窗体,ObjectSpyLib 还包含 ObjectSpyEvaluator
类,它具有此静态方法。
public static string Evaluate(IntPtr hWnd, string expression)
expression
参数与浏览器中可以输入的“对象路径”相同。表达式的结果将被返回。在执行自动 GUI 测试时,这使得编写验证代码非常容易。验证代码例如可以进行此类调用来检查 TreeView
中根节点的 Text
属性。string result = (string)Injector.InvokeRemote(hWnd, "ObjectSpyLib.dll",
"Bds.ObjectSpy.ObjectSpyEvaluator", "Evaluate",
new object[] { hWnd, "treeView.Nodes[0].Text" });
使用 Evaluate
方法的示例应用程序 - ObjectSpyEE - 包含在下载文件中。未来的增强
我希望这个工具能像现在这样有用(至少制作它很有趣!)。但是,当我得空时,还有一些改进我希望去做。
- 包含静态成员。
- 如果可能,查找并包含应用程序中的“根对象”(即,不仅仅是选定控件引用的对象)。
- 在遇到“简单类型”(
Int32
等)时停止展开。 - 在
PropertyGrid
中显示私有属性+公共和私有字段(通过实现ICustomTypeDescriptor
)。 - 显示有关对象的其他类型信息(声明类型的
FullName
,实际类型的FullName
(+Name
?)),可能在PropertyGrid
中。 - 允许更改“简单类型”(
Int32
等)的值,即使不是公共属性,并在父级的PropertyGrid
中更改。 - 当枚举的对象数量巨大时(当花费时间过长时),中止获取枚举对象。
- 显示有关属性或字段是从哪个基类继承的信息,可能在
PropertyGrid
中。 - 选项以仅默认显示字段,而不是属性(以避免调用
get
访问器可能产生的副作用)。
历史
- 2007-02-23(ObjectSpy 1.2.0)
- 添加了
ObjectSpyEvaluator
、ObjectSpyEE 以及浏览器窗体中的相关功能。
- 添加了
- 2006-12-05(ObjectSpy 1.1.0)
- 成员不再使用
SortedList
(按键)排序,而是使用普通List
上的Sort
方法(以及 ObjectInfo 上的IComparable
实现)。这解决了处理混淆名称(如 .NET Reflector 中的)的问题。由于区域性问题,不同成员的键被视为相等。有关详细信息,请参阅 Microsft Forums 和 MSDN。 - 浏览器窗口中的树在没有焦点时不再隐藏选择。
- 在浏览器上下文菜单中添加了刷新选项。
- ObjectInfo 已被拆分为基类和派生类。
- 浏览器窗口的图标略有修改(以区别于主窗口)。
- 成员不再使用
- 2006-11-21(ObjectSpy 1.0.0)