2023年1月

什么是CLR Exception---E0434352

CLR异常是.NET应用程序生成的异常类型。异常被封装在从System.exception类派生的类中。它的异常代码是0xE0434352,代码的后面三个字节对应assic码的 "CCR",所以又叫CCR异常。

利用公共语言运行时 (CLR) 调试 API,工具供应商可以编写调试器来调试运行于 CLR 环境中的应用程序。 要调试的代码可为 CLR 支持的任何代码种类。CLR 调试 API 主要是使用非托管代码实现的。 因此,调试 API 呈现为一组组件对象模型 (COM) 接口。 API 由以下各项组成:

  • CLR 实现的 COM 对象和接口的集合。

  • 必须由调试器实现的 COM 回调接口的集合。

CLR 调试方案

以下各部分介绍公共语言运行时调试 API 如何处理典型的调试方案。 请注意,该运行时直接支持某些方案,并且可与当前方法进行互操作来支持其他方案。

Bb397953.collapse_all(zh-cn,VS.110).gif进程外调试

在进程外调试中,调试器不在所调试的进程中,而在另一个进程中(即在调试对象外部)。 此方案减少了调试器与调试对象之间的交互。 因此,它可以更准确地描述进程。

CLR 调试 API 直接支持进程外调试。 API 将处理调试器与调试对象托管部分之间的所有通信以支持托管代码调试。

虽然 CLR 调试 API 用于进程外,但某些调试逻辑(例如线程同步)却与调试对象在同一进程内。 大多数情况下,这是应该对调试器保持透明的实现详细信息。 有关线程同步的更多信息,请参见 CLR 调试体系结构。 调试 API 的缺点是在进程外使用时无法用它来检查崩溃转储。

Bb397953.collapse_all(zh-cn,VS.110).gif进程内调试

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

Bb397953.collapse_all(zh-cn,VS.110).gif远程进程调试

在远程进程调试中,调试器用户界面与所调试的进程不在同一计算机上。 如果调试器和调试对象在同一计算机上运行时相互影响,此方案可能很有用。 这种影响可能由下列原因引起:

  • 有限资源。

  • 位置依赖性。

  • 影响操作系统的 Bug。

CLR 调试 API 不直接支持远程进程调试。 基于 CLR 调试 API 的调试器仍然必须存在于调试对象进程外。 因此,此解决方案需要在调试对象所在的计算机上有一个代理进程。

Bb397953.collapse_all(zh-cn,VS.110).gif非托管代码调试

托管代码和非托管代码通常共存于同一进程中。 通常需要同时调试这两种代码类型。

CLR 调试 API 支持跨越托管代码和非托管代码之间的界限执行单步调试,但是不直接支持非托管代码调试。 然而,通过共享 Win32 调试功能,CLR 调试 API 可以与非托管代码调试器共存。

此外,CLR 调试 API 为调试进程提供了两种选择方案:

  • “软附加”选择方案,在此选择方案中只调试进程的托管部分。 软附加到进程的调试器随后可以从该进程中分离出来。

  • “硬附加”选择方案,在此选择方案中将调试进程的托管和非托管部分,并且通过调试 API 公开所有 Win32 调试事件。

Bb397953.collapse_all(zh-cn,VS.110).gif混合语言环境

在组件式软件中,可以用不同的语言构建不同的组件。 调试器必须知道语言差异,以便能够用正确的格式显示数据、用正确的语法对表达式进行求值等等。

CLR 调试 API 不对混合语言环境提供任何直接支持,因为 CLR 没有源语言概念。 通过应用调试器的现有源映射功能,应该能够将给定的函数映射到实现该函数所采用的语言。

Bb397953.collapse_all(zh-cn,VS.110).gif多个进程和分布式程序

组件式程序可以包括协作组件,这些组件可以在不同的进程上运行,甚至可以在整个网络中的不同计算机上运行。 调试器应该能够跟踪进程之间以及计算机之间的执行逻辑,以提供所发生事件的逻辑视图。

CLR 调试 API 不对多进程调试提供任何直接支持。 而使用 API 的调试器应该直接提供这类支持,并且用于执行此操作的现有方法应该继续有效。

