@@ -72,7 +72,7 @@ clang++ -S -emit-llvm try_catch_test.cpp
7272
7373## 怎么抛
7474
75- 所谓怎么抛,就是如何抛出异常,主要需要保证抛出的异常结构体不会因为stack unwinding而释放,并且能够改变控制流 。
75+ 所谓怎么抛,就是如何抛出异常,主要需要保证抛出的异常结构体不会因为stack unwinding而释放,并且能够正确处理栈 。
7676
7777对于第一点,也就是让异常结构体存活,我们就需要不在栈上分配它。同时,我们也不能直接裸调用` malloc ` 等在堆上分配的方法,因为这个结构体也不需要我们手动释放。C++中采用的方法是运行时提供一个API:` __cxa_allocate_exception ` 。我们可以在` foo ` 函数编译而成的` @_Z3foov ` 中看到:
7878
@@ -87,24 +87,160 @@ define void @_Z3foov() #0 {
8787
8888第一步就使用了` @__cxa_allocate_exception ` 这个函数,为我们异常结构体开辟了内存。
8989
90- 然后就是要处理第二点,也就是正确地改变控制流 。这里的方法是使用另一个C++运行时提供的API:` __cxa_throw ` 。这个API开启了我们的stack unwinding。我们可以在[ libc++abi Specification] ( http://libcxxabi.llvm.org/spec.html ) 中看到这个函数的签名:
90+ 然后就是要处理第二点,也就是正确地处理栈 。这里的方法是使用另一个C++运行时提供的API:` __cxa_throw ` ,这个API同时也兼具了改变控制流的作用 。这个API开启了我们的stack unwinding。我们可以在[ libc++abi Specification] ( http://libcxxabi.llvm.org/spec.html ) 中看到这个函数的签名:
9191
9292``` c++
9393void __cxa_throw (void* thrown_exception, struct std::type_info * tinfo, void (* dest)(void* ));
9494```
9595
9696其第一个参数,是指向我们需要抛出的异常结构体的指针,在LLVM IR的代码中就是我们的`%1`。第二个参数,`std::type_info`如果了解C++底层的人就会知道,这是C++的一个RTTI的结构体。简单来讲,就是存储了异常结构体的类型信息,以便在后面`catch`的时候能够在运行时对比类型信息。第三个参数,则是用于销毁这个异常结构体时的函数指针。
9797
98- 这个函数是如何改变控制流的呢 ?粗略来说,它依次做了以下几件事:
98+ 这个函数是如何处理栈并改变控制流的呢 ?粗略来说,它依次做了以下几件事:
9999
1001001. 把一部分信息进一步储存在异常结构体中
1011012. 调用`_Unwind_RaiseException`进行stack unwinding
102102
103- 也就是说,用来改变控制流的核心 ,就是`_Unwind_RaiseException`这个函数。这个函数也可以在我上面提供的Itanium的ABI指南中找到。
103+ 也就是说,用来处理栈并改变控制流的核心 ,就是`_Unwind_RaiseException`这个函数。这个函数也可以在我上面提供的Itanium的ABI指南中找到。
104104
105105## 怎么接
106106
107- 所谓怎么接,就是当stack unwinding遇到`try`块之后,如何处理相应的异常。
107+ 所谓怎么接,就是当stack unwinding遇到`try`块之后,如何处理相应的异常。根据我们上面提出的要求,怎么接应该处理的是如何改变控制流并且在运行时进行类型匹配。
108+
109+ 首先,我们来看如果`bar`单纯地调用`foo`,而非在`try`块内调用,也就是
110+
111+ ```c++
112+ void bar() {
113+ foo();
114+ }
115+ ```
116+
117+ 编译出的LLVM IR是:
118+
119+ ``` llvm
120+ define void @_Z3barv() #0 {
121+ call void @_Z3foov()
122+ ret void
123+ }
124+ ```
125+
126+ 和我们正常的不会抛出异常的函数的调用形式一样,使用的是` call ` 指令。
127+
128+ 那么,如果我们代码改成之前的例子,也就是
129+
130+ ``` c++
131+ void bar () {
132+ try {
133+ foo ();
134+ } catch (MyError err) {
135+ // do something with err
136+ } catch (AnotherError err) {
137+ // do something with err
138+ } catch (...) {
139+ // do something
140+ }
141+ }
142+ ```
143+
144+ 其编译出的LLVM IR是一个很长很长的函数。其开头是:
145+
146+ ``` llvm
147+ define void @_Z3barv() #0 personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
148+ %1 = alloca i8*
149+ %2 = alloca i32
150+ %3 = alloca %struct.AnotherError, align 1
151+ %4 = alloca %struct.MyError, align 1
152+ invoke void @_Z3foov()
153+ to label %5 unwind label %6
154+ ; ...
155+ }
156+ ```
157+
158+ 我们发现,传统的` call ` 调用变成了一个复杂的` invoke ` .. ` to ` .. ` unwind ` 的指令,这个指令是什么意思呢?
159+
160+ ` invoke ` 指令就是我们改变控制流的另一个关键,我们可以在[ ` invoke ` instruction] ( http://llvm.org/docs/LangRef.html#invoke-instruction ) 中看到其详细的解释。粗略来说,在我们上面编译出的LLVM IR代码中
161+
162+ ``` llvm
163+ invoke void @_Z3foov() to label %5 unwind label %6
164+ ```
165+
166+ 这个代码的意思是:
167+
168+ 1 . 调用` @_Z3foov ` 函数
169+ 2 . 判断函数返回的方式:
170+ * 如果是以`ret`指令正常返回,则跳转至标签`%5`
171+ * 如果是以`resume`指令或者其他异常处理机制返回(如我们上面所说的`__cxa_throw`函数),则跳转至标签`%6`
172+
173+ 所以这个` invoke ` 指令其实和我们之前在跳转中讲到的` phi ` 指令很类似,都是根据之前的控制流来进行之后的跳转的。
174+
175+ 我们的` %5 ` 的标签很简单,因为原来C++代码中,在` try ` .. ` catch ` 块之后啥也没做,就直接返回了,所以其就是简单的
176+
177+ ``` llvm
178+ 5:
179+ br label %18
180+ 18:
181+ ret void
182+ ```
183+
184+ 而我们的` catch ` 的方法,也就是在运行时进行类型匹配的关键,就隐藏在` %6 ` 标签内。
185+
186+ 我们通常称在调用函数之后,用来处理异常的代码块为landing pad,而` %6 ` 标签,就是一个landing pad。我们来看看` %6 ` 标签内是怎样的代码:
187+
188+ ``` llvm
189+ 6:
190+ %7 = landingpad { i8*, i32 }
191+ catch i8* bitcast ({ i8*, i8* }* @_ZTI7MyError to i8*) ; is MyError or its derived class
192+ catch i8* bitcast ({ i8*, i8* }* @_ZTI12AnotherError to i8*) ; is AnotherError or its derived class
193+ catch i8* null ; is other type
194+ %8 = extractvalue { i8*, i32 } %7, 0
195+ store i8* %8, i8** %1, align 8
196+ %9 = extractvalue { i8*, i32 } %7, 1
197+ store i32 %9, i32* %2, align 4
198+ br label %10
199+ 10:
200+ %11 = load i32, i32* %2, align 4
201+ %12 = call i32 @llvm.eh.typeid.for(i8* bitcast ({ i8*, i8* }* @_ZTI7MyError to i8*)) #3
202+ %13 = icmp eq i32 %11, %12 ; compare if is MyError by typeid
203+ br i1 %13, label %14, label %19
204+ 19:
205+ %20 = call i32 @llvm.eh.typeid.for(i8* bitcast ({ i8*, i8* }* @_ZTI12AnotherError to i8*)) #3
206+ %21 = icmp eq i32 %11, %20 ; compare if is Another Error by typeid
207+ br i1 %21, label %22, label %26
208+ ```
209+
210+ 说人话的话,是这样一个步骤:
211+
212+ 1 . ` landingpad ` 将捕获的异常进行类型对比,并返回一个结构体。这个结构体的第一个字段是` i8* ` 类型,指向异常结构体。第二个字段表示其捕获的类型:
213+ * 如果是`MyError`类型或其子类,第二个字段为`MyError`的TypeID
214+ * 如果是`AnotherError`类型或其子类,第二个字段为`AnotherError`的TypeID
215+ * 如果都不是(体现在`catch i8* null`),第二个字段为`null`的TypeID
216+ 2 . 根据获得的TypeID来判断应该进哪个` catch ` 块
217+
218+ 我将上面代码中一些重要的步骤之后都写上了注释。
219+
220+ 我们之前一直纠结的如何在运行时比较类型信息,` landingpad ` 帮我们做好了,其本质还是根据C++的RTTI结构。
221+
222+ 在判断出了类型信息之后,我们会根据TypeID进入不同的标签:
223+
224+ * 如果是` MyError ` 类型或其子类,进入` %14 ` 标签
225+ * 如果是` AnotherError ` 类型或其子类,进入` %22 ` 标签
226+ * 如果都不是,进入` %26 ` 标签
227+
228+ 这些标签内错误处理的框架都很类似,我们来看` %14 ` 标签:
229+
230+ ``` llvm
231+ 14:
232+ %15 = load i8*, i8** %1, align 8
233+ %16 = call i8* @__cxa_begin_catch(i8* %15) #3
234+ %17 = bitcast i8* %16 to %struct.MyError*
235+ call void @__cxa_end_catch()
236+ br label %18
237+ ```
238+
239+ 都是以` @__cxa_begin_catch ` 开始,以` @__cxa_end_catch ` 结束。简单来说,这里就是:
240+
241+ 1 . 从异常结构体中获得抛出的异常对象本身(异常结构体可能还包含其他信息)
242+ 2 . 进行异常处理
243+ 3 . 结束异常处理,回收、释放相应的结构体
108244
109245# 在哪可以看到我的文章
110246
0 commit comments