更新:2007 年 11 月
本主题概述公共语言运行库 (CLR) 调试服务提供的功能。本主题包括下列小节:
附加到程序或启动程序
控制执行
检查程序状态
修改程序状态
使用“编辑并继续”
对函数进行求值
动态地注入代码
附加到程序或启动程序
CLR 允许您将调试器附加到正在运行的程序或启动进程。CLR 调试服务允许您将调试器附加到引发未处理异常的程序,因此它支持实时 (JIT) 调试。但是,未在可调试模式下运行的程序提供的调试信息可能较少。程序可以始终使自身运行在可调试模式下以避免此问题。有关可调试模式的更多信息,请参见以下内容:
控制执行
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 的参数,使副作用产生直接并永久的效果。