API 类别

调试 API 包括以下三组接口,这三组接口通常都由 CLR 调试器使用,并均以非托管代码的形式实现:

  • 支持对 CLR 应用程序进行调试的接口。

  • 允许访问符号调试信息的接口,该信息通常存储在程序数据库 (PDB) 文件中。

  • 支持在计算机上查询进程和应用程序域的接口。

调试 API 依赖于另外两组接口:

  • 用于处理静态程序信息(比如类和方法类型信息)检查的元数据 API。

  • 用于为托管代码调试器支持源级别调试的符号存储区 API。

调试接口也可以划分为下表中所示的功能类别。

API 类别

说明

注册

调试器调用以向 CLR 注册并在发生特定事件时请求通知的接口。

通知

CLR 用于将各种事件通知调试器并返回请求的信息的回调接口。 这些接口必须由调试器实现。

断点

调试器调用以检索有关断点的信息的接口。

执行

调试器调用以控制调试对象执行和访问调用堆栈的接口。

信息

调试器调用以获取有关调试对象的信息的接口。

Enumeration

调试器调用以枚举对象的接口。

修改

调试器调用以修改所调试代码的接口。

公共语言运行时 (CLR) 调试 API 专门用作操作系统内核的一部分。 在非托管代码中,当程序生成异常时,内核将暂停执行进程,并使用 Win32 调试 API 将异常信息传递给调试器。 CLR 调试 API 可以为托管代码提供相同功能。 当托管代码生成异常时,CLR 调试 API 将暂停执行进程,并将异常信息传递给调试器。

进程体系结构

CLR 调试 API 包括以下两个主要组件:

  • 调试 DLL,始终加载到与正在调试的程序相同的进程中。 运行时控制器负责与 CLR 进行通信并对正在运行托管代码的线程进行执行控制和检查。

  • 调试器接口,加载到与正在调试的程序不同的进程中。 调试器接口负责代表调试器与运行时控制器进行通信。 它还负责处理来自正在调试的进程的 Win32 调试事件,要么处理这些事件,要么将这些事件传递给非托管代码调试器。 调试器接口是 CLR 调试 API 中唯一具有公开 API 的部件。

CLR 调试 API 不支持跨计算机或跨进程的远程使用;也就是说,使用该 API 的调试器必须从其自己的进程内执行此操作,如下面的 API 体系结构示意图所示。 此图显示了 CLR 调试 API 的不同组件所在的位置以及它们与 CLR 和调试器的交互方式。

CLR 调试 API 体系结构

CLR 调试体系结构

托管代码调试器

可以构建一个只支持托管代码的调试器。 通过使用“软附加”机制,CLR 调试 API 使这种调试器能够根据需要附加到进程。 软附加到进程的调试器随后可以从该进程中分离出来。

线程同步

CLR 调试 API 具有与进程体系结构有关的相互冲突的要求。 一方面,将调试逻辑与正在调试的程序保持在相同进程中的原因很多,而且也很有说服力。 例如,数据结构复杂,经常要通过函数而不是通过固定内存布局来处理它们。 直接调用函数(而不是从进程外尝试对数据结构进行解码)要容易得多。 将调试逻辑保持在同一进程中的另一个原因是消除了进程间的通信开销,从而提高了性能。 最后,CLR 调试的一项重要功能就是能够在调试对象所在的进程中运行用户代码,很明显,这需要与调试对象进程进行一些协作。

另一方面,CLR 调试必须与非托管代码调试共存,后者只能从外部进程中正确执行。 此外,进程外调试器比进程内调试器更加安全,因为在进程外调试器中最大程度地减小了调试器的操作与调试对象进程之间的相互影响。

由于存在这些相互冲突的要求,所以 CLR 调试 API 会将各种方法的一些内容组合在一起。 主要的调试接口位于进程外,并且与本机 Win32 调试服务共存。 但是,CLR 调试 API 添加了与调试对象进程同步的功能以便能够在用户进程中安全地运行代码。 为了执行此同步操作,API 将与操作系统和 CLR 进行协作以便在进程中的所有线程不会中断操作的位置处暂停这些线程并使运行时处于不相干的状态。 然后,调试器可以在特殊的线程中运行代码,该线程可以检查运行时的状态并根据需要调用用户代码。

