LLVM里的异常处理

原文:http://llvm.org/docs/ExceptionHandling.html

介绍

本文是与LLVM里有关异常处理的所有信息的集散地。它描述了LLVM异常处理信息所具有的格式,这对对构建前端或直接处理该信息感兴趣的人是有用的。另外,本文提供了关于在C与C++中使用什么异常处理信息的特定例子。

Itanium ABI零开销异常处理

对大多数编程语言,异常处理被设计来从应用程序正常使用中很少发生的情形中恢复。为了达到这个目的,通过执行检查点任务,比如保存当前pc或寄存器状态,异常处理不应该与应用程序算法的主控制流相互干扰。

Itanium ABI异常处理规范定义了提供异常表形式外围数据的方法论,而无需在应用程序主算法控制流中嵌入推测的异常处理代码。因此,称该规范对应用程序的正常执行增加了“零代价”。

Itanium ABI异常处理运行时支持更全面的描述可以在Itanium C++ ABI: Exception Handling找到。异常帧格式的描述可以在Exception Frames找到,DWARF4规范的细节在DWARF4 Standard。C++异常表的格式的描述可在Exception Handling Tables找到。

Setjmp/Longjmp异常处理

基于setjmp/longjmp(SJLJ)的异常处理使用LLVM的固有函数llvm.eh.sjlj.setjmp以及llvm.eh.sjlj.longjmp来处理异常处理的控制流。

对于每个执行异常处理的函数——不管是try/catch块还是清理——该函数把自己注册到一个全局的帧列表。在异常回滚时,运行时通过这个列表来识别哪个函数需要处理。

着陆场(landing pad)选择编码在函数上下文的调用点入口。运行时通过llvm.eh.sjlj.longjmp返回该函数,在那里有一个分发表,基于保存在函数上下文的索引,将控制流转移到合适的着陆场。

相比将异常区域及帧信息编码在外部表的DWARF异常处理,SJLJ异常处理在运行时构建及删除回滚帧上下文。这导致更快的异常处理,代价是在没有抛出异常时更慢的执行。因为异常就本质而言是为不常见的代码路径准备的,DWRAF异常处理通常要优于SJLJ。

Windows运行时异常处理

LLVM支持由Windows运行时生成的异常处理,不过它要求一个非常不同的中间表示。不像其他两个模型,它不是基于landingpad指令的,稍后在本文的“使用Windows的异常处理”一节描述。

概况

当一个异常在LLVM代码里抛出时,运行时尽最大努力去查找适合处理该事件的句柄。

运行时首先尝试找出对应抛出该异常函数的异常帧(exception frame)。如果编程语言支持异常处理(比如C++),这个异常帧包含一个描述如何处理该异常的异常表引用。如果语言不支持异常处理(比如C),或者异常需要被转发到之前的活动(activation),该异常帧包含了关于如何回滚当前活动以及恢复之前活动状态的信息。这个过程被重复直到异常被处理。如果异常没有被处理且没有遗留的活动,那么程序以恰当的错误消息退出。

因为不同的编程语言在处理异常时有不同的行为,异常处理ABI提供了一个支持personality的机制。一个异常处理personality以一个personality函数的方式定义(即C++的__gxx_personality_v0),它接受异常上下文,一个包含异常对象类型与值的异常结构,以及当前函数异常表的一个引用。当前编译单元的personality函数在一个通用异常帧中指定。

异常表的组织依赖于语言。对于C++,异常表被组织为一系列定义了异常在其中发生时如何做的代码区域。典型地,与一个区域关联的信息定义了在该区域处理的异常对象的类型(使用C++type info),及应该执行的相关行动。行动通常将控制权交给一个着陆场。

着陆场大致上对应一个try/catch序列中catch部分的代码。当执行在一个着陆场继续时,它接受一个异常结构以及一个对应抛出异常类型的选择符值。然后该选择符被用来确定这个异常应该由哪个catch处理。

LLVM代码生成

从一个C++开发者的角度来看,异常以throw及try/catch语句的方式定义。在本节我们将以C++例子来描述LLVM异常处理的实现。

抛出

支持异常处理的语言通常提供一个throw操作来启动异常过程。在内部,一个throw操作分为两步。

