托管异常处理构建在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代码中引发异常
假设Zoo抛出了一个未在其中处理的Native异常,则该异常将向上传播,因为OS沿着堆栈向上移动,寻找异常的潜在处理程序。在这种情况下,由于堆栈上的下一个帧是Bar,而Bar是受管理的,因此操作系统最终会调用CLR的异常处理程序。由于SEH异常来自Native代码,因此它可能有任何异常代码。例如,它可以是一个C++异常(它具有SEH 0X06D7363的异常代码),或者它可以是一个特定于应用程序的SEH异常代码。关键在于CLR不知道存在的所有SEH异常代码(并且将来会被创建)。从映射到特定托管异常类型的角度来看,只有现有的子集是有意义的。
因此,当CLR的异常处理程序看到Native异常时,它将尝试将最常见的SEH异常代码映射到它们相应的托管异常类型(如前面用一些示例解释的那样)。对于运行时未标识的任何其他SEH异常代码,它将映射到System.runtime.InteropServices.SEH exception类型。
接下来会发生什么?
- 当操作系统从特定SEH异常的抛出点为堆栈上的第一个托管帧调用CLR的异常处理程序(假设堆栈上没有上下文转换)时,此映射仅完成一次。
- 由于此映射是基于SEH异常代码完成的,因此CLR知道给定的托管异常实例是否表示真正的Native SEH异常。例如,CLR知道System.AccessViolationException实例是否表示真正的AV或是托管代码引发的常规托管异常。为什么这很重要?好吧,当CLR确定了异常的损坏严重性时,它就会这样做!
映射完成后,CLR将开始查找异常处理程序的常规异常处理过程。