利用公共语言运行时 (CLR) 调试 API,工具供应商可以编写调试器来调试运行于 CLR 环境中的应用程序。 要调试的代码可为 CLR 支持的任何代码种类。CLR 调试 API 主要是使用非托管代码实现的。 因此,调试 API 呈现为一组组件对象模型 (COM) 接口。 API 由以下各项组成:
-
CLR 实现的 COM 对象和接口的集合。
-
必须由调试器实现的 COM 回调接口的集合。
CLR 调试方案
以下各部分介绍公共语言运行时调试 API 如何处理典型的调试方案。 请注意,该运行时直接支持某些方案,并且可与当前方法进行互操作来支持其他方案。

在进程外调试中,调试器不在所调试的进程中,而在另一个进程中(即在调试对象外部)。 此方案减少了调试器与调试对象之间的交互。 因此,它可以更准确地描述进程。
CLR 调试 API 直接支持进程外调试。 API 将处理调试器与调试对象托管部分之间的所有通信以支持托管代码调试。
虽然 CLR 调试 API 用于进程外,但某些调试逻辑(例如线程同步)却与调试对象在同一进程内。 大多数情况下,这是应该对调试器保持透明的实现详细信息。 有关线程同步的更多信息,请参见 CLR 调试体系结构。 调试 API 的缺点是在进程外使用时无法用它来检查崩溃转储。

在 .NET Framework 1.0 和 1.1 版本中,CLR 调试 API 支持受限的进程内调试,在受限的进程内调试中,探查器可以使用调试 API 的检查功能。 在 .NET Framework 2.0 中,进程内调试被替换为一组与分析 API 更加一致的功能。 有关这些更改的更多信息,请参见分析概述中的堆栈快照和对象检查功能。

在远程进程调试中,调试器用户界面与所调试的进程不在同一计算机上。 如果调试器和调试对象在同一计算机上运行时相互影响,此方案可能很有用。 这种影响可能由下列原因引起:
-
有限资源。
-
位置依赖性。
-
影响操作系统的 Bug。
CLR 调试 API 不直接支持远程进程调试。 基于 CLR 调试 API 的调试器仍然必须存在于调试对象进程外。 因此,此解决方案需要在调试对象所在的计算机上有一个代理进程。

托管代码和非托管代码通常共存于同一进程中。 通常需要同时调试这两种代码类型。
CLR 调试 API 支持跨越托管代码和非托管代码之间的界限执行单步调试,但是不直接支持非托管代码调试。 然而,通过共享 Win32 调试功能,CLR 调试 API 可以与非托管代码调试器共存。
此外,CLR 调试 API 为调试进程提供了两种选择方案:
-
“软附加”选择方案,在此选择方案中只调试进程的托管部分。 软附加到进程的调试器随后可以从该进程中分离出来。
-
“硬附加”选择方案,在此选择方案中将调试进程的托管和非托管部分,并且通过调试 API 公开所有 Win32 调试事件。

在组件式软件中,可以用不同的语言构建不同的组件。 调试器必须知道语言差异,以便能够用正确的格式显示数据、用正确的语法对表达式进行求值等等。
CLR 调试 API 不对混合语言环境提供任何直接支持,因为 CLR 没有源语言概念。 通过应用调试器的现有源映射功能,应该能够将给定的函数映射到实现该函数所采用的语言。

