zoukankan      html  css  js  c++  java
  • 深入了解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异常处理的内部工作原理,大多数情况下,程序员们只要了解清楚所使用的编程语言所提供的异常处理功能,并会用就行了。

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

  • 相关阅读:
    MS CRM 2011的自定义和开发(10)——CRM web服务介绍(第一部分)——IDiscoveryService
    MS CRM 2011的自定义和开发(7)——视图编辑器(第二部分)
    MS CRM 2011 SDK 5.06版本已经发布
    MS CRM 2011的自定义和开发(11)——插件(plugin)开发(一)
    近来遇到的MS CRM 2011方面的几个问题
    MS CRM 2011的自定义与开发(6)——表单编辑器(第二部分)
    Microsoft Dynamics CRM 2011中,Lookup字段的赋值
    MS CRM 2011的自定义和开发(6)——表单编辑器(第三部分)
    Visual Studio 目标框架造成 命名空间“Microsoft”中不存在类型或命名空间名称“Crm”。是否缺少程序集引用中错误的处理
    一步步学习Reporting Services(二) 在报表中使用简单的参数作为查询条件
  • 原文地址:https://www.cnblogs.com/bitfan/p/1616550.html
Copyright © 2011-2022 走看看