LLVM 中的异常处理

简介

本文档是 LLVM 中所有与异常处理相关信息的核心存储库。它描述了 LLVM 异常处理信息采用的格式,这对于有兴趣创建前端或直接处理这些信息的人很有用。此外,本文档提供了 C 和 C++ 中异常处理信息用途的具体示例。

Itanium ABI 零成本异常处理

大多数编程语言的异常处理旨在从应用程序常规使用过程中很少发生的条件中恢复。为此,异常处理不应通过执行检查点任务(例如保存当前 pc 或寄存器状态)来干扰应用程序算法的主要流程。

Itanium ABI 异常处理规范定义了一种方法,用于以异常表的形式提供外部数据,而无需在应用程序主算法的流程中内联推测性异常处理代码。因此,该规范被认为在应用程序的正常执行中增加了“零成本”。

有关 Itanium ABI 异常处理运行时支持的更完整描述,请参见 Itanium C++ ABI:异常处理。有关异常框架格式的描述,请参见 异常框架,DWARF 4 规范的详细信息请参见 DWARF 4 标准。有关 C++ 异常表格式的描述,请参见 异常处理表

Setjmp/Longjmp 异常处理

基于 Setjmp/Longjmp (SJLJ) 的异常处理使用 LLVM 内联函数 llvm.eh.sjlj.setjmpllvm.eh.sjlj.longjmp 来处理异常处理的控制流。

对于每个执行异常处理的函数(无论是 try/catch 块还是清理操作),该函数都会在全局帧列表上注册自身。当异常展开时,运行时使用此列表来识别哪些函数需要处理。

着陆点选择编码在函数上下文的调用站点条目中。运行时通过 llvm.eh.sjlj.longjmp 返回到函数,其中一个开关表根据存储在函数上下文中的索引将控制权转移到相应的着陆点。

与 DWARF 异常处理(在内联表中对异常区域和框架信息进行编码)相比,SJLJ 异常处理在运行时构建和移除展开框架上下文。这导致异常处理速度更快,但代价是在不抛出异常时执行速度较慢。由于异常本质上是为非常规代码路径设计的,因此通常优先选择 DWARF 异常处理而不是 SJLJ。

Windows 运行时异常处理

LLVM 支持处理由 Windows 运行时产生的异常,但它需要一个非常不同的中间表示。它不像其他两种模型那样基于“landingpad”指令,并在本文档后面 使用 Windows 运行时的异常处理 部分进行描述。

概述

当在 LLVM 代码中抛出异常时,运行时会尽力找到适合处理这种情况的处理程序。

运行时首先尝试查找与抛出异常的函数对应的 *异常框架*。如果编程语言支持异常处理(例如 C++),则异常框架包含对异常表的引用,该异常表描述了如何处理异常。如果语言不支持异常处理(例如 C),或者如果需要将异常转发到先前的激活,则异常框架包含有关如何展开当前激活并恢复先前激活状态的信息。此过程会重复进行,直到处理异常。如果未处理异常且没有剩余激活,则应用程序将终止并显示相应的错误消息。

由于不同的编程语言在处理异常时具有不同的行为,因此异常处理 ABI 提供了一种机制来提供 *个性*。异常处理个性通过 *个性函数*(例如 C++ 中的 __gxx_personality_v0)来定义,该函数接收异常的上下文、包含异常对象类型和值的 *异常结构* 以及对当前函数异常表的引用。当前编译单元的个性函数在 *通用异常框架* 中指定。

异常表的组织方式取决于语言。对于 C++,异常表被组织成一系列代码范围,这些代码范围定义了如果在该范围内发生异常该怎么做。通常,与范围关联的信息定义了在该范围内处理的异常对象类型(使用 C++ *类型信息*),以及应该采取的相关操作。操作通常将控制权传递给 *着陆点*。

