源码路径
llvm\include\llvm\IR\Instruction.h
llvm\include\llvm\IR\Instruction.def
llvm\include\llvm\IR\Instructions.h
llvm\include\llvm\IR\InstrTypes.h
LLVM中的异常处理
当LLVM的代码中抛出异常时,LLVM runtime会先尝试找到对应函数的异常帧(exception frame),如果是支持异常处理的语言(如C++),则异常帧中会包含异常处理表(exception table)的引用,描述怎么处理异常。
异常处理表是语言相关的,对C++语言,异常表由一系列代码区间(code range)组成,代码区间描述了当异常在这个区间发生时应该如何处理。典型地,包含了可以处理的异常对象(exception object)的类型和应该采取的行动(action),而行动通常是把控制流转移到降落垫(landing pad)。在降落垫中,选择器(selector)会接收到异常结构(exception structure)和1个异常类型相关的选择器值(selector value),这样选择器就可以决定使用哪个catch中的代码来处理当前的异常。
llvm支持3种类型的异常处理:
- Itanium ABI异常处理(GCC):通过将异常相关指令和数据放到正常程序控制流之外,实现异常不发生时近似零开销的异常处理。
- Setjmp/Longjmp异常处理:将异常处理代码融入正常程序控制流,优点是异常处理速度更快,缺点是异常不发生时也有额外的开销。
- Windows运行时异常处理:与上述两者都不同,是Windows专属的异常处理机制。
本文后续内容主要介绍Itanium ABI异常处理,以及相关的llvm指令。
Itanium C++ ABI:异常处理
异常处理数据结构(Exception Handling Data Structures)
异常处理机制中,主要使用3种数据结构:
Unwind Table:
start:函数起始地址
end:函数结束地址
info ptr:指向一个变长的Info Block,里面包含展开描述符和语言相关的数据区。
Info Block:
v:版本号
ulen:unwind descriptor的长度。
Personality:指向Personality Routine的指针。
f:标志,可以通过如下代码获取,在C++中EHANDLER标志表示函数是否存在try-catch region,UHANDLER标志表示是否存在CleanUp Actions。
#define UNW_FLAG_MASK 0x0000f f f f 00000000L
#define UNW_FLAG_OSMASK 0x0000f 00000000000L
#define UNW_FLAG_EHANDLER(x) ((x) & 0x0000000100000000L)
#define UNW_FLAG_UHANDLER(x) ((x) & 0x0000000200000000L)
unwind descriptor:由一系列Records组成,Records分为3类:
- region header records
- descriptor records for prologue regions
- descriptor records for body regions
这些描述组用于组合起来描述Prologue Region和Body Region,用于描述如何正确地进行栈展开。
语言相关的数据区(Language-specific data area):
tcnt:try/catch region table entry的数量
ccnt:cleanup action table entry的数量
try/catch region table entry:
- start:region起始地址
- end:region结束地址
- catch:指向catch语句块的指针
- handler:指向exception handler的指针
cleanup action table entry:
- start:region起始地址
- end:region结束地址
- action:指向cleanup action的指针
异常处理框架(Exception Handling Framework)
Itanium架构下的异常处理如下所示,分为语言相关和语言无关的两个部分。
语言无关的部分负责:
- 传递异常
- 分发异常(回调Personality Routine)
- 栈展开
语言相关的部分:在语言运行时库中提供个性化的过程(personality routine),配合语言无关部分,完成异常处理。
异常处理的过程需要顺序进行2次栈展开:
第1次:
- Unwind library从当前栈帧中找到PC指针
- Unwind library通过Unwind Table找到地址范围内的Personality Routine的地址。
- Unwind library调用Personality Routine,完成:1)在当前栈帧中寻找能够处理异常的Handler,如果找到,则终止第1次栈展开。2)将第2次栈展开中,landing pad回调的handler和调用landing pad的参数回传给Unwind library。
第2次:
- Unwind library在每一级栈帧中用使用Personality Routine设置的参数调用landing pad,landing pad逐级进行栈展开,恢复寄存器和栈状态,这部分代码由编译器后端生成。
- 接着会进行try block出口处相关的清理工作,比如try block中自动变量的析构,这部分由编译器前端生成。
- Exception Handler会根据异常类型生成一个Switch Value,如果在当前异常表中,没有匹配的Handler,则会执行Cleanup Actions,然后通过resume继续进行栈回退;否则会调用Handler进行处理,Handler会根据Switch Value选择对应的catch语句块进行处理。
- 当Handler处理完成后,Unwind library会判断栈展开过程结束,控制流会转移到handler对应的try语句块的结尾处,结束异常处理。
例如如下代码:
try { foo(); }
catch (TYPE1) { ... }
catch (TYPE2) { buz(); }
bar();
生成的汇编代码如下:
// In "Normal" area:
foo(); // Call Attributes: Landing Pad L1, Action Record A1
goto E1;
...
E1: // End Label
bar();
// In "Exception" area;
L1: // Landing Pad label
[Back-end generated "compensation" code]
goto C1;
C1: // Cleanup label
[Front-end generated cleanup code, destructors, etc]
[corresponding to exit of try { } block]
goto S1;
S1: // Switch label
switch(SWITCH_VALUE_PAD_ARGUMENT)
{
case 1: goto H1; // For TYPE1
case 2: goto H2; // For TYPE2
//...
default: goto X1;
}
X1:
[Cleanup code corresponding to exit of scope]
[enclosing the try block]
_Unwind_Resume();
H1: // Handler label
adjusted_exception_ptr = __cxa_get_exception_ptr(exception);
[Initialize catch parameter]
__cxa_begin_catch(exception);
[User code]
goto R1;
H2:
adjusted_exception_ptr = __cxa_get_exception_ptr(exception);
[Initialize catch parameter]
__cxa_begin_catch(exception);
[User code]
buz(); // Call attributes: Landing pad L2, action record A2
goto R1;
R1: // Resume label:
__cxa_end_catch();
goto E1;
L2:
C2:
// Make sure we cleanup the current exception
__cxa_end_catch();
X2:
[Cleanup code corresponding to exit of scope]
[enclosing the try block]
_Unwind_Resume();
LLVM C++异常处理实现
对如下代码:
class MyException1 {
};
class MyException2 {
};
class AutoVar {
public:
~AutoVar() {
}
};
void foo(int a) {
if (a) {
throw MyException1();
} else {
throw MyException2();
}
}
void bar() {
}
void catch1(MyException1 &e) {
}
void catch2(MyException2 &e) {
}
void buz(int a) {
try {
AutoVar v;
foo(a);
}
catch (MyException1 &e) {
catch1(e);
}
catch (MyException2 &e) {
catch2(e);
}
bar();
}
生成的IR中,foo函数如下:
; Function Attrs: noinline optnone
define dso_local void @_Z3fooi(i32) #0 {
%2 = alloca i32, align 4
store i32 %0, i32* %2, align 4
%3 = load i32, i32* %2, align 4
%4 = icmp ne i32 %3, 0
br i1 %4, label %5, label %8
; <label>:5: ; preds = %1
%6 = call i8* @__cxa_allocate_exception(i64 1) #3
%7 = bitcast i8* %6 to %class.MyException1*
call void @__cxa_throw(i8* %6, i8* bitcast ({ i8*, i8* }* @_ZTI12MyException1 to i8*), i8* null) #4
unreachable
; <label>:8: ; preds = %1
%9 = call i8* @__cxa_allocate_exception(i64 1) #3
%10 = bitcast i8* %9 to %class.MyException2*
call void @__cxa_throw(i8* %9, i8* bitcast ({ i8*, i8* }* @_ZTI12MyException2 to i8*), i8* null) #4
unreachable
; No predecessors!
ret void
}
在foo函数中:
- 先通过__cxa_allocate_exception申请了异常的内存
- 再通过bitcast指令转换为对应的异常类型
- 最后通过__cxa_throw抛出异常,进入unwind library
buz函数如下:
; Function Attrs: noinline optnone
define dso_local void @_Z3buzi(i32) #0 personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
%2 = alloca i32, align 4
%3 = alloca %class.AutoVar, align 1
%4 = alloca i8*
%5 = alloca i32
%6 = alloca %class.MyException2*, align 8
%7 = alloca %class.MyException1*, align 8
store i32 %0, i32* %2, align 4
%8 = load i32, i32* %2, align 4
invoke void @_Z3fooi(i32 %8)
to label %9 unwind label %10
; <label>:9: ; preds = %1
call void @_ZN7AutoVarD2Ev(%class.AutoVar* %3) #4
br label %24
; <label>:10: ; preds = %1
%11 = landingpad { i8*, i32 }
cleanup
catch i8* bitcast ({ i8*, i8* }* @_ZTI12MyException1 to i8*)
catch i8* bitcast ({ i8*, i8* }* @_ZTI12MyException2 to i8*)
%12 = extractvalue { i8*, i32 } %11, 0
store i8* %12, i8** %4, align 8
%13 = extractvalue { i8*, i32 } %11, 1
store i32 %13, i32* %5, align 4
call void @_ZN7AutoVarD2Ev(%class.AutoVar* %3) #4
br label %14
; <label>:14: ; preds = %10
%15 = load i32, i32* %5, align 4
%16 = call i32 @llvm.eh.typeid.for(i8* bitcast ({ i8*, i8* }* @_ZTI12MyException1 to i8*)) #4
%17 = icmp eq i32 %15, %16
br i1 %17, label %18, label %25
; <label>:18: ; preds = %14
%19 = load i8*, i8** %4, align 8
%20 = call i8* @__cxa_begin_catch(i8* %19) #4
%21 = bitcast i8* %20 to %class.MyException1*
store %class.MyException1* %21, %class.MyException1** %7, align 8
%22 = load %class.MyException1*, %class.MyException1** %7, align 8
invoke void @_Z6catch1R12MyException1(%class.MyException1* dereferenceable(1) %22)
to label %23 unwind label %39
; <label>:23: ; preds = %18
call void @__cxa_end_catch()
br label %24
; <label>:24: ; preds = %23, %33, %9
call void @_Z3barv()
ret void
; <label>:25: ; preds = %14
%26 = call i32 @llvm.eh.typeid.for(i8* bitcast ({ i8*, i8* }* @_ZTI12MyException2 to i8*)) #4
%27 = icmp eq i32 %15, %26
br i1 %27, label %28, label %44
; <label>:28: ; preds = %25
%29 = load i8*, i8** %4, align 8
%30 = call i8* @__cxa_begin_catch(i8* %29) #4
%31 = bitcast i8* %30 to %class.MyException2*
store %class.MyException2* %31, %class.MyException2** %6, align 8
%32 = load %class.MyException2*, %class.MyException2** %6, align 8
invoke void @_Z6catch2R12MyException2(%class.MyException2* dereferenceable(1) %32)
to label %33 unwind label %34
; <label>:33: ; preds = %28
call void @__cxa_end_catch()
br label %24
; <label>:34: ; preds = %28
%35 = landingpad { i8*, i32 }
cleanup
%36 = extractvalue { i8*, i32 } %35, 0
store i8* %36, i8** %4, align 8
%37 = extractvalue { i8*, i32 } %35, 1
store i32 %37, i32* %5, align 4
invoke void @__cxa_end_catch()
to label %38 unwind label %49
; <label>:38: ; preds = %34
br label %44
; <label>:39: ; preds = %18
%40 = landingpad { i8*, i32 }
cleanup
%41 = extractvalue { i8*, i32 } %40, 0
store i8* %41, i8** %4, align 8
%42 = extractvalue { i8*, i32 } %40, 1
store i32 %42, i32* %5, align 4
invoke void @__cxa_end_catch()
to label %43 unwind label %49
; <label>:43: ; preds = %39
br label %44
; <label>:44: ; preds = %43, %38, %25
%45 = load i8*, i8** %4, align 8
%46 = load i32, i32* %5, align 4
%47 = insertvalue { i8*, i32 } undef, i8* %45, 0
%48 = insertvalue { i8*, i32 } %47, i32 %46, 1
resume { i8*, i32 } %48
; <label>:49: ; preds = %39, %34
%50 = landingpad { i8*, i32 }
catch i8* null
%51 = extractvalue { i8*, i32 } %50, 0
call void @__clang_call_terminate(i8* %51) #6
unreachable
}
在buz函数中:
通过invoke指令调用foo,正常结束跳转到label9,调用AutoVar类变量v的析构函数,再跳转到label24,调用函数bar,正常退出;若有异常,则跳转到label10。
在label10中,通过landingpad指令获取异常对象的地址,存入变量4%,获取异常对象的typeid,存入变量5%,接着调用AutoVar类变量v的析构函数,完成清理工作,跳转到label14。
在label14中,通过@llvm.eh.typeid.for获取MyException1的typeid,并与捕获到的异常对象的typeid比较,如果相等,则跳转到label18。
在label18中,调用__cxa_begin_catch获取抛出的MyException1的对象指针,再通过invoke调用catch1进行处理,如果正常结束,跳转到label23。
在label23中,调用__cxa_end_catch结束异常处理,跳转到label24,调用函数bar,正常退出。
Exception Handling Instructions
从上文可以看出,llvm通过一系列指令(如landingpad)配合intrinsic(如llvm.eh.typeid.for),完成异常处理。
LandingPadInst(父类:Instruction)
landingpad指令用于说明所在的BasicBlock是一个异常降落垫。
语法
<resultval> = landingpad <resultty> <clause>+
<resultval> = landingpad <resultty> cleanup <clause>*
<clause> := catch <type> <value>
<clause> := filter <array constant type> <array constant>
参数说明:
resultty:Personality Routine设置的参数类型。
resultval:Personality Routine设置的类型为resultty的参数。
cleanup(可选):如果设置了cleanup,标识当前的BasicBlock是用于执行清理动作,通常会被执行;如果没有设置cleanup,标识当前的BasicBlock用于处理异常,既异常类型一定和landingpad的类型匹配。
catch <type> <value>:value为一个全局变量,表示当前landingpad可以捕获的异常类型(如@_ZTI12MyException1)。
filter <type> <value>:value为一个数组,表示当前landingpad可以抛出的异常类型。
语义
landingpad指令定义了Personality Routine设置的参数类型和值,是BasicBlock的第一个非PHI指令,用于提供异常降落垫。
示例
;; A landing pad which can catch an integer.
%res = landingpad { i8*, i32 }
catch i8** @_ZTIi
;; A landing pad that is a cleanup.
%res = landingpad { i8*, i32 }
cleanup
;; A landing pad which can catch an integer and can only throw a double.
%res = landingpad { i8*, i32 }
catch i8** @_ZTIi
filter [1 x i8**] [i8** @_ZTId]
ResumeInst(父类:Instruction)
resume指令用于恢复异常传播。
语法
resume <type> <value>
语义
指令用于恢复被landingpad指令打断的异常传播过程(栈回退)。
示例
resume { i8*, i32 } %exn
CleanupPadInst(父类:FuncletPadInst)
用于WINDOWS运行时异常处理,不做介绍。
CleanupPad(父类:FuncletPadInst)
用于WINDOWS运行时异常处理,不做介绍。
CatchSwitchInst(父类:Instruction)
用于WINDOWS运行时异常处理,不做介绍。
CatchReturnInst(父类:Instruction)
用于WINDOWS运行时异常处理,不做介绍。
CleanupReturnInst(父类:Instruction)
用于WINDOWS运行时异常处理,不做介绍。