组件式程序可以包括协作组件,这些组件可以在不同的进程上运行,甚至可以在整个网络中的不同计算机上运行。 调试器应该能够跟踪进程之间以及计算机之间的执行逻辑,以提供所发生事件的逻辑视图。
CLR 调试 API 不对多进程调试提供任何直接支持。 而使用 API 的调试器应该直接提供这类支持,并且用于执行此操作的现有方法应该继续有效。
API 类别
调试 API 包括以下三组接口,这三组接口通常都由 CLR 调试器使用,并均以非托管代码的形式实现:
-
支持对 CLR 应用程序进行调试的接口。
-
允许访问符号调试信息的接口,该信息通常存储在程序数据库 (PDB) 文件中。
-
支持在计算机上查询进程和应用程序域的接口。
调试 API 依赖于另外两组接口:
-
用于处理静态程序信息(比如类和方法类型信息)检查的元数据 API。
-
用于为托管代码调试器支持源级别调试的符号存储区 API。
调试接口也可以划分为下表中所示的功能类别。
API 类别 |
说明 |
---|---|
注册 |
调试器调用以向 CLR 注册并在发生特定事件时请求通知的接口。 |
通知 |
CLR 用于将各种事件通知调试器并返回请求的信息的回调接口。 这些接口必须由调试器实现。 |
断点 |
调试器调用以检索有关断点的信息的接口。 |
执行 |
调试器调用以控制调试对象执行和访问调用堆栈的接口。 |
信息 |
调试器调用以获取有关调试对象的信息的接口。 |
Enumeration |
调试器调用以枚举对象的接口。 |
修改 |
调试器调用以修改所调试代码的接口。 |
以下各部分介绍公共语言运行时 (CLR) 调试服务提供的功能。
附加到程序或启动程序
CLR 允许您将调试器附加到正在运行的程序或启动进程。 CLR 调试服务允许您将调试器附加到引发未经处理的异常的程序,因此它支持实时 (JIT) 调试。 但是,未在可调试模式下运行的程序提供的调试信息可能较少。 程序可以始终使自身运行在可调试模式下以避免此问题。 有关可调试模式的更多信息,请参见以下内容:
-
调试、跟踪和分析
-
应用特性
-
DebuggableAttribute
控制执行
CLR 调试服务提供了多种控制程序执行的方式。 这些方式包括断点、单步执行、异常通知、函数求值以及其他与程序的启动和关闭相关的事件。
CLR 调试 API 只为托管代码提供执行控制。 如果要在非托管代码中实施执行控制,您必须在调试器中单独实现该功能。

通过指定代码以及应进行中断的位置的 Microsoft 中间语言 (MSIL) 或本机偏移量,您可以创建断点。 随后,当遇到断点时,调试器将得到通知。 调试 API 不直接支持条件断点。 但是,通过对表达式求值以响应断点并决定是否将有关停止的信息通知用户,调试器可以实现这些断点。

CLR 调试服务提供了各种各样的单步执行功能。 程序可以采用一次一条指令的方式单步执行代码(单一单步执行),也可以采用一次一系列指令的方式单步执行代码(范围单步执行)。 它可以跳过、进入并单步执行或者跳出函数。 如果发生了中断单步执行操作的异常,CLR 调试服务还可以通知调试器。
尽管调试服务不直接支持单步执行非托管代码,但是,当单步执行操作到达非托管代码时,调试服务将提供回调以将控制交给调试器。 调试服务还提供了允许调试器确定何时将从非托管代码进入托管代码的功能。
CLR 未直接提供源级别单步执行。 调试器可通过将范围单步执行与它自己的源映射信息结合使用来提供此功能。 您可以使用符号存储区接口来获得源级别信息。 有关这些接口的更多信息,请参见诊断符号存储区(非托管 API 参考)。

利用 CLR 调试服务,当托管代码中出现首次异常和第二次异常时,调试器都会收到通知。 每次都可以使用引发的对象进行检查。
CLR 不会处理非托管代码中的本机异常,除非这些异常向上传播到了托管代码。 但是,您仍然可以使用与 CLR 调试服务共享的 Win32 调试服务来处理非托管异常。

在发生多个程序事件时,CLR 调试服务将通知调试器。 这些事件包括进程创建和退出、线程创建和退出、应用程序域创建和退出、程序集加载和卸载、模块加载和卸载以及类加载和卸载。 为了保证良好的性能,您可以为模块禁用类加载和卸载事件。 默认情况下,类加载和卸载事件处于禁用状态。