着陆点大致对应于在 try/catch 序列的 catch 部分中找到的代码。当执行在着陆点恢复时,它会接收一个 *异常结构* 和一个 *选择器值*,该值对应于抛出的异常的 *类型*。然后使用选择器来确定哪个 *catch* 实际上应该处理异常。

LLVM 代码生成

从 C++ 开发人员的角度来看,异常是根据 throwtry/catch 语句定义的。在本节中,我们将根据 C++ 示例描述 LLVM 异常处理的实现。

抛出异常 (Throw)

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

  1. 请求为异常结构分配异常空间。此结构需要在当前激活之外持续存在。此结构将包含正在抛出的对象的类型和值。

  2. 调用运行时以引发异常,并将异常结构作为参数传递。

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

Try/Catch

try 语句范围内进行的调用可能会引发异常。在这些情况下,LLVM C++ 前端会将调用替换为 invoke 指令。与调用不同,invoke 具有两个潜在的继续点

  1. 当调用按正常情况成功时继续执行的位置,以及

  2. 如果调用引发异常(通过抛出异常或抛出异常的展开)继续执行的位置

用于定义 invoke 在异常后继续执行的位置的术语称为 *着陆点*。LLVM 着陆点在概念上是替代函数入口点,其中异常结构引用和类型信息索引作为参数传递。着陆点保存异常结构引用,然后继续选择对应于异常对象类型信息的 catch 块。

LLVM ‘landingpad’ 指令 用于将有关着陆点的信息传达给后端。对于 C++,landingpad 指令返回一个指针和整数对,分别对应于 *异常结构* 的指针和 *选择器值*。

在父函数的属性列表中,landingpad 指令查找对要用于此 try/catch 序列的个性函数的引用。该指令包含 *清理*、*catch* 和 *过滤器* 子句的列表。异常会从第一个到最后一个依次针对这些子句进行测试。这些子句具有以下含义

  • catch <type> @ExcType

    • 此条款表示,如果抛出的异常类型为@ExcType@ExcType的子类型,则应进入landingpad块。对于C++,@ExcType是指向表示C++异常类型的std::type_info对象的指针(一个RTTI对象)。

    • 如果@ExcTypenull,则任何异常都匹配,因此应始终进入landingpad。这用于C++的catch-all块(“catch (...)”)。

    • 当此条款匹配时,选择器值将等于“@llvm.eh.typeid.for(i8* @ExcType)”返回的值。这将始终是一个正值。

  • filter <type> [<type> @ExcType1, ..., <type> @ExcTypeN]

    • 此条款表示,如果抛出的异常与列表中的任何类型都不匹配,则应进入landingpad(对于C++,这些类型再次指定为std::type_info指针)。

    • C++前端使用此功能来实现C++异常规范,例如“void foo() throw (ExcType1, ..., ExcTypeN) { ... }”。(注意:此功能在C++11中已弃用,并在C++17中删除)。

    • 当此条款匹配时,选择器值将为负。

    • filter的数组参数可以为空;例如,“[0 x i8**] undef”。这意味着应始终进入landingpad。(注意,此filter与“catch i8* null”并不等价,因为filtercatch分别生成负和正的选择器值)。

  • cleanup

    • 此条款表示应始终进入landingpad。

    • C++前端使用此功能来调用对象的析构函数。

    • 当此条款匹配时,选择器值将为零。

    • 运行时可能会将“cleanup”与“catch <type> null”区别对待。

      在C++中,如果发生未处理的异常,语言运行时将调用std::terminate(),但运行时是否首先展开堆栈并调用对象析构函数是实现定义的。例如,GNU C++展开器在发生未处理的异常时不会调用对象析构函数。这样做的原因是为了提高可调试性:它确保从throw的上下文中调用std::terminate(),以便在展开堆栈时不会丢失此上下文。运行时通常会通过搜索匹配的非cleanup条款来实现这一点,如果找不到,则在进入任何landingpad块之前中止。