1.      构造一个请求,为一个异常结构分配异常空间。这个结构需要在当前活动之外存活。它将包含被抛出对象的类型与值。

2.      构造对运行时的一个调用,以抛出该异常,异常结构将作为一个实参传递。

在C++中,异常结构的分配由__cxa_allocate_exception运行时函数完成。由__cxa_throw处理异常提交(raising)。异常的类型使用C++RTTI结构体表示。

Try/Catch

在一个try语句作用域中的函数调用潜在地会抛出一个异常。在那样的情形下,LLVMC++前端以一条invoke指令替换该调用。与调用不同,invoke有两个潜在的继续点:

1.       当调用像正常那样成功时,继续执行的点

2.       如果调用通过抛出或异常回滚提交一个异常时,继续执行的点

定义invoke在一个异常之后继续执行的位置的术语被称为着陆场(landingpad)。LLVM着陆场在概念上是另一个函数入口,一个异常结构引用,一个类型信息(typeinfo)索引作为实参传入。着陆场保存这个异常结构的引用,然后着手选择对应该异常对象typeinfo的catch块。

LLVM的landingpad指令用于将关于着陆场的信息传递给后端。对于C++,landingpad指令返回一个指针与整数组对,分别对应异常结构指针与选择符值。

Landingpad指令在父函数的属性列表中查找用于这个try/catch序列的personality函数的一个引用。该指令包含一组cleanup,catch与filter条款。对异常依次检测这些条款。它们具有以下含义:

·        catch <type> @ExcType

o  这个条款意味着,如果抛出的异常具有类型@ExcType或者是其派生类,进入该着陆场块。对于C++,@ExcType是指向代表该C++异常类型的std::type_info对象(一个RTTI对象)的一个指针。

o  如果@ExcType是null,匹配任何异常,因此总是进入该着陆场。这用于C++的catch-all块(catch(…))。

o  在匹配这个条款时,选择符值将等于由@llvm.eh.typeid.for(i8*@ExcType)返回的值。这将总是一个正数。

·        filter <type> [<type] @ExcType1, …,<type> @ExcTypeN]

o  这个条款意味着,如果抛出异常不匹配列表中的任何类型(对于C++,它们还是由std::type_info指针指定),进入该着陆场。

o  C++前端使用它来实现C++异常规范,比如voidfoo() throw (ExcType1, …, ExcTypeN) { … }。

o  在匹配这个条款时,选择符值将是负数。

o  Filter的数组实参可以是空的;例如,[0 x i8**] undef。这意味着总是进入该着陆场。(注意这样一个filter不等于catchi8* null,因为filter与catch分别产生负的与正的选择符值)。

·        Cleanup

o  这个条款意味着总是进入该着陆场。

o  C++前端使用它来调用对象的析构函数。

o  在匹配这个条款时,选择符值将是0。

o  运行时可能以不同于catch <type> null的方式处理cleanup。

在C++中,如果出现了一个未处理的异常,语言的运行时将调用std::terminate(),但运行时是否首先回滚栈并调用对象的析构函数是实现定义的。例如,在出现未处理异常时,GNUC++回滚器不会调用对象的析构函数。原因是为了提高可调试性:确保std::terminate()在throw的上下文中被调用,这样在回滚栈时不会丢失这个上下文。在进入任何着陆场块之前,运行时通常通过查找一个匹配的非cleanup条款,如果找不到就中止,来实现它。

一旦着陆场拥有类型信息选择符,代码跳转到第一个catch的代码。这个catch核对该类型信息操作符的值与其类型信息的索引。因为类型信息索引直到后端收集了所有的类型信息之后才知道,catch代码必须调用固有函数llvm.eh.typeid.for来确定给定类型信息的索引。如果catch匹配该选择符失败,那么控制交给下一个catch。

最后,以__cxa_begin_catch与__cxa_end_catch的调用封闭catch代码的出入口。

·        __cxa_begin_catch接受一个异常结构引用作为参数并返回该异常对象的值。

·        __cxa_end_catch不带参数。这个函数;

1.      定位最近被捕捉的异常并确定其句柄计数。

2.      如果句柄计数为0,从caught栈删除该异常,并且

