附图简述
图1是示出典型的编译器及其组件的各种处理阶段的流程图。
图2是示出用于使用能够表示多个语言专用异常处理模型的统一异常处理框架来生成异常处理指令的中间表示的系统的框图。
图3A是示出用于使用能够表示多个语言专用异常处理模型的统一异常处理框架来生成异常处理指令的中间表示的方法的流程图。
图3B是示出用于读取软件的中间表示并从其中生成可执行版本的方法的流程图。
图4是示出用于CIL和MSIL语言的多个IL读取器的图2的系统的一个实施例的框图。
图5是用于中间表示的指令的数据结构的一个实施例的图示。
图6是未保护的导致异常的指令的伪代码表示的清单。
图7是图6的代码的中间表示的清单。
图8是具有由finally块保护的非导致异常的指令的try代码段的伪代码表示的清单。
图9是图8的代码的中间表示的清单。
图10是具有由finally块保护的导致异常的指令的try代码段的伪代码表示的清单。
图11是图10的代码以及适当的异常处理程序标签的中间表示的清单。
图12是具有由两个过滤器和两个catch块保护的导致异常的指令的try代码段的伪代码表示的清单。
图13是图12的代码以及涉及catch块的适当处理程序标签和过滤器的中间表示的清单。
图14是具有由两个过滤器、两个catch块和结束代码块保护的导致异常的指令的try代码段的伪代码表示的清单。
图15是图14的代码以及涉及catch和结束块的适当处理程序标签和过滤器的中间表示的清单。
图16是由catch块保护的嵌套try代码段的伪代码表示的清单。
图17是图16的代码以及涉及嵌套与外部的catch和结束块的适当处理程序标签和过滤器的中间表示的清单。
图18是示出用于将异常处理构造从中间语言转换成另一中间表示的一种方法的框图。
图19A是异常处理数据表的数据结构的图示。
图19B是用于将偏移量映射到其标签的标签映射的一个图示。
图19C是在将受保护的块映射到其各自的偏移量之后的标签映射的另一图示。
图20是用于使用异常处理数据表以及受保护的块及其处理程序和用于生成中间表示的目标块之间的包含信息的一种方法的流程图。
图21是示出用于确定受保护的块及其处理程序和目标块之间的包含关系的范围树图的一个示例的图示。
图22是示出局部对象的构造和析构的C++程序的清单。
图23是用于在对象的构造和析构期间表达可能的异常处理路径的伪代码表示的清单。
图24是图22和23的代码的中间表示的清单。
图25是示出表达式临时对象的条件构造的C++程序的清单。
图26是用于表达表达式临时对象的条件构造和析构的可能异常路径的伪代码表示的清单。
图27是图26的代码的中间表示的清单。
图28是传值返回对象的C++程序的清单。
图29A是图28所示的传值对象析构的可能异常路径的中间表示的清单。
图29B是图29A的清单的延续。
图30是传值抛出对象的C++程序的清单。
图31是表达图30所示的抛出值类型对象的可能异常路径的伪代码表示的清单。
图32是图31的中间表示的清单。
图33是由异常代码块保护的try代码段的伪代码表示的清单。
图34是图33的代码的中间表示的清单。
图35是用于将以后缀表示法形式表达的中间语言转换成另一中间表示的示例性方法的流程图。
图36是通过读取以后缀表示法形式表达的代码中构建中间表示的数据结构的一种实现的图示。
图37是示出用于使用图36的数据结构来通过读取以后缀表示法形式表达的代码构建中间表示的示例性方法的流程图。
图38A是使用后缀表示法实现的图22的示例性代码段的清单。
图38B是图38A的延续。
图38C是图38A和B的进一步延续。
图39是示出在将图38的代码转换成中间表示的过程中图36的数据结构的状态的框图。
详细描述
异常处理构造的语言无关中间表示
图2示出了用于由编译器后端240为代码优化而实现的用于多个源语言(205-208)的统一异常处理中间表示230的系统200。如图2所示,系统200包括用于多个源代码表示205-208的每一个的中间语言(IL)表示210-213,它由将多个IL表示210-213转换成单个中间表示230的IL读取器220分析或读取。IL表示是比中间表示230更高级的中间表示,并可以用任意数量的公知中间语言,如MSIL(Microsoft CLR)(用于C#、Visual Basic、JScript、C和FORTRAN)以及CIL(用于C++)来表达。即使用于生成用于多个语言的统一异常处理框架的系统200被示出为具有用于多个源语言的单个IL读取器处理,也可能实现多个这样的读取器,其每一个都对应于IL表示210-213中的一个或多个。
图3A示出了用于使用IL读取器220来生成用于以多种不同的源语言表达的异常处理构造的统一中间表示集的一般总体方法。在310,由读取器220接收软件的中间语言表示(例如,源代码文件的中间语言表示),且在315,读取或分析该文件以标识IL代码流中的异常处理构造(320)。然后在330,读取器220(也可以被认为是虚拟机)生成先前在320标识的异常处理构造的单个统一的中间表示。这一异常处理框架然后可用于简化诸如代码优化和代码生成等编译器后端处理。
具有异常处理构造的软件的统一中间表示可显式地表达软件的异常处理控制。图3B示出了用于从软件的统一中间表示生成可执行代码的方法350。这一方法可由例如编译器或其它软件开发工具在为软件生成可执行版本(例如,机器专用代码或其它目标代码)时使用。
在360,读取该统一中间表示(例如,由编译器或其它软件开发工具)。例如,可使用由图3A的方法生成的统一中间表示。可在需要时执行对该统一中间表示的其他变换、转换或优化。
在370,生成软件的计算机可执行版本(例如,由编译器或其它软件开发工具)。软件的计算机可执行版本基于该统一中间表示执行软件的异常处理控制流。
图4示出了用于生成以多个IL表示的形式表达的多个源语言内的异常处理构造的简单且统一的中间表示的系统的另一实施例。如图4所示,微软的.NET框架(例如,C#、C、Microsoft Visual Basic、Jscript和FORTRAN)中支持的源语言组410首先被转换成MSIL表示440。然而,由于其与其它源语言的差异,C++是以被称为CIL 430的另一中间语言来表达的。CIL和MSIL内的控制流和异常处理模型以完全不同的方式来表达,且因此可能需要为CIL和MSIL表示提供单独的IL读取器(435和445)。
读取器435和445都可使用在其各自的读取器中实现的适当的算法来分析或读取其各自的中间语言代码流,以使用提供给后端460的统一异常处理指令框架450来表达中间语言代码流中的异常处理构造或指令或表达式。以下本文的剩余部分描述了这一语言无关的异常处理指令集的各种成分。此外,中间语言中的异常处理构造的示例被示出为被转换成其各自的语言无关的中间表示。本文也描述了用于分析中间语言和生成异常处理构造的中间语言表示的算法和方法。
在中间表示的主控制流内显式表达的导致异常的指令
导致异常的指令由其处理程序或finally区域来保护。当指令导致异常时,控制流可传递到处理程序,且有时候可基于过滤器指令的处理来有条件地选择处理程序。控制可基于异常或直接流至代码的finally区域,不论以何种方式,它都被处理并用于实现清理代码。最终,总是在控制退出对应的try区域之前执行这些区域。该机制可用于实现清理代码,诸如关闭文件句柄、套接字、锁定等。图8和12示出了表示各种异常处理相关指令的伪代码。例如,图12示出了由两个catch块保护的try区域。要选择这两个catch块(图12)中的哪一个来处理取决于过滤器块的处理结果。作为另一示例,图8示出了由finally块保护的try区域。
如参考图3所描述的,可分析具有用于表达异常处理的各种模型的中间语言表示以确定导致异常的指令及其各自的处理程序和延续之间的控制流,然后可在同一控制流内将这些处理程序和延续显式地表达为不会导致异常的指令的剩余部分。实现这一目的的一种方法是使用具有适度的存储器分配成本(例如,每一指令一个字)的指令来构建控制流表示。处理程序可由使用可由导致异常的指令定义的异常变量的指令来表示。处理程序或过滤器指令然后可测试该异常变量,并基于异常对象的值或类型分支到处理程序主体或另一处理程序。类似地,C++或C#中由finally子句保护的指令具有指向为finally区域之外的控制转移的目标而捕捉延续的指令的控制流边缘或指针。在本情况下finally区域的末尾可由将控制转移到finally区域的起始处的所捕捉的延续的指令来建模。中间语言的这些特征将在下文中参考示例进一步描述。
指令的格式
如上所述,中间语言表示的异常处理构造的中间表示可以在指令级表达。图5示出了用于指令或节点(IR节点)的数据结构的一个这样的通用实现,该数据结构允许在代码的剩余部分的中间表示的控制流内表达异常处理构造。异常处理指令及其功能的特定中间表示在本文的稍后部分中描述。一般而言,IR指令可以在编译器组件分层结构的各种级别处执行。它们具有操作符(op代码)字段和一组源(或输入)操作数、一组目标(或输出)操作数。这些操作数通常是对符号节点的引用。另外,源和目标操作数的每一个可以是类型化的,且操作符和操作数的类型可用于解决任何二义性。在图5所示的示例指令中,504处的操作符具有两个源操作数506和507,以及两个目标操作数508和509。
异常处理语义可以通过向每一指令505提供指向标签指令520的处理程序字段510来表示,该标签指令是用于指令505的处理程序530的起始。如果指令无法抛出异常,则指令的处理程序字段510被设为NULL。如果指令能够抛出异常但没有处理程序,则编译器可构建一特殊的处理程序以将控制传播到当前方法之外。
用于描述图5的IR指令505的文本表示法如下:
CC,DST=OPER1 SRC1,SRC2;$HANDLER1
如果存在处理程序标签,则它出现在分号之后。当指令不抛出异常时,处理程序字段被设为NULL。这可以通过指令的语义来指定,或被认为是作为优化或程序分析的结果的情况。在该情况下,指令可以在文本上表示如下:
CC,DST2=OPER1 SRC1,SRC2;
在对于指令没有目标操作数或结果的情况下,省略指令描述中的目标和“=”符号。例如,条件分支指令没有任何显式的目标操作数,因此它可以在文本上表示如下:
CBRANCH SRC1,SRC1-LABEL,SRC2-LABEL;
异常处理指令
以下段落通过描述其操作、输入和输出,描述了中间表示的各种异常处理相关指令。示例将示出该指令集如何可用于生成与不涉及异常处理的指令在同一控制流中的各种模型的异常处理构造的中间表示。
展开
UNWIND |
将控制传播到当前方法之外 |
句法 |
UNWIND x |
表1
UNWIND指令用于在不存在用于异常的匹配处理程序时表示当前方法之外的控制流。展开指令之前有一标签,且之后是方法的退出。UNWIND操作的源操作数(x)表示抛出的异常对象。这使得数据流是显式的。在一个方法中可以有一个或多个展开指令。然而,每一方法仅有一个UNWIND允许对每一方法节省中间表示空间。UNWIND指令的处理程序字段通常也被设为NULL。
图6和7示出了在中间表示中对UNWIND指令的使用。图6示出了用于可导致异常的未受保护区域的伪代码。在转换成中间表示的过程中,IL 220读取器将分析中间语言(210-213)表示中的代码,而非伪代码。然而,在这些示例中使用伪代码以简化控制流的说明。例如,在图6中,如果尝试除以零的操作,则表达式x=a div b可导致异常。即使原始的源代码或其中间语言表示(例如,MSIL或CIL)无法为该区域指定处理程序,该中间语言表示也可提供一默认处理程序,它通常是UNWIND指令。由此,图6的代码的中间表示可以如图7所示。在该中间表示中,示出导致异常的指令的处理程序字段710被填写,且指向具有标签$HANDLER的处理程序,该标签标记了UNWIND指令的起始。现在如果导致了异常,则UNWIND指令将控制流移至方法之外。
结束
控制流进入或离开finally区域可以在中间表示中由一组相关的指令来表示,如FINAL、FINALLY和ENDFINALLY。FINAL指令一般处理控制向finally区域的转移,而FINALLY指令可接受来自FINAL指令的转移或通过具有处理程序的导致异常的指令。ENDFINALLY指令表示离开finally区域的控制流。
FINAL |
分支到finally区域的起始 |
句法 |
FINAL标签,延续 |
表2
FINAL指令表示控制向finally指令的起始的显式转移。该指令的第一个源操作数是相关联的finally指令的起始标签,而第二个操作数是其中控制在执行了finally区域之后被转移的延续标签。FINAL指令的处理程序字段通常被设为NULL。
FINALLY |
接受来自final或异常处理指令的控制转移 |
句法 |
E,R=FINALLY |
表3
FINALLY指令具有两个目标操作数。第一个操作数是异常变量。这对异常对象的数据流进行建模。第二个操作数是所捕捉的延续的标签或代码操作数。当执行FINALLY指令作为所捕捉的异常的结果时,延续是词法上封闭的处理程序的标签、FINALLY标签或UNWIND指令。该延续标签被反映为匹配的ENDFINALLY(见下文)的处理程序字段。FINALLY指令的处理程序字段通常被设为NULL。
ENDFINALLY |
离开finally区域并分支到延续或展开 |
句法 |
ENDFINALLY E,R,[情况列表];$HANDLER |
表4
ENDFINALLY指令具有两个或多个操作数。第一个操作数是异常变量。第二个操作数是其类型为标签或代码操作数的类型的延续变量。它也具有情况列表,该列表用于表示程序中显式结束调用的可能的控制转移。ENFINALLY指令必须将其处理程序字段设为词法上封闭的外部finally或处理程序(即,FILTER或UNWIND指令)的标签。如果对匹配的finally指令没有异常的控制流,则处理程序字段可为NULL。此外,FINALLY指令的目标操作数E和R与ENDFINALLY指令的源操作数E和R相同。这确保了可由后端组件在代码优化期间使用的两个指令之间的数据依赖性。
图8和9示出了使用FINAL以及FINALLY和ENDFINALLY指令实现从IL表示到中间表示的finally块的一个示例。图8示出了try块的伪代码。在源代码或其中间语言表示中没有指定处理程序。然而,与前一示例不同,不需要指定默认处理程序,因为表达式810不是导致异常的指令。由此,控制仅显式地流至finally区域。图8的代码的中间表示可以如图9所示地表达。表达式910没有指定处理程序。FINAL指令915可以显式地将控制转移到由指向finally块920的标签$FINALIZE指示的finally区域。一旦执行了finally块,控制就转移到FINAL指令915所指示的延续标签(在该情况下为“$END”)。
图10和11示出了使用FINAL、FINALLY和ENDFINALLY指令又一try-finally块向中间表示的转换。然而,在该表示中,向异常处理指令添加了处理程序。图10示出了可导致由finally块1015保护的异常的指令1010。图11示出了try finally块的中间表示以及异常处理程序。1110处导致异常的指令被分配定向到1115处的finally指令的起始的处理程序标签$FINALIZE。在本示例中,对通过finally区域的两种类型的控制流建模。首先,通过1110处导致异常的操作执行FINALLY和ENDFINALLY指令。在该情况下,在执行了ENDFINALLY指令1120之后,控制被传递到由$PROPAGATE标签标记的区域。这实际上捕捉了异常路径上的延续。然而,到finally区域的控制流也可通过FINAL指令1112显式地转移。在该情况下,在ENDFINALLY指令1120的末端,延续到达由$END区域标记的区域,该区域不实现UNWIND指令。
用于表示结束控制流的又一组异常处理中间表示可被称为FAULT和ENDFAULT指令。它们类似于FINALLY和ENDFINALLY指令,然而,与FINALLY指令不同,控制流不能显式地从FINAL指令传递到FAULT指令。到FAULT指令的控制仅通过导致异常的指令分支。
表5
ENDFAULT指令终止相关的FAULT处理程序,且向指定的处理程序抛出异常。如果移除了到对应的FAULT指令的所有异常控制流,则处理程序字段可为NULL。在该情况下,错误处理程序是不可达的,且可被删除。
ENDFAULT |
离开错误区域/处理程序并搜索异常处理程序。 |
句法 |
ENDFAULT E;$HANDLER |
表6
基于过滤器的处理程序
某些中间语言(例如,MSIL)实现基于过滤器的处理程序,由此不同的处理程序基于导致异常的事件的特性被分配给导致异常的事件。这一控制流可以使用捕捉和过滤异常然后向异常指定处理程序的指令(例如,FILTER、ENDFILTER以及TYPEFILTER)以中间表示来表示。如下所述,TYPEFILTER指令可以是FILTER和ENDFILTER指令的简短形式。
FILTER |
捕捉和过滤异常。 |
句法 |
E=FILTER |
表7
该指令可用于实现MSIL中通用的基于过滤器的处理程序。这匹配任何异常,并仅在指令的目标操作数中返回异常对象。过滤器指令被加标签,并且其后跟随可以使用或不使用异常变量的任意指令序列。过滤器指令最终必须到达ENDFILTER,而不会干涉FILTER指令。FILTER指令的处理程序字段通常为NULL。
ENDFILTER |
终止非恢复过滤器。 |
句法 |
ENDFILTER X,处理程序标签,过滤器或展开标签 |
表8
ENDFILTER指令测试布尔操作数(X),且如果它为1,则分支到处理程序标签,否则尝试另一过滤器或展开。
TYPEFILTER |
捕捉给定类型的异常。 |
句法 |
E=TYPEFILTER处理程序标签,过滤器或展开标签 |
表9
TYPEFILTER指令测试异常对象的类型是否为目标操作数(静态已知)的类型的子类型。如果是,则控制转移到第一标签(处理程序标签)。否则,尝试另一过滤器或展开指令标签。当类型过滤器匹配时,目标操作数被设为异常对象。TYPEFILTER指令的处理程序字段通常为NULL。
注意,TYPEFILTER指令是一种简短形式,且实际上可被表示为FILTER和ENDFILTER操作的组合,如下:
t.obj32=FILTER;
e.Type=CHKTYPE t.obj32;
x.cc=CMP(NEQ)e.Type,0.null;
ENDFILTER x.cc,$LABEL1,$LABEL2;
FILTER指令返回其类型被验证为e.Type的异常对象,且如果它是e.Type,则x.cc被设为TRUE,否则设为FALSE。然后,在ENDFILTER处,根据x.cc的值,延续被确定为$LABEL1或$LABEL2。同一表达式可被表示为如下的TYPEFILTER指令。
e.Type=TYPEFILTER$LABEL1,$LABEL2;
图12和13示出了使用基于过滤器的异常处理指令的try-catch块的实现。图12描述了由两个不同的处理程序区域1220和1230保护的导致异常的try区域1210。1215和1225处的过滤器基于所返回的异常对象的类型确定要实现哪一处理程序。图13示出了使用中间表示的TYPEFILTER指令的try-catch对的中间表示。两个导致异常的指令1310的处理程序字段都被设为指向第一个过滤器1315的$HANDLER1。如果异常对象是DivideByZeroException(除以零异常)类型,则控制流至标签为$CATCH1的catch块。如果不是,则控制流至由标签$HANDLER2引用的下一过滤器1325。基于异常对象的类型是否为Excpetion(异常)类型,控制要么流至由标签$CATCH2标识的第二个catch块1330,要么流至由标签$PROPAGATE标识的UNWIND指令1335。
MATCHANYFILTER指令是基于过滤器的异常处理指令的一种形式。
MATCHANYFILTER |
匹配任何异常类型。 |
句法 |
E=MATCHANYFILTER处理程序标签 |
表10
该过滤器总是无条件地匹配任何异常,并将控制转移到有效标签。这等效于FILTER-ENDFILTER对,其中ENDFILTER的第一个操作数总是1,且第二个标签未指定。MATCHANYFILTER指令的处理程序标签必须为NULL。MATCHANYFILTER指令也是一种简短形式,并可使用FILTER和ENDFILTER指令来表示,如下所示。
e.Type=FILTER;
ENDFILTER 1.cc,$LABEL1,$LABEL2;
用于上述FILTER和ENDFILTER组合的等效MATCHANYFILTER指令如下。
e.Type=MATCHANYFILTER$LABEL1;
表示由基于过滤器的处理程序保护的try块和结束
又一异常处理模型基于所导致的异常的类型将控制从try块流至一个或多个处理程序区域,然后流至一个或多个finally区域。图14示出了用于由一对处理程序1420和1430保护的try块1410以及finally块1440的中间语言表示(例如,MSIL)的伪代码。过滤器1415和1425基于所返回的异常对象的类型确定要处理两个处理程序块中的哪一个。不论遍历的catch块是什么,finally块都必须在退出方法之前接触到。
图15示出了使用基于过滤器的TYPEFILTER指令和FINAL、FINALLY和ENDFINALLY结束指令的图14所示的控制流的中间表示。该示例展示了未出现在前一示例中的若干有意思的要点。首先,每一处理程序1520和1530通过分别位于1521和1531处的finally指令的显式调用来终止。这反映了如图14所示的原始程序的语义,其中控制从处理程序1420和1430流至finally块1440。其次,控制最后一个处理程序1530的过滤器1525为finally指令而非展开指令1550指定了标签。这确保如果最后一个过滤器不匹配,则它将控制转移到正确的finally块。该要点将也在异常处理的嵌套情况的示例中进一步示出。
图16和17示出了嵌套的异常处理。图16示出了用于嵌套在try-catch块的try部分中的try-catch-finally块的中间语言表示的伪代码。图17示出了使用上述过滤器、处理程序和finally指令的这一异常控制流的中间表示。如图16所示,外部try块1610由1620和1630处具有过滤器的两个处理程序块来保护。嵌套的try块1615由具有过滤器的catch块1625和1635处的finally块来保护。此处基于若干因素存在若干可能的异常路径,这些因素包括在源代码中发生异常的情况。所有这些各种异常路径都可使用如图17所示的中间表示异常指令来表达。导致异常的指令1705是由标签为$HANDLER1 1710的外部基于过滤器的处理程序块来保护的,该标签可将控制传递到标签为$HANDLER2 1715的又一基于过滤器的处理程序块。内部try块1615中导致异常的指令1706不仅由标签为$HANDLER3 1720的内部基于过滤器的块来保护,且其异常路径也可通过标签为1710处的$HANDLER1和/或1715处的$HANDLER2的块。例如,如果在1706处的内部try块中的表达式处导致了DivideByZero异常,则异常路径通过将1726处ENDFINALLY块的处理程序字段设为$HANDLER1标签,经由1725处的结束块到达1710处的适当处理程序块。该流程表示了从try块1615到finally块1635然后到处理程序1620的流程。
用于将异常处理构造从中间语言代码转换成较低级中间表示的方法
如图2所示,使用上述指令的异常处理构造的中间表示可由IL读取器220生成,该读取器处理中间语言的代码以生成这一表示。IL读取器220可使用任意数量的不同过程或算法,并且算法的选择或设计可以部分地依赖于中间语言本身,尤其是其用于表示异常处理构造的模型。例如,图4示出了适用于读取MSIL中间语言的源代码表示并使用上述异常处理指令生成中间表示的IL读取器445。
图18示出了用于生成异常处理指令的中间表示的一种可能的方法。在某些中间语言(例如,MSIL)中,异常数据可在与主代码流分离的数据结构中捕捉。用于这些语言的IL读取器(转换器)可接收异常处理数据表1810以及中间语言形式的主代码流1820作为输入。这一数据然后可由1830处的IL读取器读取,以确定导致异常的指令和与这些指令相关联的任何catch、finally或过滤器块之间的关系,以生成1840处的中间表示。
例如,图19A示出了包含异常处理数据的数据表,该异常处理数据可作为源代码的中间语言表示的一部分可用。图19A所示的数据对应于图12和13所示的代码片段。如上所述,为简明起见,图12仅示出了中间语言表示的伪代码。然而,IL读取器实际上将分析或读取诸如MSIL等中间语言代码。到各种指令的偏移量1240可以如图12所示的注明。IL读取器知道封闭可区分的代码块的偏移量对。例如,图19A的异常处理数据注明了1910处try块1210的偏移量入口、1915处的块类型(例如,try-catch、try-finally、try-catch-finally等)、以及1920处的其处理程序块的偏移量入口。类似地,指令的异常路径上的finally块、过滤器块、延续块和其它块的偏移量入口及其与彼此的关系可以以如图19A所示的偏移量入口的形式注明。
然而,如上所述,中间表示的异常指令使用标签来标记或标识各种粘合的代码块,且这些标签用于构建中间表示以及控制流。由此,IL读取器使用诸如图19A所示的异常处理数据表来生成用于构建中间表示的标签。图20是用于处理中间语言代码形式的输入的方法的一个示例,该输入包括用于生成中间表示的所捕捉的异常处理数据。在2010,如果在代码内没有要分析或读取的方法,则转换过程在2015停止。如果不是,则在2020读取当前方法及其相关联的异常处理数据表。在2030,使用包括对每一代码块的偏移量范围的异常处理数据来建立可被保护的代码块与形成其处理程序、过滤器、结束、延续块等的代码块之间的包含关系。
例如,图21示出了构建映射图12所示的各种代码块之间的包含关系的树形数据结构。首先,将属于整个方法2110的偏移量分配给树中的节点。然后,当读取异常处理数据中的每一偏移量范围(图19A)时,它们被分配给其它关系,诸如基于由异常处理数据提供的信息让哪些范围由哪些处理程序来保护。例如,try-catch偏移量范围2120被示出为包含由两个处理程序,即2140处的第一catch和2150处的第二catch保护的try范围2130。该示例示出了方法内的try-catch块的单个情况,但是当多个代码块彼此嵌套时树形结构可以变得更大,且树形结构提供了一种表示这一多个包含关系的合适方式。
现在参考图20,当建立各种代码块之间的包含关系(2030)时,在2040,可仅由其偏移量范围标识的每一处理程序和目标块(例如,finally或延续等)现在可被分配不同的标签。例如,图19B示出了标签$HANDLER1和$HANDLER2被分配给图12的代码中所标识的两个处理程序块。再次返回到图20,在2050,使用各种代码块的包含关系,受保护的块如图19C所示的向其处理程序和目标块分配标签。这允许以上述中间表示表达式的标签形式来表达受保护的块及其相关联的处理程序和目标块之间的关系。一旦将受保护的块映射到其适当的处理程序和其它目标块,在2060,通过再次分析代码构建该方法的中间表示。在这一轮分析中,读取每一指令,且如果它是导致异常的指令,则使用先前所构建的范围树数据结构来标识其处理程序。该过程可以重复,直到程序内的所有方法都被转换成其中间表示。
对象构造和析构作为try-finally块的中间表示
诸如C++等某些语言允许程序员声明在声明它们的块或表达式内具有局部生命期的对象(类)变量。该语言语义要求当退出该块作用域或表达式作用域时,调用对应的对象析构函数。这些操作可在中间表示中表示为一组或多组try-finally块。例如,图22示出了使用局部对象的程序。语句S1和S3分别生成对class1和class2的构造函数的调用。这些类的构造函数以及调用这些对象的语句S2和S4可能抛出异常。在该情况下,可能需要适当的清除。如果语句S1抛出异常,则传播该异常,因为尚未成功地创建任何对象。如果S2或S3抛出异常,则需要调用class1的析构函数。然而,如果S4抛出异常,则需要调用class2的析构函数,然后调用class1的析构函数。由于该示例没有处理程序,因此异常被传播到方法的调用者。这些操作可以在概念上由图23所示的嵌套try-finally构造来表达。obj1的构造函数2310在任何try块的外部。由此,如果在构造函数2310期间抛出异常,则在退出方法之前无需析构任何对象。然而,如果在2310处成功地构造了obj1,则它必须经由通过2320处的finally块来析构。然而,如果控制到达2330处的内部try块,则obj1和obj2都必须经由通过finally块2320和2340来析构。对这些操作使用FINAL、FINALLY和ENDFINALLY指令的中间表示可如图24所示。两个FINAL指令2410和2420提供了到分别在2430和2440处表示的两组FINALLY和ENDFINALLY指令的显式入口。控制流也可在第二个对象的析构期间导致异常的情况下通过异常到达这些指令。2435处第二个对象的析构函数指令具有指向包含2445处的第一个对象的析构函数的FINALLY和ENDFINALLY指令2440的处理程序标签($DTOR1)。这是因为如果控制流到达第二个对象的析构函数2430,则第一个对象必须被构造,且因此需要在退出方法之前被销毁。即使在析构第二个对象时没有抛出任何异常,在退出方法之前仍在2440处($DTOR1)析构第一个对象。
表达式临时对象的中间表示
诸如C++等某些语言允许创建表达式临时对象。这些对象是在表达式求值期间创建的,且在表达式求值之后,通常在包含表达式的语句求值之后被销毁。如果表达式是条件表达式,则所创建的对象必须被有条件地析构。例如,图25示出了表达式临时对象obj1(x)和obj2(x+1)。在调用了foo()之后必须调用这些对象的析构函数。然而,注意这些对象的创建是根据变量“x”的值有条件地发生的。由此,这些对象的析构函数指令也必须由同一条件来保护。这可以被表达为如图26的伪代码中所示的一组嵌套的try-finally块。取决于“x”的值,分别在2610和2620创建obj1和obj2。由此,基于同一条件,obj1或obj2必须分别在2630和2640处析构。与前一示例不同,此处在任何给定时刻仅创建一个对象。图27示出了使用多组上述FINAL、FINALLY和ENDFINALLY指令的这一构造的一个中间表示。取决于“x”的值,分支指令2710指向2720处obj1的构造函数的代码,或指向2730处obj2的构造函数的代码。同样要注意,如果在创建2720或2730处的对象的期间抛出了异常,则将处理程序标签设为$PROPAGATE(2721和2731),它标记了将控制传递到方法之外的UNWIND指令。然后再一次取决于所创建的对象,在2740出析构obj1,或者在2750处析构obj2,这两个析构函数都包含在一对FINALLY和ENDFINALLY指令中。以此方式,可以使用中间表示指令来表示表达式临时对象的条件创建和析构。
传值返回对象的中间表示
诸如C++等某些语言允许传值返回对象。在返回对象之前,在局部创建的对象上调用析构函数。例如,在图28中,在2810局部创建对象a和b。如果这些局部对象的任一个上的析构函数抛出异常,则在退出方法之前必须销毁2820处的返回对象r1以及2830处的对象r2。图28中的代码的异常处理控制流可以用如图29A和29B所示的中间表示来表达。在返回2910处的对象r1之前,该方法必须调用分别位于2920和2930处的局部创建的对象a和b的析构函数。然而,如果这些析构函数2920或2930的任一个抛出异常,则必须调用返回对象的析构函数2940。这一控制流可以如图29所示使用适当的FINAL、FINALLY和ENDFINALLY指令组来表示。例如,2930处的对象b的析构函数的处理程序标签($final a1)指向对象a的析构函数的标签2920,该对象的处理程序标签($final r1)进而指向2940处的返回对象r1的析构函数。这确保如果在析构对象a或b或两者的期间导致异常,则在退出该方法之前也析构返回对象r1。注意,在2940,如果标志$F1等于“1”,则调用r1的析构函数,该标志在2950处被设为“1”,且如果对象a或对象b的任一个不能被成功销毁,则保持被设为“1”。以此方式,如果对象a或对象b的析构不成功,则析构返回对象r1。返回对象r2的条件析构以同样的方式处理。
传值抛出对象的中间表示
诸如C++等某些语言允许传值(即在栈上分配的值)抛出和捕捉对象。如类型“int”等简单的原语值不会产生任何问题。然而,抛出在栈上分配的结构和类可能要求调用构造函数和析构函数。图30示出了用于传值抛出对象的源代码。该操作在概念上可用图31所示的伪代码来表示。在本示例中,做出局部值的副本,并且在该副本上调用副本构造函数3110。当在3120抛出值时,必须传递局部值的新副本的析构函数,使得接收该值的方法稍后可以调用该析构函数。finally块3130保护所有的异常,且负责调用析构函数。
try-finally块可以用中间表示被表示为一组FINAL、FINALLY和ENDFINALLY指令,且以下指令可用于表示抛出值类型,该值类型具有在复制指令中为其定义的副本构造函数和析构函数。
句法 |
THROWVAL E,Dtor;$HANDLER |
表11
这是用于抛出具有为其定义的副本构造函数和析构函数的值类型的抛出的特殊形式。它具有两个操作数。第一个操作数是指向具有被抛出的值的位置的指针,而第二个操作数是执行析构的函数指针。其语义是当找到处理程序时析构抛出的对象。这本质上保持局部值类型位置在运行时是活的。这可用于对值类型的C++异常语义进行建模。THROW指令的处理程序字段通常不被设为NULL。图32示出了图30和31的代码的中间表示。THROWVAL指令3210用于表示值抛出,且在该示例中,它被示出为接收指向被抛出的值的位置的指针3220以及指向稍后由接收该抛出的对象的方法使用的其析构函数的指针3230。
try-except构造的中间表示
对诸如C和C++等语言的结构化异常处理(SEH)扩展提供了一种被表达为try-except块的异常处理构造。图33示出了一个try-except块。与带有过滤器的catch块一样,except块基于所导致的异常的类型指定了指向导致异常的指令的处理程序的指针。然而,except块也考虑到了恢复导致异常的指令的执行的可能性。以下两个中间表示表达式可用于表示这一异常控制流。
SEHENTER |
进入SEH保护的区域 |
句法 |
SEHENTER $HANDLER |
表12
SENENTER指令标记了对try-except区域的入口。其处理程序指定了对处理程序的控制依赖性以及受保护区域的主体。
ENDRESUMEFILTER |
终止恢复过滤器. |
句法 | ENDRESUMEFILTER X.处理程序标签,过滤器或展开标签,恢复标签 |
表13
ENDRESUMEFILTER类似于ENDFILTER,不同之处在于它可引起执行导致异常的指令以在源操作数具有-1的值时恢复。图34示出了使用上述SEHENTER、FILTER和ENDRESUMEFILTER表达式用于图33的try-except构造的中间表示。在3410,使用SHENTER指令对尝试3420的主体的调用加前缀。同样,为确保try-except区域中的操作之间的正确的控制依赖性,SEHENTER表达式的处理程序被设为FILTER指令3430,如同对3420处的导致异常的指令foo()的调用一样。ENDRESUME指令用于表示基于由过滤器函数3450返回的值的异常的延续。如果值“t”为1,则控制传递到处理程序主体($HANDLERBODY)。如果该值为“0”,则退出该方法。然而,如果返回的t的值为“-1”,则控制返回到$LABEL以恢复在最先引起异常的操作的执行。同样,图34的表示提供了一种异常路径,它具有直接从SENENTER指令3410的退出。这确保了仅完成安全的代码移动。然而,在同一表示中,对3450处的过滤器函数的调用没有处理程序,但是可如果filter()函数可能导致异常,则可设置诸如展开指令(未示出)等处理程序。
用于将异常处理构造的中间语言表示从中间语言代码转换成较低级中间表示的
替换方法
如图4所示,可能需要单独的IL读取器来从不同的中间语言(例如,CIL、MSIL等)生成中间表示,以符合对该特定语言专用的异常处理模型。以下章节描述了用于从如CIL那样以后缀表示法形式表达操作的语言生成异常处理构造的中间表示的方法。
一般而言,在后缀表示法表达式中,操作的操作数在操作符之前表达,例如,在用于诸如T=5+3等ADD操作的代码中,读取器将在遇到操作符“+”的代码(即,ADD)之前遇到操作数5和3的代码。使用后缀表示法形式的这一代码的转换器,尤其是能够在一遍中转换这一代码的转换器可以首先转换操作数的代码,然后基于其操作数或其子节点(也称为表达式的子表达式)的代码为整个操作构建经转换的代码。
图35示出了用于从诸如CIL等使用后缀表示法的中间语言生成异常处理构造的中间表示的一个总体方法。在3510,逐个节点(即,逐个表达式)读取输入代码。然后,在3520,由于后缀表示法形式的递归特性,确定代码流的剩余部分内的当前节点的上下文。该上下文稍后可在3530处用于基于其子节点的代码将父节点的代码放在一起。
后缀表示法语言可以用使用选中操作符的操作的形式来表达异常信息,该选中操作符可以由IL读取器以图35的方式来处理,以生成指令的统一框架(例如,FINAL、FINALLY以及ENDFINALLY)形式的中间表示。图36示出了一种实现图34的方法的方式。图36示出了担当构建块的若干数据结构(例如,3610、3620、3630),用于包含可稍后可被组合以共同完成诸如方法等代码段的中间表示形式的经转换的中间表示代码。数据结构3610和3630可以被实现为其自己的节点具有其自己的数据结构的概念栈。当读取并转换中间语言(例如,CIL)的代码时,与每一子操作、子表达式、子节点等有关的中间表示代码可被储存在数据结构(例如,3610、3620和3630)中。然后,当为每一操作建立上下文或包含关系或在其它适当的时刻,所有经转换的中间代码被加在一起以生成完整的转换。
在一个这样的方法中,从中间语言输入中读取的节点被压入求值栈3610中,在该栈中可对它们进行求值。从输入中读取的节点的求值可能要求将某些节点从求值栈或EH栈中弹出,并将新节点压入求值栈或EH栈。一般而言,求值栈3610可包含与大多数主代码流有关的中间代码。来自求值栈的某些节点然后可被出栈,以当建立父和子节点的上下文或包含关系时为其它节点构建代码。例如,回到简单的加法表达式T=5+3,当建立5和3为“+”操作的操作数的表达式时,求值栈3610中与常数5和3有关的节点被出栈,且用于加法操作的代码可通过组成其子节点,即表示5和3的节点的代码来合成。转换算法使用并维护了求值栈上的节点已计算了所有的属性的不变性。
DTOR代码数据结构3620可以被认为是用于为出现在方法主体中的所有的对象析构函数、catch块以及finally块封装经转换的中间表示代码序列的结构。由此,不使用异常处理(EH)栈3630来包含代码序列,但是EH栈可被认为是用于通过构建标签来建立各种代码序列之间的关系的延续栈。EH栈建立try、catch和finally区域之间的嵌套关系。每一区域可由与该区域相关联的标签来标识。EH栈中的每一节点具有唯一的ID,称为状态。状态的概念用于计算在表达式求值中所分配的对象的数目。该信息稍后可用于诸如确定需要被添加到经转换的代码及其与代码的剩余部分的关系的析构函数的数目。
求值栈3610中的每一节点的数据结构可示出如下。
字段 |
描述 |
Opcode |
节点的IL操作码 |
IL Type |
由编译器的前端提供的节点的IL类型 |
Opnd |
表示作为节点求值的结果的操作数的IR数据结构。当读取节点时该字段为空,且节点的求值计算Opnd字段 |
EHStart |
表示当读取节点时EH栈的顶部的整数 |
EHEnd |
表示当对节点求值时EH栈的顶部的整数。EHStart和EHEnd数字之间的差别给出了在表达式求值期间被压入EH栈的节点。它用于生成条件表达式的表达式临时对象的finally区域。 |
FirstInstr |
指向与代码有关的经转换的代码序列的起始 |
LastInstr |
指向与节点有关的经转换的代码序列的末端 |
表14
仅“Opcode”和“IL Type”字段可以由编译器的前端提供,剩余字段是在生成中间表示代码时在转换期间填充的。FirstInstr和LastInstr字段允许代码的串接和前置。整个DTOR数据结构3620可以类似于求值栈3610的一个节点的数据结构来实现。
EH栈节点可具有以下用于表示异常路径上的延续的数据结构。
字段 |
描述 |
Label |
到finally块或catch块的标签 |
Flags |
表示EH标志的标志 |
State Type |
表示状态类型 |
State Id |
表示EH状态id |
表15
Label字段指向可在Finally或TypeFilter指令之前的标签指令。Flags字段可用于标识所执行的异常处理操作的特征。例如,它可用于标识要转换的析构函数代码是否用于诸如表达式临时对象等临时对象,或者它是否用于可在构造它的表达式之外析构的对象。
在用于求值栈3610、DTOR代码3620和EH栈3630的节点的数据结构的上下文中,可如图37所示的更详细地描述用于生成如图35所示的中间表示的方法。图37的方法是为转换的中间语言流中的每一方法实现的。在3710,图36所示的转换数据结构(例如,3610、3620和3630)被初始化。例如,求值栈3610和EH栈3620被初始化为空。同样,可将UNWIND节点压入EH栈中,因为所有的方法至少都具有一个到该方法外部的异常路径延续,且也可初始化EH栈的当前状态。稍后在3720,从中间语言代码中读取节点,并将其EH状态初始化为CurrentEHState(当前EH状态)。再一次,使用状态来维护递归地读取的节点的上下文。稍后在3740,在求值栈和EH栈的现有上下文中对节点求值。求值的结果是定义节点的Opnd、FirstInstr和LastInstr字段,以及上下文中的改变(即,节点可被压入或弹出求值栈或EH栈,且CurrentEHState值可被修改)。求值栈上的节点表示已求值的节点,它们已完整地填充了其字段。然后,在3750,如果求值的操作不是退出,则读取下一节点。然而,如果是方法的退出,则在3760,包含在求值栈3610中的所有已求值节点中的经转换的代码被前置,且在DTOR代码数据结构3620中的代码被串接成这一结果以产生完整的中间表示代码。
后缀表示法的中间语言代码向较低级中间表示的示例转换
求值方法3740可以在读取中间语言代码的过程中遇到它们时对不同的操作是不同的。以下示例示出了用于对中间语言代码中某些这样的操作求值的方法。图22和23示出了用于诸如C++等语言的对象构造函数和析构函数的伪代码。图24示出了使用诸如FINAL、FINALLY、ENDFINALLY等指令被转换成中间表示的图22和23的代码。由IL读取器(即,转换器)处理的后缀表示法中间语言代码可以是图38A、38B和38C中所示的形式。这一代码可以被读取并使用上文参考图36和37所描述的方法来转换成中间表示(图24)。
图39示出了使用转换数据结构(例如,3905、3915和3925)来转换图38A、38B、38C中包含对象构造函数和析构函数的中间语言代码过程。首先,所有数据结构被初始化为空。然后,当读取图38A、38B和38C中的代码时,对每一操作求值,并且将节点压入求值栈3905中,或从该栈中弹出以生成DTOR代码数据结构3925中的代码并操纵EH栈3915。在本示例中,对象1的构造函数首先在3810处标识,这作为节点被压入3910处的求值栈中。稍后在3820,遇到用于对象1的析构函数的代码,它也被临时压入求值栈中。当读取器在3830处遇到Oppushstate操作符时,知道析构函数3820和构造函数3810是Oppushstate操作符3830的操作数。也知道析构函数代码是处理程序,因此需要被放置在DTOR代码数据结构3925中。由此,求值栈3905上与对象1的析构函数有关的顶部节点被出栈,并最终连同标识该块的标签一起被追加到DTOR代码数据结构3925。DTOR代码数据结构3925也用具有如图39所示的适当的延续的FINALLY和ENDFINALLY指令来追加。EH栈3915将被初始化为具有带UNWIND指令前的标签的节点3920,但是现在新节点3940被添加到EH栈,且其标签被设为被添加到包含FINALLY和ENDFINALY指令的代码块的标签。在这一点上,确定了与异常路径和延续有关的中间代码。对象2的代码以类似的方式来求值。稍后在3840,当遇到Opdtoraction操作符时,如2410和2420所示地构建与进入finally区域的显式入口有关的中间代码(即,FINAL指令)。图39示出了在图38的中间语言代码到达其中已对OPpushstate指令3830进行求值的点之后,求值栈3905、DTOR代码数据结构3925以及EH栈3915的状态。由此,将用于中间表示的代码缝合在一起所需的所有代码被包含在数据结构3905和3925及其各节点内,它们可被添加以形成完整的经转换的代码。虽然,用于对各种操作符的代码求值并将它们缝合在一起的方法可以不同,上述各种数据结构可以在每一个别的操作符及其函数和形式的上下文中使用,以生成期望的中间表示。
替换方案
参考所示的实施例描述并示出了本发明的原理之后,可以认识到,所示的实施例可以在排列和细节上进行修改而不脱离这些原理。尽管此处所描述的技术是通过使用编译器的示例来示出的,但这些技术中的任一种可使用其它软件开发工具(例如,调试器、优化器、模拟器和软件分析工具)。同样,应当理解,此处所描述的程序、过程或方法不涉及或限于任何特定类型的计算机装置。各种类型的通用或专用计算机装置可用于或执行依照此处所描述的教导的操作。此处所描述的动作可以由包括用于执行这些动作的计算机可执行指令的计算机可读介质来实现。所示实施例中以软件示出的元素可以用硬件来实现,反之亦然。鉴于可向其应用本发明的原理的许多可能的实施例,应当认识到,详细描述的实施例仅是说明性的,且不应当被理解为局限本发明的范围。相反,要求保护落入所附权利要求书及其等效技术方案的范围和精神之内的所有这样的实施例作为本发明。