一旦landing pad拥有类型信息选择器,代码就会分支到第一个catch的代码。然后,catch会将类型信息选择器的值与该catch的类型信息的索引进行比较。由于类型信息索引在后端收集所有类型信息之前是未知的,因此catch代码必须调用llvm.eh.typeid.for内联函数来确定给定类型信息的索引。如果catch未能匹配选择器,则控制权将传递给下一个catch。

最后,catch代码的入口和出口用对__cxa_begin_catch__cxa_end_catch的调用括起来。

  • __cxa_begin_catch将异常结构引用作为参数,并返回异常对象的值。

  • __cxa_end_catch不带参数。此函数

    1. 查找最近捕获的异常并将其处理程序计数减 1,

    2. 如果处理程序计数变为零,则从捕获堆栈中删除异常,并且

    3. 如果处理程序计数变为零且异常未由throw重新抛出,则销毁异常。

    注意

    从catch内部重新抛出可能会用__cxa_rethrow替换此调用。

清理

清理是需要作为展开作用域的一部分运行的额外代码。C++析构函数是一个典型的例子,但其他语言和语言扩展提供了各种不同的清理类型。通常,landing pad可能需要在实际进入catch块之前运行任意数量的清理代码。为了指示清理的存在,‘landingpad’指令应该有一个cleanup条款。否则,如果没有任何catch或filter需要它,则展开器将不会在landing pad处停止。

注意

不要允许新的异常从清理的执行中传播出去。这可能会破坏展开器的内部状态。不同的语言对这些情况描述了不同的高级语义:例如,C++要求终止进程,而Ada则取消两个异常并抛出一个第三个异常。

当所有清理完成后,如果当前函数未处理异常,则通过调用resume指令恢复展开,并将原始landing pad的landingpad指令的结果传递给它。

抛出过滤器

在C++17之前,C++允许指定可以从函数中抛出的异常类型。为了表示这一点,可能存在一个顶级landing pad来过滤掉无效类型。为了在LLVM代码中表达这一点,‘landingpad’指令将有一个filter条款。该条款由一个类型信息数组组成。landingpad如果异常与任何类型信息都不匹配,则将返回一个负值。如果没有找到匹配项,则应调用__cxa_call_unexpected,否则调用_Unwind_Resume。这两个函数都需要一个指向异常结构的引用。请注意,landingpad指令的最通用形式可以具有任意数量的catch、cleanup和filter条款(尽管拥有多个cleanup毫无意义)。由于内联创建了嵌套的异常处理作用域,因此LLVM C++前端可以生成此类landingpad指令。

限制

展开器将是否在调用帧中停止的决定委托给该调用帧的特定于语言的个性函数。并非所有展开器都能保证它们会停止执行清理。例如,GNU C++展开器不会这样做,除非异常实际上是在堆栈中更高级别的地方被捕获的。

为了使内联能够正确地执行,landing pad必须准备好处理它们最初未声明的选择器结果。假设一个函数捕获类型为A的异常,并且它被内联到一个捕获类型为B的异常的函数中。内联器将更新内联landing pad的landingpad指令,以包含B也被捕获的事实。如果该landing pad假设它只会进入捕获A,那么它将面临一个粗暴的觉醒。因此,landing pad必须测试它们理解的选择器结果,然后如果没有任何条件匹配,则使用resume指令恢复异常传播。

异常处理内联函数

除了landingpadresume指令之外,LLVM还使用几个内联函数(名称以llvm.eh为前缀)在生成的代码中的各个点提供异常处理信息。

llvm.eh.typeid.for

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

此内联函数返回当前函数的异常表中的类型信息索引。此值可用于与landingpad指令的结果进行比较。单个参数是对类型信息的引用。

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.longjmp用作目标地址。此内联函数的缓冲区格式和整体功能与GCC __builtin_setjmp实现兼容,允许使用clang和GCC构建的代码互操作。

单个参数是指向一个五字缓冲区的指针,在该缓冲区中保存调用上下文。前端将帧指针放在第一个字中,并且此内联函数的目标实现应将llvm.eh.sjlj.longjmp的目标地址放在第二个字中。以下三个字可用于目标特定的方式。

