LLVM 项目博客

LLVM 项目新闻和来自前线的详细信息

LLVM 3.0 异常处理重新设计

LLVM 3.0 版本中最大的 IR 变化之一是 LLVM IR 异常处理模型的重新设计和重新实现。旧模型虽然在大多数情况下都能正常工作,但在一些关键情况下却会失效,导致难以理解的编译错误、优化遗漏和糟糕的编译时间。这篇文章讨论了 LLVM 3.0 中的更改以及如何将现有的 LLVM 前端迁移到新的设计。它假设读者对 Itanium C++ ABI 的异常处理有一定了解。

异常处理系统的目标

异常处理需要成为 LLVM IR 的一等公民。这让我们能够以智能的方式操作异常处理信息(例如,在内联期间)。此外,代码生成需要能够可靠地找到与特定invoke调用相关联的各种信息(例如,与调用一起使用的个性函数)。最后,我们需要遵循已建立的异常处理 ABI,以确保与其他编译器的二进制兼容性。

虽然要让异常处理正常工作(关于 ABI),需要许多细节需要正确处理,但我们的目标是使 LLVM IR 尽可能易于生成和操作。通过使 EH 成为一等公民,新的指令将具有简单易懂的语法和约束,这些约束可以进行测试以确保在每次代码转换后 IR 都是正确的。

旧的异常处理系统

旧系统使用 LLVM 内联函数将异常处理信息传达给代码生成器。旧系统的主要问题是,没有任何东西将这些内联函数绑定到可以被解开调用的 invoke 调用,这使得代码生成变得脆弱,并且像内联这样的优化无法表示(在一般情况下)。

此外,内联函数对于代码转换来说非常难以维护和正确更新:我们经常会得到异常表,其中包含错误信息(例如,指定特定类型无法传播到该点,而这在原始程序中并没有指定)。它也不能在没有大量工作的情况下处理“清理”情况。

由于正常的代码移动,内联函数(包含代码生成器生成正确表所需的信息)可以从invoke它们所关联的指令移动很远。也就是说,它们可以从invoke's landingpad 移动。这使得以前异常处理构造的代码生成变得脆弱,有时会导致异常处理代码的编译错误,这是不可接受的。

最后一个(有点理论上的)问题是旧系统只适用于标准个性函数。它几乎不可能使用自定义个性函数(例如,在 landingpad 中返回 3 个寄存器而不是 2 个)。虽然我们没有这个特定的用例,但我们无法使用自定义个性函数来优化 C++ 异常的代码大小或性能。

LLVM 3.0 异常处理系统

新异常处理系统的支柱是两个新的指令landingpadresume:
landingpad
定义一个 landingpad 基本块。它包含代码生成器生成正确 EH 表所需的所有信息。它也必须是 invoke 指令解开目的地的第一个非PHI指令。此外,landingpad 只能被invoke指令的解开边跳转到。这些约束确保始终能够准确地将解开信息与 invoke 调用匹配起来。它取代了@llvm.eh.exception@llvm.eh.selector内联函数。
resume
导致当前异常恢复传播到堆栈的上层。它取代了@llvm.eh.resume内联函数。

这是一个简单示例,展示了新的语法是什么样的。对于这个程序



void bar();
void foo() throw (const char *) {
try {
bar();
} catch (int) {
}
}

IR 看起来像这样

@_ZTIPKc = external constant i8*
@_ZTIi = external constant i8*
define void @_Z3foov() uwtable ssp {
entry:
invoke void @_Z3barv()
to label %try.cont unwind label %lpad

lpad:
%0 = landingpad { i8*, i32 } personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*)
catch i8* bitcast (i8** @_ZTIi to i8*)
filter [1 x i8*] [i8* bitcast (i8** @_ZTIPKc to i8*)]

%1 = extractvalue { i8*, i32 } %0, 0
%2 = extractvalue { i8*, i32 } %0, 1
%3 = tail call i32 @llvm.eh.typeid.for(i8* bitcast (i8** @_ZTIi to i8*)) nounwind
%matches = icmp eq i32 %2, %3
br i1 %matches, label %catch, label %filter.dispatch

filter.dispatch:
%ehspec.fails = icmp slt i32 %2, 0
br i1 %ehspec.fails, label %ehspec.unexpected, label %eh.resume

