本节描述使用公共语言运行时 (CLR) 调试 API 进行的动态代码注入。 动态代码注入执行原始可迁移可执行 (PE) 文件中没有的函数。 例如,在 Microsoft Visual Studio 集成开发环境 (IDE) 中调试时,您可以使用动态代码注入在**“即时”**窗口中执行表达式。 CLR 将截获活动线程来执行代码。 调试器可能会请求 CLR 运行或冻结其余的线程。 由于动态代码注入建立在函数求值的基础上,因此,您可以调试动态注入的函数,就好像它是正常代码一样。 也就是说,您可以为动态注入的代码调用所有标准调试服务(比如设置断点、单步执行等等)。
调试器必须执行下列操作来动态地注入代码:
编写将执行动态代码的函数。
通过使用“编辑并继续”将代码注入调试对象。
通过调用编写的函数来执行代码,并在需要时重复执行。
以下小节详细描述了这些步骤。
编写函数
计算将执行动态代码的函数的签名。 为此,请将局部变量的签名追加到正在叶帧中执行的方法的签名。 通过此方式构造的签名不需要计算已使用变量的最小子集。 运行时将忽略未使用的变量。 有关更多信息,请参见本主题后面的“从元数据中获取签名”。
必须将该函数的所有参数声明为 ByRef。 这样,函数求值就能够将已注入函数中变量的任何更改传播回到调试对象中的叶帧。
执行动态代码时,某些变量可能不在范围内。 在这种情况下,您应传递 null 引用。 如果引用不在范围内的变量,将会引发 NullReferenceException。 如果出现这种情况,调试器可通过调用 ICorDebugManagedCallback::EvalException 回调来完成函数求值。
为函数选择唯一的名称。 您必须为函数名称加上字符串前缀“_Hidden:”,这样,调试器将可防止用户浏览函数。 添加了此函数后,将会设置一个标志,该标志指示函数名称是特殊名称。
注入代码
调试器应要求编译器建立一个函数,该函数的主体是将动态注入的代码。
调试器将计算一个增量可迁移可执行 (PE) 文件。 为此,调试器将调用 ICorDebugModule::GetEditAndContinueSnapshot 方法来获取 ICorDebugEditAndContinueSnapshot 对象。 调试器调用 ICorDebugEditAndContinueSnapshot 方法来创建增量 PE 映像,并调用 ICorDebugController::CommitChanges 在运行的映像中安装增量 PE。
动态注入的函数应放在可见性级别与将在其中执行该函数的叶帧相同的位置。 如果叶帧是实例方法,动态注入的函数也应该是同一个类中的实例方法。 如果叶帧是静态方法,动态注入的函数也应该是静态方法。 全局方法是属于特定类的静态方法。
注意
函数将存在于调试对象中,即使在动态代码注入过程完成之后也是这样。这样,就能够重复地对以前注入的代码进行重新求值,而不必重新编写函数和再次注入代码。因此,对于以前注入的代码,可以跳过本节和上一节中所述的步骤。
执行注入的代码
通过使用调试检查例程获取签名中每个变量的值(一个 ICorDebugValue 对象)。 您可以使用 ICorDebugThread::GetActiveFrame 或 ICorDebugChain::GetActiveFrame 方法获取叶帧,并为 ICorDebugILFrame 获取 QueryInterface。 调用 ICorDebugILFrame::EnumerateLocalVariables、ICorDebugILFrame::EnumerateArguments、ICorDebugILFrame::GetLocalVariable 或 ICorDebugILFrame::GetArgument 方法来获取实际变量。
注意
如果调试器附加到未设置 CORDBG_ENABLE 的调试对象(也就是说,未在收集调试信息的调试对象),调试器将无法获取 ICorDebugILFrame 对象,从而无法为函数求值收集值。
至于充当动态注入函数参数的对象是弱取消引用还是强取消引用对象,这一点并不重要。 对函数进行求值时,运行时将截获在其中进行注入的线程。 这将在堆栈上留下原始叶帧和所有原始强引用。 但是,如果调试对象中的所有引用都是弱引用,则运行动态代码注入可能会触发垃圾回收,而这可能会导致对象被作为垃圾回收。
使用 ICorDebugThread::CreateEval 方法创建一个 ICorDebugEval 对象。 ICorDebugEval 提供了对函数进行求值的方法。 调用其中的一种方法。 诸如 ICorDebugEval::CallFunction 等方法只会设置函数求值。 调试器必须调用 ICorDebugController::Continue 来运行调试对象并对函数求值。 求值完成后,调试服务将调用 ICorDebugManagedCallback::EvalComplete 或 ICorDebugManagedCallback::EvalException 方法通知调试器有关函数求值的信息。
如果函数求值返回对象,该对象将是强引用对象。
如果动态注入的代码尝试取消引用传递到代码包装函数的 null 引用,CLR 调试服务将调用 ICorDebugManagedCallback::EvalException。 作为响应,调试器可能会通知用户它无法对注入的代码进行求值。
请注意,ICorDebugEval::CallFunction 方法不执行虚拟调度;如果需要虚拟调度,请改用 ICorDebugObjectValue::GetVirtualMethod 方法。
如果调试对象是多线程对象,并且调试器不需要任何其他线程运行,则调试器应调用 ICorDebugController::SetAllThreadsDebugState 并将所有线程(用于函数求值的线程除外)的状态设置为 THREAD_SUSPEND。 此设置可能会导致死锁,具体情况视动态注入代码所执行的操作而定。
其他问题
由于 .NET Framework 安全性由上下文策略确定,因此动态代码注入将采用与叶帧相同的安全权限和功能运行,除非您明确更改了安全设置。
可以在“编辑并继续”功能允许添加函数的任何位置添加动态注入的函数。 合理的选择是将它们添加到叶帧。
可通过使用“编辑并继续”操作添加到类的字段、实例方法或静态方法的数量不受限制。 允许的最大静态数据量是预定义的,当前限制为每模块 1 MB。
不允许在动态注入的代码中使用非局部 goto 语句。
从元数据中获取签名
获取元数据分配器
调试器使用 REFIID IID_IMetaDataDispenser 调用 ICorDebugModule::GetMetaDataInterface 方法来获取元数据分配器。
调试器使用 REFIID IID_IMetaDataImport 调用 IMetaDataDispenser::OpenScope 方法来获取 IMetaDataImport 接口。
使用 IMetaDataImport 查找方法
调试器调用 IMetaDataImport::GetMethodProps,通过使用方法标记来查找方法。 方法标记可通过使用 ICorDebugFunction::GetToken 方法获得。 IMetaDataImport::GetMethodProps 将返回该方法的签名。
调试器调用 IMetaDataImport::GetSigFromToken 方法来获取局部签名(即局部变量的签名)。 调试器必须为局部变量的签名提供标记。 调试器可通过调用 ICorDebugFunction::GetLocalVarSigToken 方法来获取此标记。
构造函数签名
签名的格式在“Type and Signature Encoding in Metadata”(元数据中的类型和签名编码)规范中加以说明,并优先于本概述中的信息。
该规范中的“Method Declaration”(方法声明)一节描述了方法签名的格式。 格式如下:一个代表调用约定的单一字节,后跟代表参数计数的单一字节,再后面是类型的列表。 每个类型都可以有不同的大小。 如果指定了可变参数调用约定,则参数计数将是参数的总数,也就是说,固定参数加可变参数的数量。 ELEMENT_TYPE_SENTINEL 字节标记固定参数的结束位置和可变参数的开始位置。
该规范中的“Stand-Alone Signatures”(独立签名)一节描述了局部签名的格式。 独立签名不使用可变参数调用约定。
获得了方法签名和局部签名后,调试器应为新签名分配空间。 然后,调试器应循环访问方法签名。 对于每种类型,它应在新签名中放置后跟类型的 ELEMENT_TYPE_BYREF 字节。 应重复该过程,直至到达方法结尾签名或标记为 ELEMENT_TYPE_SENTINEL 的类型。 接下来,调试器应复制局部签名类型,并将每个类型标记为 ELEMENT_TYPE_BYREF。 如果方法签名具有可变参数调用约定,调试器应复制这些类型并将它们标记为 ELEMENT_TYPE_BYREF。 最后,调试器应更新参数的计数。