llvm.eh.sjlj.longjmp

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

对于基于 SJLJ 的异常处理,llvm.eh.sjlj.longjmp 内联函数用于实现 __builtin_longjmp()。单个参数是指向由 llvm.eh.sjlj.setjmp 填充的缓冲区的指针。帧指针和栈指针从缓冲区中恢复,然后控制转移到目标地址。

llvm.eh.sjlj.lsda

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

对于基于 SJLJ 的异常处理,llvm.eh.sjlj.lsda 内联函数返回当前函数的语言特定数据区域 (LSDA) 的地址。SJLJ 前端代码将此地址存储在异常处理函数上下文中,供运行时使用。

llvm.eh.sjlj.callsite

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

对于基于 SJLJ 的异常处理,llvm.eh.sjlj.callsite 内联函数标识与以下 invoke 指令关联的调用站点值。这用于确保以匹配的顺序生成 LSDA 中的着陆点条目。

Asm 表格格式

异常处理运行时使用两个表格来确定在抛出异常时应采取哪些操作。

异常处理帧

异常处理帧 eh_frame 与 DWARF 调试信息使用的展开帧非常相似。该帧包含拆卸当前帧并恢复先前帧状态所需的所有信息。每个编译单元中的每个函数都有一个异常处理帧,此外还有一个公共异常处理帧,用于定义单元中所有函数的公共信息。

但是,此调用帧信息 (CFI) 的格式通常依赖于平台。例如,ARM 定义了自己的格式。Apple 有自己的紧凑展开信息格式。在 Windows 上,自 32 位 x86 以来,所有架构都使用另一种格式。LLVM 将发出目标所需的信息。

异常表

异常表包含有关在函数代码的特定部分抛出异常时应采取哪些操作的信息。这通常称为语言特定数据区域 (LSDA)。LSDA 表格的格式特定于个性函数,但大多数个性都使用 __gxx_personality_v0 使用的表格的变体。每个函数都有一个异常表,除了叶函数和仅调用不抛出异常的函数的函数。它们不需要异常表。

使用 Windows 运行时进行异常处理

Windows 异常背景

与 Windows 上的异常交互比在 Itanium C++ ABI 平台上复杂得多。两种模型之间的根本区别在于 Itanium EH 是围绕“连续展开”的概念设计的,而 Windows EH 不是。

在 Itanium 下,抛出异常通常涉及分配线程本地内存以保存异常,并调用 EH 运行时。运行时识别具有适当异常处理操作的帧,并连续将当前线程的寄存器上下文重置为最近活动的操作帧。在 LLVM 中,执行在 landingpad 指令处恢复,该指令生成运行时提供的寄存器值。如果函数仅清理已分配的资源,则该函数负责调用 _Unwind_Resume 以在完成清理后转换为下一个最近活动帧。最终,负责处理异常的帧调用 __cxa_end_catch 来销毁异常,释放其内存并恢复正常的控制流。

Windows EH 模型不使用这些连续的寄存器上下文重置。相反,活动异常通常由堆栈上的一个帧描述。在 C++ 异常的情况下,异常对象在堆栈内存中分配,其地址传递给 __CxxThrowException。通用结构化异常 (SEH) 更类似于 Linux 信号,它们由 Windows 提供的用户空间 DLL 分派。堆栈上的每个帧都有一个分配的 EH 个性例程,该例程决定采取哪些操作来处理异常。C 和 C++ 代码有几个主要的个性:C++ 个性 (__CxxFrameHandler3) 和 SEH 个性 (_except_handler3_except_handler4__C_specific_handler)。所有这些都通过回调到父函数中包含的“funclet”来实现清理。

