在这篇文章中,我主要介绍的是LLVM IR中的异常处理的方法。主要的参考文献是Exception Handling in LLVM。
异常处理的要求
异常处理在许多高级语言中都是很常见的,在诸多语言的异常处理的方法中,try
… catch
块的方法是最多的。对于用返回值来做异常处理的语言(如C、Rust、Go等)来说,可以直接在高级语言层面完成所有的事情,但是,如果使用try
… catch
,就必须在语言的底层也做一些处理,而LLVM的异常处理则就是针对这种情况来做的。
首先,我们来看一看一个典型的使用try
… catch
来做的异常处理应该满足怎样的要求。C++就是一个典型的使用try
… catch
来做异常处理的语言,我们就来看看它的异常处理的语法:
// try_catch_test.cpp
struct SomeOtherStruct { };
struct AnotherError { };
struct MyError { /* ... */ };
void foo() {
SomeOtherStruct other_struct;
throw MyError();
return;
}
void bar() {
try {
foo();
} catch (MyError err) {
// do something with err
} catch (AnotherError err) {
// do something with err
} catch (...) {
// do something
}
}
int main() {
return 0;
}
这是一串典型的异常处理的代码。我们来看看C++中的异常处理是怎样一个过程(可以参考throw expression和try-block):
当遇到throw
语句的时候,控制流会沿着函数调用栈一直向上寻找,直到找到一个try
块。然后将抛出的异常与catch
相比较,看看是否被捕获。如果异常没有被捕获,则继续沿着栈向上寻找,直到最终能被捕获,或者整个程序调用std::terminate
结束。
按照我们上面的例子,控制流在执行bar
的时候,首先执行foo
,然后分配了一个局部变量other_struct
,接着遇到了一个throw
语句,便向上寻找,在foo
函数内部没有找到try
块,就去调用foo
的bar
函数里面寻找,发现有try
块,然后通过对比进入了第一个catch
块,顺利处理了异常。
这一过程叫stack unwinding,其中有许多细节需要我们注意。
第一,是在控制流沿着函数调用栈向上寻找的时候,会调用所有遇到的自动变量(大部分时候就是函数的局部变量)的析构函数。也就是说,在我们上面的例子里&#