当托管代码执行断点指令或生成异常时,将通知运行时控制器。 此组件将确定正在执行托管代码的线程以及正在执行非托管代码的线程。 通常,在运行托管代码的线程达到可以安全挂起的状态之前,将允许这些线程继续执行。 例如,它们可能必须完成正在进行的垃圾回收。 当托管代码线程达到安全状态时,所有线程都将被挂起。 然后,调试器接口会通知调试器已经收到了断点或异常。

当非托管代码执行断点指令或生成异常时,调试器接口组件将通过 Win32 调试 API 接收通知。 此通知将传递给非托管调试器。 如果调试器确定需要执行同步(例如,为了能够检查托管代码堆栈帧),则调试器接口必须首先重新启动停止的调试对象进程,然后通知运行时控制器执行同步。 然后,当同步已完成时将会通知调试器接口。 此同步对于非托管调试器是透明的。

在同步进程期间,不得执行生成断点指令或异常的线程。 为了便于执行此规定,调试器接口在线程的筛选器链中放置了一个特殊的异常筛选器来控制线程。 当重新启动线程时,线程将进入异常筛选器,异常筛选器会将线程交给运行时控制器控制。 该继续处理异常(或该取消异常)时,筛选器会将控制权返还给线程的常规异常筛选器链或者返回正确的结果以继续执行。

在极少数情况下,生成本机异常的线程可能拥有重要的锁,只有先打开这些锁,然后才能完成运行时同步。 (通常,这些锁将为低级别库锁,例如 malloc 堆上的锁。)在这些情况下,同步操作一定会超时并且将会失败。 这也将导致需要同步的某些操作失败。

进程中的帮助器线程

每个 CLR 进程中只使用一个调试器帮助器线程以确保 CLR 调试 API 正常运行。 此帮助器线程负责处理由调试 API 提供的许多检查服务,以及在某些情况下协助线程同步。 您可以使用 ICorDebugProcess::GetHelperThreadID 方法识别帮助器线程。

与 JIT 编译器交互

为了使调试器能够调试实时 (JIT) 编译的代码,CLR 调试 API 必须能够将 Microsoft 中间语言 (MSIL) 版本的函数中的信息映射到本机版本的函数中。 此信息包括代码中的序列点以及局部变量位置信息。 在 .NET Framework 1.0 和 1.1 版中,只有当运行时处于调试模式时,才会产生此信息。 在 .NET Framework 2.0 中,始终会产生此信息。

另外,可以对 JIT 编译的代码进行高度优化。 优化(例如公共子表达式消除、函数内联展开、循环展开、代码检查等)可能会导致函数的 MSIL 代码与被调用执行的本机代码之间的相互关系丢失。 因此,这些主动代码优化方法将会严重影响 JIT 编译器提供正确映射信息的能力。 所以,在调试模式下运行运行时时,JIT 编译器将不会执行某些优化。 此限制使调试器能够准确地确定所有局部变量和参数的源行映射和位置。

调试模式

CLR 调试 API 提供了以下两种特殊的调试模式:

  • “编辑并继续”模式。 在此情况下,运行时将以不同方式运行以便以后能够更改代码。 这是因为某些运行时数据结构的布局必须不同以便支持“编辑并继续”。 因为这对性能有负面影响,所以除非想使用“编辑并继续”功能,否则请不要使用此模式。

  • 调试模式。 此模式使 JIT 编译器能够忽略优化。 因此,它可以使执行的本机代码与高级语言源代码更加匹配。 除非需要,否则请不要使用此模式,因为这种模式也对性能有负面影响。

如果在“编辑并继续”模式外调试程序,则不支持“编辑并继续”功能。 如果在调试模式外调试程序,则将仍然支持大多数调试功能,但优化可能会引起异常行为。 例如,单步执行看来像是在方法中的行与行之间随机跳转,内联方法可能未在堆栈跟踪中出现。