在这种情况下,funclet 是父函数的区域,可以像调用函数指针一样调用它们,并且具有非常特殊的调用约定。父帧的帧指针通过标准 EBP 寄存器或作为第一个参数寄存器传递到 funclet,具体取决于体系结构。funclet 通过帧指针访问内存中的局部变量来实现 EH 操作,并返回一些适当的值,继续 EH 过程。funclet 中没有变量可以分配在寄存器中。

C++ 个性还使用 funclet 来包含 catch 块的代码(即 catch (Type obj) { ... } 中括号之间的所有用户代码)。运行时必须对 catch 主体使用 funclet,因为 C++ 异常对象是在处理异常的函数的子堆栈帧中分配的。如果运行时将堆栈回绕到 catch 的帧,则保存异常的内存将很快被后续的函数调用覆盖。funclet 的使用还允许 __CxxFrameHandler3 在不诉诸 TLS 的情况下实现重新抛出。相反,运行时抛出一个特殊的异常,然后使用 SEH (__try / __except) 在子帧中使用新信息恢复执行。

换句话说,连续展开方法与 Visual C++ 异常和通用 Windows 异常处理不兼容。因为 C++ 异常对象位于堆栈内存中,所以 LLVM 无法提供使用着陆点的自定义个性函数。类似地,SEH 没有提供任何机制来重新抛出异常或继续展开。因此,LLVM 必须使用本文档后面描述的 IR 结构来实现兼容的异常处理。

SEH 筛选器表达式

SEH 个性函数还使用 funclet 来实现筛选器表达式,这些表达式允许执行任意用户代码以决定捕获哪些异常。筛选器表达式不应与 LLVM landingpad 指令的 filter 子句混淆。通常,筛选器表达式用于确定异常是否来自特定的 DLL 或代码区域,或者代码在访问特定内存地址范围时是否发生故障。LLVM 目前没有 IR 来表示筛选器表达式,因为很难表示它们的控制依赖关系。筛选器表达式在 EH 的第一阶段运行,在清理运行之前,这使得构建真实的控制流图非常困难。目前,新的 EH 指令无法表示 SEH 筛选器表达式,前端必须提前概述它们。可以使用 llvm.localescapellvm.localrecover 内联函数转义和访问父函数的局部变量。

新的异常处理指令

新 EH 指令的主要设计目标是在保留 CFG 信息的同时支持 funclet 生成,以便 SSA 形成仍然有效。作为次要目标,它们旨在跨 MSVC 和 Itanium C++ 异常通用。它们对个性所需的数据做很少的假设,只要它使用熟悉的核心 EH 操作:捕获、清理和终止即可。但是,如果不了解 EH 个性的详细信息,则很难修改新指令。虽然它们可用于表示 Itanium EH,但着陆点模型对于优化目的来说严格更好。

以下新指令被认为是“异常处理垫”,因为它们必须是可能成为 EH 流边的展开目标的基本块的第一个非 phi 指令:catchswitchcatchpadcleanuppad。与着陆点一样,在进入 try 范围时,如果前端遇到可能抛出异常的调用站点,则应发出一个展开到 catchswitch 块的 invoke。类似地,在具有析构函数的 C++ 对象的范围内,invoke 应展开到 cleanuppad

新指令还用于标记从 catch/cleanup 处理程序中转移控制的点(这将对应于从生成的 funclet 中退出)。通过正常执行到达其末尾的 catch 处理程序执行 catchret 指令,这是一个指示控制返回到函数中何处的终止符。通过正常执行到达其末尾的清理处理程序执行 cleanupret 指令,这是一个指示活动异常接下来将展开到何处的终止符。

这些新的 EH pad 指令都有一种方法可以识别在该指令之后应该执行哪个操作。 catchswitch 指令是一个终止符,并具有类似于 invoke 的 unwind 目标操作数。 cleanuppad 指令不是终止符,因此 unwind 目标存储在 cleanupret 指令中。成功执行 catch 处理程序应恢复正常的控制流,因此 catchpadcatchret 指令都不能 unwind。所有这些“unwind 边缘”都可能引用包含 EH pad 指令的基本块,或者它们可能 unwind 到调用方。unwind 到调用方与 landingpad 模型中的 resume 指令具有大致相同的语义。在内联 invoke 时,unwind 到调用方的指令将连接到 unwind 到调用站点的 unwind 目标。