3.      如果句柄计数为0且该异常不是再次抛出的,销毁该异常。

注意:catch内的异常重新抛出可能以一个__cxa_rethrow来替代这个调用。

Cleanup

作为回滚一个作用域的一部分,cleanup是需要运行的额外代码。C++析构函数是一个典型的例子,但其他语言及语言扩展提供了各种不同类型的cleanup。通常,在实际进入一个catch块之前,一个着陆场可能需要运行任意数量的cleanup代码。要表示cleanup的存在,landingpad指令应该带一个cleanup条款。否则,回滚器将不会停在该着陆场,如果没有catch或filter要求它这样做。

注意:不要允许一个新异常在一个cleanup的执行中传播出去。这会毁坏回滚器的内部状态。对这些情形,不同的语言有不同的高级语义:例如,C++要求中止该过程,而Ada会取消这两个异常,而抛出第三个。

当所有cleanup都完成时,如果当前函数没有处理这个异常,通过调用resume指令,向其传递原始着陆场landingpad指令的结果,继续回滚。

抛出过滤器

C++允许异常类型从一个函数抛出。为此,需要一个顶层的着陆场来过滤无效的类型。要在LLVM代码里表达,landingpad指令要有一个过滤条款。该条款包含一个类型信息数组。如果异常不匹配任何类型信息,landingpad返回一个负数。如果没有找到匹配,调用__cxa_call_unexpected,否则调用_Unwind_Resume。这些函数都要求一个异常结构的引用。注意landingpad指令最通用的形式可以带有任意数量的catch,cleanup及过滤条款(虽然多个cleanup是无意义的)。因为内联所造成的异常处理作用域嵌套,LLVM的C++前端可以生成这样的landingpad指令。

约束

回滚器将是否停在一个调用帧的决定委托给该调用帧的语言特定的personality函数。不是所有的回滚器都保证它们会停下来执行清理。例如,GNUC++回滚器就不这样做,除非异常在栈的更上方某处被捉住了。

为了使内联的行为正确,着陆场必须准备好处理没有向它通告的选择符结果。假设一个函数捕捉类型A的异常,它被内联到捕捉类型B异常的一个函数中。内联器将更新被内联着陆场的landingpad指令,包括B也被捕捉的事实。如果该着陆场假设进入它只为捕捉A,它肯定要遭受粗鲁的唤醒。因此,着陆场必须检测它们理解的选择符结果,如果没有条件匹配成功,使用resume指令重启异常传播。

异常处理固有函数

除了landingpad与resume指令,LLVM使用几个固有函数(以llvm.eh为前缀),在生成代码的各处,提供异常处理信息。

llvm.eh.typeid.for

i32 @llvm.eh.typeid.for(i8*%type_info)

这个固有函数返回类型信息在当前函数异常表中的索引。这个值可以用于比较landingpad指令的结果。参数是类型信息的一个引用。

由C++前端生成这个固有函数的使用。

llvm.eh.begincatch

void @llvm.eh.begincatch(i8*%ehptr,i8*%ehobj)

在跟随landingpad指令的基本块内,这个固有函数标记捕捉处理代码的开始。这个函数的确切行为依赖于目标机器以及与该landingpad指令关联的personality函数。

这个固有函数的第一个实参是该landingpad指令的聚合返回值的一个指针。第二个实参是指向异常对象所在栈空间的指针。将异常对象拷贝到栈槽的细节由运行时处理。如果第二个参数是null,不进行拷贝。

由C++前端生成这个固有函数的使用。许多目标机器不使用它,而是使用实现特定的函数(比如__cxa_begin_catch)。该固有函数提供给要求更抽象接口的目标机器。

当用在原生Windows C++异常处理实现时,在一个捕捉句柄被概括出来之前,这个固有函数作为界定代码的一个占位符。在该句柄被概括出来后,这个固有函数将被从帧分配基本块提取异常对象指针的指令所替代。

llvm.eh.endcatch

void @llvm.eh.endcatch()

这个固有函数标记当前基本块中捕捉处理代码的结尾,该基本块是调用llvm.eh.begincatch基本块的一个后继。这个函数的确切行为依赖目标机器以及与该landingpad指令关联的personality函数。