ehspec.unexpected:
tail call void @__cxa_call_unexpected(i8* %1) no return
unreachable

catch:
%4 = tail call i8* @__cxa_begin_catch(i8* %1) nounwind
tail call void @__cxa_end_catch() nounwind
br label %try.cont

try.cont:
ret void

eh.resume:
resume { i8*, i32 } %0
}

Thelandingpad指令指定 EH 运行时使用的个性函数,它可以捕获的类型列表(int),以及foo允许抛出的类型列表(const char *).

Theresume指令如果异常没有被捕获并且是允许的类型,则恢复异常传播。

转换为 LLVM 3.0 异常处理系统

从旧的 EH API 转换为新的 EH API 非常简单,因为很多复杂性都被去掉了。要生成 LLVM 2.9 中的 EH 代码,您需要执行类似于以下操作

Function *ExcIntr =
Intrinsic::getDeclaration(TheModule, Intrinsic::eh_exception);
Function *SlctrIntr =
Intrinsic::getDeclaration(TheModule, Intrinsic::eh_selector);
Function *PersonalityFn =
Function::Create(FunctionType::get(Type::getInt32Ty(Context), true),
Function::ExternalLinkage,
"__gxx_personality_v0", TheModule);

// The exception pointer.
Value *ExnPtr = Builder.CreateCall(ExcIntr, "exn");

// The arguments to the @llvm.eh.selector instruction.
std::vector<Value*> Args; Args.push_back(ExnPtr);
Args.push_back(Builder.CreateBitCast(PersonalityFn,
Type::getInt8PtrTy(Context)));

// ... Complex code to add catch types, filters, cleanups, and catch-alls to Args ...

// The selector call.
Value *Sel = Builder.CreateCall(SlctrIntr, Args, "exn.sel");

您应该改为生成一个landingpad指令,它返回一个异常对象和选择器值

LandingPadInst *LPadInst =
Builder.CreateLandingPad(StructType::get(Int8PtrTy, Int32Ty, NULL),
PersonalityFn, 0);
Value *ExnPtr = Builder.CreateExtractValue(LPadInst, 0);
Value *Sel = Builder.CreateExtractValue(LPadInst, 1);

现在可以轻松地将各个子句添加到landingpad指令中。

// Adding a catch clause
Constant *TypeInfo = getTypeInfo();
LPadInst->addClause(TypeInfo);

// Adding a C++ catch-all
LPadInst->addClause(Constant::getNullValue(Builder.getInt8PtrTy()));

// Adding a cleanup
LPadInst->setCleanup(true);

// Adding a filter clause
std::vector<Value*> TypeInfos;
Constant *TypeInfo = getFilterTypeInfo();
TypeInfos.push_back(Builder.CreateBitCast(TypeInfo, Builder.getInt8PtrTy()));
ArrayType *FilterTy = ArrayType::get(Int8PtrTy, TypeInfos.size());
LPadInst-<addClause(ConstantArray::get(FilterTy, TypeInfos));

从使用@llvm.eh.resume内联函数转换为使用resume指令非常简单。它采用由landingpad指令

Type *UnwindDataTy = StructType::get(Builder.getInt8PtrTy(),
Builder.getInt32Ty(), NULL);
Value *UnwindData = UndefValue::get(UnwindDataTy);
Value *ExcPtr = Builder.CreateLoad(getExceptionObjSlot());
Value *ExcSel = Builder.CreateLoad(getExceptionSelSlot());
UnwindData = Builder.CreateInsertValue(UnwindData, ExcPtr, 0, "exc_ptr");
UnwindData = Builder.CreateInsertValue(UnwindData, ExcSel, 1, "exc_sel");
Builder.CreateResume(UnwindData);

结论

新的 EH 系统现在比旧系统工作得更好。它更不容易出错,也更不复杂。这使得在您需要阅读 IR 以了解发生了什么时更容易理解。更重要的是,它让我们比以前更紧密地遵循 ABI。

更棒的是,从旧系统转换到新系统非常简单。事实上,您可能会发现您的代码变得更简单!如果您想了解更多详细信息和参考信息,请参阅 LLVM IR 中的异常处理 文档。