原文:http://blog.llvm.org/2011/11/llvm-30-exception-handling-redesign.html
LLVM 3.0中最大的IR改变之一是重新设计、实现了LLVMIR异常处理模型。旧的模型,虽然对大多数情形都工作,在某些关键情形下跌倒,导致隐蔽的误编译,未达成的优化,以及差劲的编译用时。本文讲述LLVM3.0中的改变,以及如何将一个现有的LLVM前端迁移到新的设计。它假定你对ItaniumC++ ABI异常处理部分有一定了解。
异常处理相同的目标
异常处理需要是LLVM IR的一等公民。这允许我们以一个聪明的方式操作异常处理信息(比如在内联期间)。同样,代码生成需要能够可靠地找出与指定invoke调用关联的各种信息(比如与一个调用一起使用的personality函数)。最后,我们需要遵守已制定的异常处理ABI来确保与其他编译器的二进制兼容。
尽管许多细节正确,异常处理才能工作(就ABI而言),我们的目标是保持LLVMIR的生成尽可能简单,操作尽可能灵活。通过使EH成为一等公民,新指令将具有简单、容易理解的语法,以及在每个代码转换后可以被测试确保IR正确的约束。
旧的异常处理系统
旧的系统使用LLVM固有函数来向代码生成器传递异常处理信息。旧系统的主要问题是,没有什么可以将这些固有函数绑定到可以回滚的invoke调用,这使得代码生成脆弱,像内联这样的优化无从表示(在通常情形下)。
另外,代码转换很难正确维护及更新固有函数:我们可以频繁地获取带有不正确信息的异常表(比如在没有在原始程序中指定时,指定一个指定的类型不能传播越过某点)。而不花费大量的工作,也不可能处理“清理”情形。
因为正常的代码移动,持有代码生成器生成正确表所需信息的固有函数,可以远离它们原本关联的invoke指令,即它们可以会离开invoke的着陆场(landingpad)。这使得前面异常处理结构的代码生成变得脆弱,有时会导致异常处理代码的错误编译,这是不可接受的。
最后(有时是理论上)的问题是旧系统仅对标准的personality函数工作。通过它使用定制的personality函数几乎不可能(比如返回在一个着陆场中的3个寄存器,而不是2个)。尽管对此我们没有特别的用例,我们不能使用定制的personality函数来优化代码大小或C++异常的执行。
LLVM 3.0异常处理系统
新异常处理系统的骨干是两个新指令landingpad与resume:
Landingpad
定义了一个着陆场基本块。它包含代码生成器生成正确EH表所需的所有信息。它也被要求是一个invoke指令的回滚终点里的第一条非PHI指令。另外,一个着陆场可能仅能从一条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 unwindlabel %lpad
lpad:
%0 = landingpad { i8*, i32 }personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*)
catchi8* bitcast (i8** @_ZTIi to i8*)
filter [1 xi8*] [i8* bitcast (i8** @_ZTIPKc to i8*)]
%1 = extractvalue { i8*, i32 } %0, 0
%2 = extractvalue { i8*, i32 } %0, 1
%3 = tail call [email protected](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
}
Landingpad指令指定了EH运行时使用的personality函数,一组类型它可以捕捉的类型(int),以及一组foo被允许抛出的类型(constchar*)。
Resume指令重启异常的传播,如果它没有被捕捉并且是允许的类型。
转换到LLVM 3.0异常处理系统
从旧的EH API转换到新的EHAPI相当简单,因为许多复杂性被移除了。在LLVM2.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);
// Theexception pointer.
Value *ExnPtr =Builder.CreateCall(ExcIntr, "exn");
// Thearguments 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 ...
// Theselector 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 aC++ catch-all
LPadInst->addClause(Constant::getNullValue(Builder.getInt8PtrTy()));
// Adding acleanup
LPadInst->setCleanup(true);
// Adding afilter 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。
更好的是,从旧系统转换到新系统相当简单。事实上,你可以看到你的代码简单多了!如果你对更多细节及参考信息感兴趣,请参考LLVMIR异常处理文档。