对任一给定的llvm.eh.begincatch调用,可以有多个llvm.eh.endcatch调用,每个llvm.eh.endcatch调用对应不同控制流路径的终点。所有跟随一个llvm.eh.begincatch调用的控制流路径必须到达一个llvm.eh.endcatch调用。

由C++前端生成这个固有函数的使用。许多目标机器不使用它,而是使用实现特定的函数(比如__cxa_end_catch)。该固有函数提供给要求更抽象接口的目标机器。

当用在原生Windows C++异常处理实现时,在一个捕捉句柄被概括出来之前,这个固有函数作为界定代码的一个占位符。在该句柄被概括出来后,这个固有函数将被删除。

llvm.eh.exceptionpointer

i8 addrspace(N)* @llvm.eh.padparam.pNi8(token %catchpad)

这个固有函数提取指向由指定catchpad捕捉的异常的指针。

SJLJ固有函数

Llvm.eh.sjlj在LLVM后端内部使用。由后端的SjLjEHPrepare遍生成它们的使用。

llvm.eh.sjlj.setjmp

i32 @llvm.eh.sjlj.setjmp(i8*%setjmp_buf)

对基于SJLJ的异常处理,该固有函数强制保存当前函数寄存器,并且保存随后作为llvm.eh.sjlj.longjump一个目标地址的指令的地址。这个固有函数的缓存格式和总体功能与GCC的__builtin_setjmp实现兼容,允许clang与GCC编译的代码互操作。

唯一的参数是指向保存了调用上下文的5字缓存的指针。前端将帧指针放入第一个字,这个固有函数的目标机器实现将llvm.eh.sljl.longjump的目标地址放入第二个字。余下三个字用于目标机器特定的用途。

llvm.eh.sjlj.longjmp

void @llvm.eh.sjlj.longjmp(i8*%setjmp_buf)

对基于SJLJ的异常处理,这个固有函数用于实现__builtin_longjump()。唯一的参数是指向由llvm.eh.sjlj.setjump占用的缓存的指针。从该缓存恢复帧指针与栈指针,然后把控制交给目标地址。

llvm.eh.sjlj.lsda

i8* @llvm.eh.sjlj.lsda()

对基于SJLJ的异常处理,这个固有函数返回当前函数的语言特定数据区(LSDA)的地址。SJLJ前端代码在异常处理函数上下文中保存这个地址为运行时使用。

llvm.eh.sjlj.callsite

void @llvm.eh.sjlj.callsite(i32%call_site_num)

对基于SJLJ的异常处理,这个固有函数标识与跟随的invoke指令关联的调用点值。这用于确保LSDA中的着陆场项以匹配的次序生成。

汇编表格式

异常处理运行时使用两个表来确定在抛出一个异常时应该采取哪个行动。

异常处理帧

异常处理帧eh_frame非常类似于DWARF调试信息使用的回滚帧。该帧包含了拆解当前帧并恢复之前帧状态的所有必要信息。在一个编译单元里的每个函数都有一个异常处理帧,加上一个定义了该单元所有函数公共信息的公共异常处理帧。

不过,这个调用帧信息(CFI)的格式通常依赖于平台。例如,ARM定义了自己的格式。苹果有自己紧凑的回滚信息帧。在Windows上,因为32位x86,所有的架构都使用另一个格式。LLVM将发布目标机器所需的任何信息

异常表

异常表包含关于当一个异常在一个函数代码的一个特定部分抛出时,执行什么行动的信息。这通常被称为语言特定数据区域(LSDA)。LSDA表的格式特定于personality函数,但大多数personality使用由__gxx_personality_v0消费的各种表。每个函数有一个异常表,除了仅调用不抛出异常函数的叶子函数。它们不需要异常表。

使用Windows运行时的异常处理

Windows异常的背景知识

在Windows上与异常的交互要远比在ItaniumC++ ABI平台上复杂。两个模型间根本的差别是,ItaniumEH是围绕“持续回滚”思想设计,而WindowsEH不是。

