Skip to content

Commit 7524b7b

Browse files
committed
complete article 07
1 parent de992aa commit 7524b7b

File tree

2 files changed

+142
-5
lines changed

2 files changed

+142
-5
lines changed

LLVM IR入门指南(7)——异常处理.md

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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++
9393
void __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
100100
1. 把一部分信息进一步储存在异常结构体中
101101
2. 调用`_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

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* [LLVM IR入门指南(4)——类型系统](./LLVM%20IR入门指南(4)——类型系统.md)
1111
* [LLVM IR入门指南(5)——控制语句](./LLVM%20IR入门指南(5)——控制语句.md)
1212
* [LLVM IR入门指南(6)——函数](./LLVM%20IR入门指南(6)——函数.md)
13+
* [LLVM IR入门指南(7)——异常处理](./LLVM%20IR入门指南(7)——异常处理.md)
1314

1415
## 在哪可以看到我的文章
1516

0 commit comments

Comments
 (0)