• 深入了解CLR异常处理机制


     

    深入了解CLR异常处理机制

         CLR实现的异常处理具有以下特点:

           1)处理异常时不用考虑生成异常的语言或处理异常的语言。换句话说,可以在C#程序中捕获用Visual Basic.NET编写的组件中引发的异常。

           2)异常处理时不要求任何特定的语言语法,而是允许每种语言定义自己的语法。

           3)允许跨进程甚至跨计算机边界引发异常。

           4)以一致的方式处理托管和非托管代码引发的异常。

           任何一种.NET编程语言所实现的异常捕获功能,本质上都是CLR异常处理系统所提供的功能的一个子集。

           如果使用IL编写程序,则可以使用CLR异常处理系统的所有功能。

           显然直接使用IL编程不太现实,但如果希望能深入地了解CLR异常处理系统,分析编译器生成的IL指令代码是一个好方法。

    1 方法的异常处理表

           请看以下这个简单的C#程序(参见示例项目CatchException):

        class Program

        {

            static void Main(string[] args)

            {

                try

                {

                    int number = Convert.ToInt32(Console.ReadLine());

                }

                catch (FormatException ex)

                {

                    Console.WriteLine(ex.Message);

                }

                finally

                {

                    Console.WriteLine("finally");

                }

            }

        }

           使用ilasm工具反汇编出来的代码框架如 1所示:

     

     1 CLR级别实现异常处理的代码框架

     

           上述代码中涉及到的与异常处理相关的IL指令有两条:

           1leave.s <int32>:离开受保护块,从当前位置转移并执行指定地址处的IL指令。

           2endfinally:标识finally语句块结束。

           从上述代码中我们可以知道:

           C#编程语言中“单层”的try…catch…finally结构会被转换为“两层嵌套”的类似结构,CLR通过执行leave指令在IL汇编程序的.trycatchfinally指令块间跳转,实现应用程序所定义的异常捕获和处理工作。

           1所示Main()方法IL代码是经过ildasm程序出于易于阅读的目的而调整过的,它实际上“隐瞒”了真正的IL指令代码序列。

           请从ildasm的“View”菜单中取消“Expand try/catch”选项(默认情况下此选项是选中的),可以看到C#编译器生成的IL代码的“真面目”( 2)。

    2“真实”的IL指令代码序列

           2所示,具体功能代码被统一地放置在方法IL代码的前半部分,而用于实现异常捕获的代码放在方法IL代码的后半部分,我们将其称为“异常处理表(Exception Handling Table”,“ret”指令是两部分的天然分界线。

           C#编译器通过在合适的地方插入leave.s指令使得在无异常情况下永远不会执行到异常处理代码。

           异常处理表中的每个表项是一个“异常处理子句(Exception Handling Clause”,IL汇编程序使用.trycatchhandlerfinally关键字,配合相应地址给前面的功能代码“自然分块”。

           位于方法IL代码后半部分的异常处理表是CLR实现异常捕获的关键。

           下面我们简要介绍一下CLR如何使用异常处理表捕获并处理异常。

    2 CLR如何捕获并处理异常

           对于任何一个.NET应用程序中的类,其所包容的方法都包容着一个异常处理表,如果此方法中没有使用try…catch…finally,则此表为空(即此方法生成的IL指令中不包容任何的异常处理子句)。

           .NET应用程序运行时,如果正在执行的某个方法引发了一个异常,CLR会首先将相应的异常对象推入计算堆栈,然后扫描此方法所包容的异常处理表查找处理程序,其处理过程可以简述如下:

           CLR获取引发异常的IL指令地址,然后从上到下地扫描异常处理表,取出每个catch子句中“.try”关键字后面跟着的用于定位“块”的起始和结束地址,判断一下引发异常的IL指令地址是否“落”入此地址范围中。如果是,取出“catch”关键字后跟着的异常类型,比对一下是否与抛出的异常对象类型一致(或相兼容),如果这个条件得到满足,CLR取出handler后的两个IL地址,“准备”执行这两个地址指定范围的IL指令(这就是catch指令块中的异常处理代码)。

           如果本方法所包容的异常处理表中找不到合适的catch子句,CLR会依据引发异常的线程所关联的方法调用堆栈,查找此方法的调用者所包容的异常处理表。

           此过程将一直进行下去,直到找到了一个可以处理此异常的处理程序为止。

           假设CLR在整个方法调用链的某个“环节”(即调用此方法的某个“祖先”方法)所包容的异常处理表中找到了可处理此异常的catch异常处理子句,它就作好了执行此子句所定义的异常处理指令代码块的“准备”。

           “扫描并查找相匹配的catch子句”过程,是CLR异常处理流程的第一轮。

           当找到了合适的异常处理代码后,CLR再“回到原地”,再次扫描引发异常方法所包容的异常处理表,这回,CLR关注的不再是catch子句,而是finally子句,如果找到了合适的finally子句(只需判断一下引发异常的IL指令地址是否“落入”某finally子句所监视的IL指令地址范围之内即可),CLR执行finally子句所指定的处理指令(即其handler部分所定范围内的IL指令)。

           “扫描并查找相匹配的finally子句”过程,是CLR处理异常流程的第二轮。

           这“第二轮”的扫描,开始于引发异常的方法,结束于最顶层的包容了那个引发异常的方法的方法(这句话很拗口,举个例子就清楚了,比如,如果你有一个嵌套了很深的函数调用语句,并且在被调用的最底层的函数中引发了异常,而你在顶层Main()函数中又用try...catch...finally包围了这一函数调用语句,则第2轮扫描会“直达”最顶层Main()方法的异常处理表,不会中途停止于找到了合适catch子句的那个中间“站”。

      在所有“下层”finally子句执行结束之后,相应的catch子句所指定的异常处理代码块才开始执行。之后,与此catch子句“同层”的finally子句所指定的异常处理代码块得到执行。

         但事情还没完,现在轮到所有包容被执行catch子句所在方法的“父辈”方法中的finally子句执行。

           经过两轮的扫描,CLR就完成了对.NET应用程序引发异常的捕获与处理工作。

           这里还遗留着一个问题:

           CLR找不到合适的catch异常处理子句怎么办?

           如果某.NET应用程序中根本没有定义处理某种异常类型的代码,而此程序在运行时又真的引发了这种类型的异常(真是哪壶不开提哪壶),那么CLR在第一轮扫描过程中,会一直“上溯”到Main()方法所包容的异常处理表,然后“无功而返”。

           紧接CLR会进行第二轮的扫描,执行所有“应该被执行”的finally子句。

           故事的尾声是:在执行完了所有finally代码后,CLR强制中止此进程所创建的所有线程(哪怕它们运行正常),由操作系统显示一个“出错”对话框,等用户响应后,或结束或附加一个调试器来调试这个进程。

    3 CLR的异常筛选和故障响应

       上一小节介绍了.NET应用程序中的异常处理表,并介绍了构成异常处理表中的两种类型的异常处理子句(catchfinally)。事实上,CLR异常处理表中还可以包容另两种类型的子句:异常筛选(filter)子句和故障(fault)响应子句。

           我们分别来看看这两种子句有什么特殊性。

    1 异常筛选

           Visual Basic.NET中,有一个When关键字用于控制是否捕获特定的异常。

           请看以下代码(示例程序VBException):

    Module Module1

        Sub Main()

            Dim ShouldCatch As Boolean = True

            Try

                Dim number As Integer = Convert.ToInt32(Console.ReadLine())

            Catch ex As FormatException When ShouldCatch

                Console.WriteLine(ex.Message)

            End Try

        End Sub

    End Module

           ShouldCatch=True时,FormatException 对象将被捕获被处理,否则,此异常将导致进程被CLR强行中止。

           下面列出ildasm反汇编示例程序集得到的IL代码,代码较长,为了方便阅读,我用“====”划分出了其中的代码块,并加了详细的注释,对于不熟悉IL指令或没有耐心看这些枯燥代码的读者,可以直接看代码后的文字说明:

    .method public static void Main() cil managed

    {

     .entrypoint

     .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )

     // Code size       60 (0x3c)

     .maxstack 3

     .locals init ([0] bool ShouldCatch,

               [1] int32 number,

               [2] class [mscorlib]System.FormatException ex)

     //=================================================

     // IL_00000001:初始化ShouldCatch=True

     IL_0000: ldc.i4.1

     IL_0001: stloc.0

    //================================================

     //0002000d:“保护块”

     IL_0002: call       string [mscorlib]System.Console::ReadLine()

     IL_0007: call       int32 [mscorlib]System.Convert::ToInt32(string)

     IL_000c: stloc.1

     IL_000d: leave.s    IL_003b //没有异常,则跳去执行ret指令结束

    //=====================================================

    //IL_000f0026:异常筛选块,最终结果(为01)将会被压入到计算堆栈中

    //判断异常是否是FormatException

      IL_000f: isinst [mscorlib]System.FormatException

     IL_0014: dup //复制计算堆栈栈顶值,再压入堆栈

     IL_0015: brtrue.s   IL_001b //如果异常是FormatException,跳转到IL_001b

     //如果异常不是FormatException,将0压入堆栈,然后跳转到IL_0026

     IL_0017: pop //出栈

     IL_0018: ldc.i4.0 

     IL_0019: br.s  IL_0026

     IL_001b: dup //复制计算堆栈栈顶值,再压入堆栈

     IL_001c: stloc.2 //保存异常对象到方法局部变量ex

     IL_001d: call void [Microsoft.VisualBasic]

                Microsoft.VisualBasic.CompilerServices.ProjectData::SetProjectError(

                        class [mscorlib]System.Exception)

     // IL_0022IL_0024:将ShouldCatch值和常量0先后压入计算堆栈,

    // 比较这两个值谁大谁小,结果(为01)再压入计算堆栈

     IL_0022: ldloc.0

     IL_0023: ldc.i4.0

     IL_0024: cgt.un

     //依据两数比较结果决定是否调用IL_ 0028003b所定义的异常处理块

     IL_0026: endfilter

    //======================================================

     //IL_ 0028003b:异常处理块

     IL_0028: pop

     IL_0029: ldloc.2

     IL_002a: callvirt   instance string [mscorlib]System.Exception::get_Message()

     IL_002f: call   void [mscorlib]System.Console::WriteLine(string)

     IL_0034: call void [Microsoft.VisualBasic]

                    Microsoft.VisualBasic.CompilerServices.ProjectData::ClearProjectError()

     IL_0039: leave.s    IL_003b

     IL_003b: ret //方法运行结束

    //=========================================

    //异常处理表

     .try IL_0002 to IL_000f filter IL_000f handler IL_0028 to IL_003b

    } // end of method Module1::Main

           解析一下上述IL代码中的关键点。

           在方法最后的“异常处理表”中包容了一个“异常筛选”子句,其中明确地定义从IL_0002IL_000f是“保护块”,如果在此范围内的IL指令引发了异常,则将跳去执行IL_000f处的指令:

     IL_000f: isinst [mscorlib]System.FormatException

           isinst指令将判断一下程序抛出的异常是不是FormatException。如果不是,后面的IL指令会将0压入堆栈,否则,依据ShouldCatch变量的值,将01压入堆栈。

        IL_000fIL_0026构成了“异常筛选块”,此代码块的执行结果不是0就是1(注意此结果会被压入计算堆栈)。

           “异常筛选块”中最后endfilter指令非常关键,它检查保存在计算堆栈中的值,如果是1,则结束第一轮扫描,并为在第2轮扫描中执行异常处理表中所定义的“异常处理块(示例程序是从IL_ 0028003b范围内的IL指令块)”做好了准备,此代码块其实对应着Visual Basic.NET示例程序中放在Catch语句块中的VB代码。

           如果计算堆栈中的值是0CLR将跳过本方法中定义的异常处理块,转去搜索上一级“父辈”方法的异常处理表,重复上述处理过程,如果还找不到合适的异常处理子句,再去搜索“父辈的父辈”,最后可能会搜索到最远古的“北京猿人”级别的方法(比如Main()方法)才结束。这就是CLR使用filter子句的第一轮搜索过程。

           紧接着CLR会进行第二轮搜索,执行合适的finally子句(其实还包括后面马上要介绍的fault子句)所定义的指令代码块。其处理流程与上一小节介绍的一样,就不再废话了。

    注意:

           由于C#编译器不生成使用CLR“异常筛选”功能的IL指令,因此,C#语言不能使用CLR提供的“异常筛选”功能。

    2 故障响应

           除了catchfinallyfilter三种类型的异常处理子句,CLR还支持一种名为“fault”异常处理子句。

           它的样子是这样的:

             .try 起始地址 to 结束地址 fault handler 起始地址 to 结束地址

           fault异常处理子句的功能与finally子句非常类似,不同之处在于:

           无论被保护块是否引发了异常,finally子句所定义的处理指令块都会被执行。而fault子句所定义的处理指令块“仅当”被保护块引发异常(不管是什么类型的异常)时被执行,如果被保护块未引发任何异常,则不会执行此fault子句所定义的处理指令块。

           由此可知,可以使用 fault异常处理子句让CLR“响应”应用程序引发的任何一种异常,所以我们可将fault子句称为“故障响应”子句。fault子句的功能类似于消防员的职责(平时无事,一旦有突发火灾发生,灭火就是消防员义不容辞的责任)。

    3 小结

           本文深入介绍了CLR所提供的异常处理机制,可以看到, C#Visual Basic.NET等编程语言所提供的异常处理机制都只是CLR异常处理系统功能的子集。

           应该来说,在实际开发中很少有这个必要去深入探究CLR异常处理的内部工作原理,大多数情况下,程序员们只要了解清楚所使用的编程语言所提供的异常处理功能,并会用就行了。

           然而,如果您的好奇心还没有被中国的应试教育所泯灭的话,不满足于“知其然”,而且要“知其所以然”,那么,我相信本文的内容能部分地满足您对技术“刨根问底”的需求。

  • 相关阅读:
    命令模式
    责任链模式
    代理模式
    享元模式
    195 Tenth Line
    test命令
    read命令
    echo命令
    java反射
    http状态码
  • 原文地址:https://www.cnblogs.com/bitfan/p/1616550.html
Copyright © 2020-2023  润新知