在Itanium,抛出一个异常通常涉及分配线程局部内存来保存该异常,调用EH运行时。该运行时确定带有恰当异常处理行动的帧,并随着行动的执行,持续将当前线程的寄存器上下文重置为最近活动的帧。在LLVM里,执行在一条landingpad指令处继续,这呈现了由运行时提供的寄存器值。如果一个函数只是清理分配的资源,在完成清理后,它负责调用_Unwind_Resume来转移到下一个最新的活动帧。最终,负责处理异常的帧调用__cxa_end_catch来销毁该异常,释放其内存,并重启正常的控制流。

Windows EH模型不使用这些持续的寄存器上下文重置。相反,活跃的异常通常由栈上的一个帧来描述。在C++异常的情形里,异常对象在栈内存中分配,其地址被传递给__CxxThrowException。通用结构化异常(SEH)更类似于Linux信号,它们与Windows一起提供的用户空间DLL分发。栈上的每个帧分配有一个EHpersonality方法,它决定采取什么行动来处理这个异常。C与C++代码有几个主要的personality:C++personality(__CxxFrameHandler3)与SEH presonality(_except_handler3,_except_handler4及__C_specific_handler)。它们都通过回调父函数包含的一个funclet实现清理。

在这个上下文里,funclet是父函数可以被调用的区域,仿佛它们是带有非常特殊调用上下文的函数指针。依赖于架构,使用标准的EBP寄存器,或第一个参数寄存器,父帧的帧指针被传递给funclet。Funclet通过这个帧指针访问内存中的局部变量,返回某个合适的值,继续EH过程,来实现EH行动。在funclet出入口存活的变量不能分配在寄存器中。

C++ personality也使用funclet来包含catch块的代码(即在catch(Type obj) { … }括号间的所有用户代码)。运行时必须对catch主体使用funclet,因为C++异常对象在处理该异常的函数的一个子栈帧中分配。如果运行时重新把栈回滚到该catch的帧,保存这个异常的内存很快会被后续的函数调用改写。Funclet的使用也允许__CxxFrameHandler3无需依靠TLS来实现重新抛出。取而代之,运行时抛出一个特殊的异常,然后通过SEH(__try / __except)使用子帧中的新信息来重启执行。

换句话说,持续回滚方法与Visual C++异常以及Windows通用异常处理是不兼容的。因为C++异常对象存活在栈内存里,LLVM不能提供一个使用landingpad的定制personality函数。类似的,SEH不能提供任何机制来重新抛出一个异常或继续回滚。因此,LLVM必须使用在本文后面描述的IR构造来实现兼容的异常处理。

SEH过滤表达式

SEH personality函数也使用funclet来实现过滤表达式,这允许执行任意用户代码来确定捕捉哪个异常。不要把过滤表达式与LLVMlandingpad指令的filter条款混淆。通常过滤表达式用于确定异常是否来自一个特定的DLL或代码区域,或者代码是否在访问一个特定的内存地址区域时失败。LLVM当前没有表示过滤表达式的IR,因为难以表示它们的控制依赖关系。过滤表达式在cleanup执行前的EH第一个阶段运行,这使得构建一个可靠的控制流图非常困难。目前,新的EH指令不能表示SEH过滤表达式,前端必须提早概括它们。可以使用llvm.localescape及llvm.localrecover固有函数避开或访问父函数的局部变量。

新的异常处理指令

新EH指令的主要设计目的是支持funclet生成,同时保留CFG的信息,使SSA构造仍然可以工作。其次,它们被设计为对MSVC及ItaniumC++异常通用。它们对personality所需的数据进行很少的假设,只需要它使用类似的EH核心行动:catch,cleanup及终止。不过,不知道EHpersonality的细节,新指令很难修改。尽管它们可以用于表达Itanium EH,对于优化而言,着陆场模型更好。

以下新指令被视为“异常处理垫”(expcetion handling pad),它们必须是一条EH控制流边回滚目标基本块的第一条非phi指令:catchswitch,catchpad及cleanuppad。正如着陆场,在进入一个try作用域时,如果前端遇到一个可能抛出异常的调用点,它应该发布一个回滚到一个catchswitch的invoke。类似的,在一个带有析构函数的C++对象作用域中,invoke应该回滚到一个cleanuppad。