如果调试器在运行时初始化自己之前获取了进程控制权,则调试器可以通过 CLR 调试 API 以编程方式启用“编辑并继续”模式和调试模式。 这足以能够达到许多目的。 但是,附加到已经运行了一段时间(例如在 JIT 调试期间)的进程的调试器将无法启动这些模式。

为了帮助处理这些问题,可以独立于调试器在 JIT 模式或调试模式下运行程序。 有关启用调试的方法的信息,请参见调试、跟踪和分析。

JIT 优化可能使应用程序的可调试性降低。 CLR 调试 API 使用经过优化的 JIT 编译代码来启用堆栈帧和局部变量检查。 单步执行虽然受支持,但可能不精确。 您可以运行一个程序来指示 JIT 编译器禁用所有 JIT 优化以产生可调试的代码。

什么是CLR Exception---E0434F4D

就是公共语言运行时(CLR)异常,异常代码为0xE0434F4D。因此任何托管异常,如NullReferenceException、invalidooperationexception或SQLException…基本上所有托管异常都在本机中称为0xe0434f4d。代码的后面三个字节对应assic码的 "COM",所以又叫COM异常。

异常代码e043432可以是任何内容。它只是表示一个CLR异常,它有很多很多类型。要获得更多信息,您必须使用类似的工具!PrintException输出异常的信息。

托管异常处理构建在Windows操作系统的结构化异常处理之上,通常称为SEH。这意味着CLR了解如何在SEH和托管异常系统之间进行互操作,这是一个非常关键的点,因为SEH基于异常代码的概念,而托管异常处理则表示使用托管类型的异常。CLR相应地将SEH异常映射到托管异常,具体取决于引发SEH异常的方式和引发者。

注意:下面的讨论重点是运行在Windows操作系统上的桌面CLR。虽然讨论的目的是帮助理解这个概念,但是它使用了一些将来可能会改变的实现细节来说明。

托管代码中的同步异常(Synchronous exceptions)

当托管代码使用throw关键字引发异常时,它已经实例化了一个托管异常对象,该对象将表示引发的异常。这将传递给CLR,CLR在线程上设置一些与异常相关的状态,并调用Kernel32的RaiseException API来引发托管异常。此API的第一个参数是引发异常的SEH异常代码,CLR传递0xE0434F4D(托管异常SEH代码)。

这时,操作系统进入处理场景,开始在引发异常的线程堆栈上寻找SEH异常处理程序。CLR将其函数之一注册为OS的异常处理程序,以处理托管代码引起的异常。当它看到CLR SEH异常代码时,它知道正在引发托管异常,并继续查找线程状态以检索与异常相关的详细信息(例如,标识表示引发异常的托管异常对象)。

因此,在同步托管抛出的情况下,很容易将SEH异常映射到托管异常类型。

托管代码中的异步异常(Asynchronous exceptions)

简单地说,异步异常是在没有显式抛出的情况下引发的异常。如果执行算术操作(例如除以零异常)或使用可能导致访问冲突(AV)等异常的不安全托管代码,则在托管代码中可能会发生这种情况。异步异常的有趣之处在于,它们是使用它们唯一的SEH异常代码来表示的。例如AV用0xc000005表示,除以0(整数)用0xC0000094表示,除以0(浮点数)用0xC000008E表示,这里列出了常见的异常,异常代码值可以在WinNT.h中找到。

当在托管代码中引发此类异常时,操作系统再次开始在引发异常的线程堆栈上查找异常处理程序。当调用CLR的异常处理程序时,它知道所讨论的异常不是托管代码同步抛出的,因为异常代码不是CLR SEH异常代码。因此,它不再查找与异常相关的详细信息的线程状态,而是将SEH异常映射到一个托管异常类型。例如,除以零异常(整数和浮点)使用System.DivideByZeroException表示。

同样,当在托管代码中生成真正的AV时,它的异常代码为0xc000005。由于托管代码具有空引用的概念,因此运行时很少再进行检查来确定AV是否表示尝试使用空引用。如果是,则映射到System.NullReferenceException。否则,它将映射到System.AccessViolationException类型(在v2.0和更高版本的运行时中)。

在Native代码中引发异常

CLR查看SEH异常的另一种方式是,托管代码使用平台调用服务(简称PInvoke)等机制调用Native代码。