将这些内容整合在一起,下面是使用所有新 IR 指令的一些 C++ 的假设降低示例。

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 ptr @__CxxFrameHandler3 {
entry:
  %obj = alloca %struct.Cleanup, align 4
  %e = alloca i32, align 4
  %call = invoke ptr @"??0Cleanup@@QEAA@XZ"(ptr nonnull %obj)
          to label %invoke.cont unwind label %lpad.catch

invoke.cont:                                      ; preds = %entry
  invoke void @"?may_throw@@YAXXZ"()
          to label %invoke.cont.2 unwind label %lpad.cleanup

invoke.cont.2:                                    ; preds = %invoke.cont
  call void @"??_DCleanup@@QEAA@XZ"(ptr 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 @"??1Cleanup@@QEAA@XZ"(ptr nonnull %obj) nounwind
  cleanupret from %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 [ptr @"??_R0H@8", i32 0, ptr %e]
  invoke void @"?may_throw@@YAXXZ"()
          to label %invoke.cont.3 unwind label %lpad.terminate

invoke.cont.3:                                    ; preds = %catch.body
  %3 = load i32, ptr %e, align 4
  catchret from %catch to label %return

lpad.terminate:                                   ; preds = %catch.body, %lpad.catch
  cleanuppad within none []
  call void @"?terminate@@YAXXZ"()
  unreachable
}

Funclet 父标记

为了生成使用 funclets 的 EH 个性表,有必要恢复源代码中存在的嵌套结构。这种 funclet 父关系使用新“pad”指令生成的标记在 IR 中进行编码。 “pad”或“ret”指令的标记操作数指示它位于哪个 funclet 中,或者如果它没有嵌套在另一个 funclet 中,则指示“none”。

catchpadcleanuppad 指令建立新的 funclets,它们的标记被其他“pad”指令使用以建立成员关系。 catchswitch 指令不会创建 funclet,但它会生成一个始终被其直接后继 catchpad 指令使用的标记。这确保了由 catchpad 建模的每个 catch 处理程序都属于一个 catchswitch,该指令对 C++ try 之后的调度点进行建模。

下面是一个使用一些假设的 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 使用 %1,该标记由外部 catchswitch 生成。

Funclet 转换

使用 funclets 的个性表的 EH 表隐式地使用 funclet 嵌套关系来编码 unwind 目标,因此在其可以表示的 funclet 转换集上受到限制。相关的 LLVM IR 指令相应地具有约束条件,以确保 EH 边缘在流图中可编码。

catchswitchcatchpadcleanuppad 在执行时被称为“进入”。随后可以通过以下任何方式“退出”

  • catchswitch 的任何组成 catchpad 均不适合正在进行的异常并且它 unwind 到其 unwind 目标或调用方时,它会立即退出。

  • 当从 catchpad 执行 catchret 时,catchpad 及其父 catchswitch 均会退出。

  • 当从 cleanuppad 执行 cleanupret 时,cleanuppad 会退出。

  • 当控制 unwind 到函数的调用方时,任何这些 pad 都会退出,这可能是由 unwind 一直到函数调用方的 call、标记为“unwinds to caller”的嵌套 catchswitch 或标记为“unwinds to caller"的嵌套 cleanuppadcleanupret 引起的。

  • 当 unwind 边缘(来自 invoke、嵌套的 catchswitch 或嵌套的 cleanuppadcleanupret)unwind 到不是给定 pad 的后代的目标 pad 时,任何这些 pad 都会退出。

请注意,ret 指令*不是*退出 funclet pad 的有效方法;当已进入但尚未退出 pad 时执行 ret 是未定义的行为。

单个 unwind 边缘可能会退出任意数量的 pad(受限于来自 catchswitch 的边缘必须至少退出自身,以及来自 cleanupret 的边缘必须至少退出其 cleanuppad),然后必须进入正好一个 pad,该 pad 必须与所有退出的 pad 不同。unwind 边缘进入的 pad 的父级必须是最近进入的尚未退出的 pad(在退出 unwind 边缘退出的任何 pad 之后),或者如果不存在这样的 pad,则为“none”。这确保了运行时正在执行的 funclets 的堆栈始终对应于 funclet 父标记编码的 funclet pad 树中的某些路径。

退出任何给定 funclet pad 的所有 unwind 边缘(包括退出其 cleanuppadcleanupret 边缘和退出其 catchswitchcatchswitch 边缘)必须共享相同的 unwind 目标。类似地,任何可能由 unwind 到调用方退出的 funclet pad 都不能由任何 unwind 到调用方以外任何地方的异常边缘退出。这确保每个 funclet 作为一个整体只有一个 unwind 目标,使用 funclet 个性的 EH 表可能需要此目标。请注意,任何退出 catchpad 的 unwind 边缘也会退出其父 catchswitch,因此这意味着对于任何给定的 catchswitch,其 unwind 目标也必须是退出其任何组成 catchpad 的任何 unwind 边缘的 unwind 目标。因为 catchswitch 没有 nounwind 变体,并且因为 IR 生成器*不需要*将不会 unwind 的调用注释为 nounwind,所以在具有除调用方之外的 unwind 目标的 funclet pad 内嵌套 call 或“unwind to callercatchswitch 是合法的;对于此类 callcatchswitch unwind 是未定义的行为。

最后,funclet pad 的 unwind 目标不能形成循环。这确保 EH 降低可以构建具有树状结构的“try 区域”,使用 funclet 的个性可能需要此结构。

目标上的异常处理支持

为了支持特定目标上的异常处理,需要实现一些内容。

  • CFI 指令

    首先,您必须为每个目标寄存器分配一个唯一的 DWARF 编号。然后在 TargetFrameLoweringemitPrologue 中,您必须发出 CFI 指令 以指定如何计算 CFA(规范帧地址)以及如何从 CFA 指向的地址使用偏移量恢复寄存器。汇编程序通过 CFI 指令指示构建 .eh_frame 部分,该部分由 unwinder 在异常处理期间用于 unwind 堆栈。

  • getExceptionPointerRegistergetExceptionSelectorRegister

    TargetLowering 必须实现这两个函数。*个性函数*通过 getExceptionPointerRegistergetExceptionSelectorRegister 分别指定的寄存器将*异常结构*(指针)和*选择器值*(整数)传递到 landing pad。在大多数平台上,它们将是 GPR,并且与调用约定中指定的相同。

  • EH_RETURN

    ISD 节点表示未记录的 GCC 扩展 __builtin_eh_return (offset, handler),它通过偏移量调整堆栈,然后跳转到处理程序。 __builtin_eh_return 用于 GCC unwinder (libgcc),但不用于 LLVM unwinder (libunwind)。如果您位于 libgcc 的顶部并在您的目标上具有特定要求,则必须在 TargetLowering 中处理 EH_RETURN

如果您不利用现有的运行时(libstdc++libgcc),则需要查看 libc++libunwind 以了解在那里需要做什么。对于 libunwind,您需要执行以下操作

  • __libunwind_config.h

    为您的目标定义宏。

  • include/libunwind.h

    为目标寄存器定义枚举。

  • src/Registers.hpp

    为您的目标定义 Registers 类,实现 setter 和 getter 函数。

  • src/UnwindCursor.hpp

    为您的 Registers 类定义 dwarfEncodingstepWithCompactEncoding

  • src/UnwindRegistersRestore.S

    编写一个汇编函数,从内存中恢复所有目标寄存器。

  • src/UnwindRegistersSave.S

    编写一个汇编函数,将所有目标寄存器保存到内存中。