新指令也用于标记一个catch/cleanup句柄传递出控制的地点(这对应从生成的funclet退出)。一个通过正常执行到达其结尾的catch句柄执行一条catchret指令,这条指令是表示该函数控制权返回到哪里的终结符。一个通过正常执行到达其结尾的cleanup句柄执行一条cleanupret指令,这条指令是表示活跃异常下一次要回滚到哪里的终结符。

这些新的EH垫指令都有一个方式来识别在这个活动后应该考虑哪个活动。Catchswitch指令是一个终结符,并具有一个类似于invoke回滚目标的回滚目标操作数。Cleanuppad指令不是一个终结符,因此回滚目标保存在cleanupret指令上。成功地执行一个catch句柄将重启正常的控制流,因此catchpad或catchret指令都不能回滚。所有这些“回滚边”可能指向一个包含一条EH垫指令的基本块,或者它们可能回滚到调用者。回滚到调用者大体上与着陆场模型中resume指令有相同的语义。在通过一个invoke内联时,回滚到调用者的指令被钩挂上以回滚到该调用点的回滚目标。

合起来,下面是某些使用所有新IR指令C++的一个推测的降级(lowering):

struct Cleanup {

  Cleanup();

  ~Cleanup();

  int m;

};

void may_throw();

int f() noexcept {

  try {

    Cleanup obj;

    may_throw();

  } catch (int e) {

    may_throw();

    return e;

  }

  return 0;

}

 

define i32 @f()nounwind personality i32 (...)* @__CxxFrameHandler3 {

entry:

  %obj = alloca %struct.Cleanup, align 4

  %e = alloca i32, align 4

  %call = invoke %struct.Cleanup*@"\01??0Cleanup@@QEAA@XZ"(%struct.Cleanup* nonnull %obj)

          to label %invoke.cont unwind label%lpad.catch

 

invoke.cont:                                      ; preds =%entry

  invoke void@"\01?may_throw@@YAXXZ"()

          to label %invoke.cont.2 unwind label%lpad.cleanup

 

invoke.cont.2:                                    ; preds =%invoke.cont

  call void@"\01??_DCleanup@@QEAA@XZ"(%struct.Cleanup* nonnull %obj) nounwind

  br label %return

 

return:                                           ;preds = %invoke.cont.3, %invoke.cont.2

  %retval.0 = phi i32 [ 0, %invoke.cont.2 ], [%3, %invoke.cont.3 ]

  ret i32 %retval.0

 

lpad.cleanup:                                     ; preds =%invoke.cont.2

  %0 = cleanuppad within none []

  call void@"\01??1Cleanup@@QEAA@XZ"(%struct.Cleanup* nonnull %obj) nounwind

  cleanupret %0 unwind label %lpad.catch

 

lpad.catch:                                       ; preds= %lpad.cleanup, %entry

  %1 = catchswitch within none [label%catch.body] unwind label %lpad.terminate

 

catch.body:                                       ; preds= %lpad.catch

  %catch = catchpad within %1[%rtti.TypeDescriptor2* @"\01??_R0H@8", i32 0, i32* %e]

  invoke void@"\01?may_throw@@YAXXZ"()

          to label %invoke.cont.3 unwind label%lpad.terminate

 

invoke.cont.3:                                    ; preds =%catch.body

  %3 = load i32, i32* %e, align 4

  catchret from %catch to label %return

 

lpad.terminate:                                   ; preds =%catch.body, %lpad.catch

  cleanuppad within none []

  call void @"\01?terminate@@YAXXZ"

  unreachable

}

Funclet父符号

为了生成使用funclet的EHpersonality所用的表,恢复源代码中呈现的嵌套是必要的。这个funclet父子关系被编码在使用由新“垫”指令生成符号的IR中。一条“垫”或“ret”指令的符号操作数表示它在哪个funclet,如果它没有嵌套在另一个funclet中,则是“none”。

Catchpad与cleanuppad指令建立新的funclet,其他垫指令消费它们的符号来建立成员身份。Catchswitch指令不创建funclet,但它生成一个总是由它直接后继catchpad指令消费的符号。这确保由一个catchpad模仿的每个catch句柄只属于一个模仿一个C++try后分发点的catchswitch。

下面是使用某些假想C++代码,这个嵌套样子的一个例子:

void f() {

  try {

    throw;

  } catch (...) {

    try {

      throw;

    } catch (...) {

    }

  }

}

 