CLR 调试服务提供了用于挂起和继续执行个别(托管)线程的接口。
检查程序状态
当进程处于停止状态时,CLR 调试服务可以详细检查正在运行托管代码的进程的各部分。 可以对进程进行检查以获得物理线程的列表。
可以对线程进行检查以检测其调用堆栈。 可以在两个级别分解线程的调用堆栈:链级别和堆栈帧级别。 调用堆栈首先分解为链。 链是一个连续的逻辑调用堆栈段。 它包含托管或非托管堆栈帧,但不能同时包含这二者。 此外,单一链中的所有托管调用帧共享同一个 CLR 上下文。 链可以是托管链,也可以是非托管链。
每个托管链都可以另行分解为单一堆栈帧。 每个堆栈帧表示一个方法调用。 您可以查询堆栈帧来获取它正在执行的代码,或者获取它的参数、局部变量和本机寄存器。
非托管链不包含堆栈帧。 相反,它提供分配给非托管代码的堆栈地址范围。 非托管代码调试器负责对堆栈的非托管部分进行解码,并提供堆栈跟踪。
备注
CLR 调试服务不支持在源代码中出现的局部变量的概念。调试器负责将局部变量映射到它们的分配。
CLR 调试服务还提供了对全局变量、类静态变量和线程局部变量的访问。
修改程序状态
CLR 调试服务允许调试器在执行期间更改指令指针的物理位置,尽管这可能是一种危险的操作。 如果满足以下条件,则可以成功更改指令指针:
-
当前指令指针和目标指令指针都位于序列点处。 序列点大致表示语句边界。
-
目标指令指针没有位于异常筛选器、catch 块或 finally 块中。
-
当前指令指针位于 catch 块内,目标指令指针不在 catch 块外部。
-
目标指令指针处在与当前指令指针相同的帧中。
当指令指针的物理位置发生变化时,位于当前指令指针位置的变量将映射到位于目标指令指针位置的变量。 位于目标指令指针位置的垃圾回收引用将正常初始化。
指令指针更改后,CLR 调试服务会将任何缓存的堆栈信息标记为无效,并在下次需要时刷新该信息。 可缓存指向堆栈信息(如帧和链)的指针的调试器应在更改指令指针后刷新此信息。
调试器还可以在程序停止时修改程序的数据。 调试器能够采用与检查类似的方式在函数运行时更改函数的局部变量和参数。 调试器还可以更新数组和对象的字段,以及静态字段和全局变量。
使用“编辑并继续”
在调试会话期间,可以使用“编辑并继续”功能执行下列操作:
-
编辑源代码。
-
重新编译修改后的源代码。
-
保留所调试的可执行文件的其余运行时状态。
-
继续运行调试会话,而不必从头开始重新运行可执行文件。
对函数进行求值
若要对用户表达式和对象的动态属性进行求值,调试器需要能够运行所调试进程的代码。 CLR 调试服务使调试器能够进行函数或方法调用,并使之在调试对象的进程内运行。
CLR 允许调试器停止此类操作,因为这种操作可能很危险(例如,它可能会使现有代码死锁)。 如果成功停止了求值,则会将线程视为如同从未进行过求值一样,只是部分求值会对局部变量产生一些副作用。 如果函数通过某种方式调入了非托管代码或块,则可能无法结束求值。
函数求值完成后,CLR 将使用回调来通知调试器求值是否正确完成或者函数是否引发了异常。 可以使用 ICorDebugValue 和 ICorDebugValue2 方法来检查求值的结果。
要在其中进行函数求值的线程必须在托管代码中可安全进行垃圾回收的安全点处停止。 (未经处理的异常也允许函数求值。)在未优化的代码中,这些安全点非常常见;大多数断点或 MSIL 级单步执行操作将在安全点 1 完成。 但是,这些点在优化的代码中可能非常少见。 有时整个函数可能没有任何安全点。 可安全进行垃圾回收的安全点的频率因函数而异。 即使在未优化的代码中,也可能不会在位置 1 停止。 在优化或未优化的代码中,ICorDebugController::Stop 方法很少位于安全点处。
CLR 调试服务将在线程上设置一个新链,以便开始函数求值和调用请求的函数。 求值一旦开始,就可以看到调试 API 的各个方面:执行控制、检查、函数求值等等。 支持嵌套求值,并将按正常方式处理断点。
动态地注入代码
某些调试器允许用户在**“即时”**窗口中输入并执行任意语句。 CLR 调试服务支持此方案。 在合理的范围内,您可以动态注入的代码没有任何限制。 (例如,不允许非本地 goto 语句。)
动态代码注入是通过将“编辑并继续”操作与函数求值结合使用实现的。 要注入的代码包装在函数中,并通过使用“编辑并继续”注入。 然后,将对注入的函数进行求值。 如果需要,您可以向包装函数提供 ByRef 参数,使副作用产生直接并永久的效果。
支持的环境
CLR 支持的所有处理器和操作系统上都提供了 CLR 调试功能,但以下情况例外:
-
64 位操作系统上不支持“编辑并继续”及混合模式调试。 SetIP 方法(ICorDebugILFrame::SetIP 和 ICorDebugNativeFrame::SetIP)在 64 位操作系统上有额外的限制。 其余的功能在所有处理器上都相同(但存在特定于处理器的数据表示形式,例如,指针大小、寄存器上下文等等)。
-
基于 Win9x 的操作系统上不支持“编辑并继续”及混合模式调试。 其余的功能在所有操作系统上都应相同。 但是,仍存在个别功能的文档中说明的某些特定例外情况。