define void @f()#0 personality i8* bitcast (i32 (...)* @__CxxFrameHandler3 to i8*) {

entry:

  invoke void @_CxxThrowException(i8* null,%eh.ThrowInfo* null) #1

          to label %unreachable unwind label%catch.dispatch

 

catch.dispatch:                                   ; preds =%entry

  %0 = catchswitch within none [label %catch]unwind to caller

 

catch:                                            ;preds = %catch.dispatch

  %1 = catchpad within %0 [i8* null, i32 64,i8* null]

  invoke void @_CxxThrowException(i8* null,%eh.ThrowInfo* null) #1

          to label %unreachable unwind label%catch.dispatch2

 

catch.dispatch2:                                  ; preds =%catch

  %2 = catchswitch within %1 [label %catch3]unwind to caller

 

catch3:                                           ;preds = %catch.dispatch2

  %3 = catchpad within %2 [i8* null, i32 64,i8* null]

  catchret from %3 to label %try.cont

 

try.cont:                                         ;preds = %catch3

  catchret from %1 to label %try.cont6

 

try.cont6:                                        ; preds = %try.cont

  ret void

 

unreachable:                                      ; preds =%catch, %entry

  unreachable

}

内层的catchswitch消费由外层catchswitch产生的%1。

Funclet转换

使用funclet的personality的EH表隐含使用funclet的嵌套关系来编码回滚目标,因此局限在它们可以表示的funclet转换的集合。相关的LLVMIR指令相应地具有确保控制流图中EH边可编码性的约束。

Catchswitch,catchpad,或cleanuppad在执行是被称为“进入”。随后可以下列任一方式“退出”它:

·        当其构成catchpad不适合正在行进的异常,并且回滚到了其回滚目标或调用者时,立即退出这个catchswitch。

·        当执行来自一个catchpad的catchret时,退出该catchpad与其父catchswitch。

·        当执行来自一个cleanuppad的cleanupret时,退出该cleanuppad。

·        当控制回滚到该函数的调用者时,通过一个一直回滚到该函数调用者的call,或者一个标记为“回滚到调用者”的嵌套catchswitch,又或者一个标记为“回滚到调用者”的嵌套cleanuppad的cleanupret,退出任何这些垫。

·        当一条回滚边(来自invoke,嵌套catchswitch,或嵌套cleanuppad的cleanupret)回滚到一个不是给定垫后代的目标垫时,退出任何这些垫。

注意到ret指令不是退出一个funclet垫的有效方式;当进入一个垫,但还没退出时,执行ret是未定义行为。

单条回滚边可以退出任意数量的垫(条件是来自catchswitch的边至少退出自己,来自cleanupret的边必须至少退出其cleanuppad),然后必须只进入一个垫,这个垫必须不能是所有已退出的垫。一条回滚边进入的垫的父亲必须是最近进入、还未退出的垫(在该回滚边退出的垫都退出后),或者如果不存在这样的垫,是“none”。这确保正在执行的funclet的栈,在运行时,总是对应父符号编码的funclet垫的树的某条路径。

所有退出任意给定funclet垫的回滚边(包括退出包含的cleanuppad的cleanupret边,以及退出自己的catchswitch边)必须共享相同的回滚目标。类似的,任何可能被回滚到调用者退出的funclet垫必须不能被任何回滚到其他地方的异常边退出。这确保每个funclet作为整体仅有一个回目标,这是用于funcletpersonality的EH表要求的。注意任何退出一个catchpad的回滚边也退出其父catchswitch,因此这暗示着对于任意给定的catchswitch,其回滚目标必须也是任何退出其构成catchpad的回滚边的回滚目标。因为catchswitch没有nounwind变种,且因为不要求IR生成器将不回滚的调用注释为nounwind,在一个具有一个调用者以外回滚目标的funclet垫中嵌套一个call或“回滚到调用者”catchswitch是合法的;这样回滚一个call或catchswitch是未定义行为。

最后,funclet垫的回滚目标不能形成一个环。这确保了EH降级可以一个树型结构构造“try”区域,这可能是基于funclet的personality所需的。

你可能感兴趣的:(compiler,编译器,llvm)