こんにちは
突然ですが皆さん、C++書いてますか?
C++は速くて標準ライブラリが充実してて、Cが苦手なメタプログラミングがしやすくて...いいところがたくさんありますね。好きな人も多いと思います!僕は嫌いです。Rustしか勝たん
さて、そんなC++ですが、他の多くの言語に存在している、「例外機構」も当然のように用意しています。次のように書くことができますね。
#include<stdio.h>
struct MyException{};
int main() {
try {
printf("Throw!\n");
throw MyException(); // 例外を投げる
} catch(MyException) { // ここで受け止める
printf("Caught a MyException\n");
}
}
結果は、
Throw!
Caught a MyException
となります。では、C++はどのように例外を実現しているのでしょうか。
例外、といっても実はいくつかの種類があります。Itanium ABIが定義した方法や、Sj/Ljと呼ばれる例外などです。また、OSなどの環境によっても異なってきます。
ここでは、Linux上のgccで使われている例外について解説します。環境はWindowsのWSL1上で、Linux環境は次の通りです。
$ cat /etc/issue
Ubuntu 16.04.6 LTS \n \l
$ uname -i
x86_64
$ gcc --version
gcc (Ubuntu 9.3.0-10ubuntu2~16.04) 9.3.0
ざっくり流れ
実際にどんな関数を実装すればいいのでしょうか。先程のコードをg++ではなくgccでコンパイルしてみます。
$ gcc qiita_exception_workspace.cpp
/tmp/cci042nX.o: In function `main':
qiita_exception_workspace.cpp:(.text+0x19): undefined reference to `__cxa_allocate_exception'
qiita_exception_workspace.cpp:(.text+0x2b): undefined reference to `__cxa_throw'
qiita_exception_workspace.cpp:(.text+0x41): undefined reference to `__cxa_begin_catch'
qiita_exception_workspace.cpp:(.text+0x50): undefined reference to `__cxa_end_catch'
qiita_exception_workspace.cpp:(.text+0x5f): undefined reference to `__cxa_end_catch'
/tmp/cci042nX.o:(.rodata._ZTI11MyException[_ZTI11MyException]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'
/tmp/cci042nX.o:(.eh_frame+0x13): undefined reference to `__gxx_personality_v0'
collect2: error: ld returned 1 exit status
おお。なんか色々出ましたね。どうやらg++はtryやthrow、catchをこれらの関数に置き換えているらしく、それによってリンクエラーが起きています。つまりこれら実装すればいいわけですね。
ではそれらの関数の役割は何なのでしょうか。例外処理の流れを次にまとめました(カッコの中身は関数名です)
- 例外を投げるコードが実行される。
- 例外情報を入れる領域を確保する(__cxa_allocate_exception)
- 例外を投げる(__cxa_throwと_Unwind_RaiseException)
- 投げられた例外の型情報を見て、CFI(Call Frame Info)を読みながらさかのぼっていく(__gxx_personality_v0)
- そこの情報を読み込む(uw_install_context)
この記事で実際に実装してみるのは、
- __cxa_allocate_exception
- __cxa_throw
- __gxx_personality_v0
です。それ以外はlibcに用意されているものを使います。
__cxa_begin_catch, __cxa_end_catchについては完全に理解しきれていないのと、例外の処理自体には必要なのでここでは扱いません。何もしない関数を定義しておけば大丈夫っぽいです。
また、この記事はこのサイトを参考にして作られています。ここでは多くの情報を省略しているため、英語が読める人は読むといいでしょう。読めない人はDeepLを使って読みましょう。結構綺麗に翻訳されます(経験談)
関数の呼び出しを確認してみる
先程の例外を投げるコードをディスアセンブルして内容を確認してみましょう。必要な部分だけ抜き出しました。
mov edi,0x400934
call 4006f0 <puts@plt> ; 文字の表示
mov edi,0x1
call 4006e0 <__cxa_allocate_exception@plt> ; 例外情報の領域確保
mov edx,0x0
mov esi,0x400950
mov rdi,rax
call 400720 <__cxa_throw@plt> ; 例外を投げる
cmp rdx,0x1
je 40086f <main+0x3d>
mov rdi,rax
call 400730 <_Unwind_Resume@plt>
mov rdi,rax
call 4006d0 <__cxa_begin_catch@plt> ; ここからキャッチ節が始まる
mov edi,0x40093b
call 4006f0 <puts@plt>
call 400700 <__cxa_end_catch@plt> ; ここで終わる
このように、確保した領域とその他の情報を引数にして__cxa_throwを呼び出しているのがわかりますね。
関数の役割の当たりをつけたら実際に実装していきましょう。
実装
下準備
最初に、例外情報のやり取りで使われる構造体を定義しましょう。libcの関数を一部使うことを考え、実際と同じメンバを追加しています。
typedef void (*unexpected_handler)(void);
typedef void (*terminate_handler)(void);
struct __cxa_exception {
std::type_info * exceptionType;
void (*exceptionDestructor) (void *);
unexpected_handler unexpectedHandler;
terminate_handler terminateHandler;
__cxa_exception * nextException;
int handlerCount;
int handlerSwitchValue;
const char * actionRecord;
const char * languageSpecificData;
void * catchTemp;
void * adjustedPtr;
_Unwind_Exception unwindHeader;
};
ここではよく使う2つだけ説明します。
- exceptionType: 投げられた型の情報
- languageSpecificData: 言語特有データ(LSDA)へのポインタ
多くの場合、関数を介してアクセスするので気にする必要はありません。
領域確保
__cxa_allocate_exception を実装しましょう。といっても領域を確保するだけなので簡単に終わります。
関数の定義は次のとおりです
void* __cxa_allocate_exception(size_t);
引数は確保する領域の大きさを表しているのですが、ここでは面倒くさいので適当に、例外情報構造体の三倍を確保することにします。
void* __cxa_allocate_exception(size_t thrown_size) {
void* ret = malloc(sizeof(__cxa_exception) * 3);
if(ret == NULL) {
printf("Could not allocate memory\n");
exit(0);
}
return ret;
}
例外スロー
次に __cxa_throw を実装します。この関数の役割はざっくりと2つあります。
- 引数として渡される型情報を記憶する
- _Unwind_RaiseExceptionを呼び出す
関数の定義は次のようになっています。
void __cxa_throw(void* ptr, std::type_info* tinfo, void (*dest)(void*));
引数のptrには、先程確保した領域へのポインタが渡され、tinfoには投げられた型の情報が渡されます。
__cxa_throw はこの情報を記憶して_Unwind_RaiseExceptionを呼び出します。
std::type_info* thrown_type;
void __cxa_throw(
void* ptr,
std::type_info *tinfo,
void (*dest)(void*)) {
thrown_type = tinfo;
_Unwind_RaiseException(&ptr->unwindHeader);
exit(0);
}
パーソナリティ関数を実装する
さて、先程までサクサク進みましたが、ここからが本番です。
__gxx_personality_v0の定義は次のようになっています。各引数の詳細は使うときに説明します。
_Unwind_Reason_Code __gxx_personality_v0(
int version,
_Unwind_Action action,
uint64_t exceptionClass,
_Unwind_Exception *unwind_exception,
_Unwind_Context *context);
この関数は、コンパイル時に生成される**CFI(Call Frame Info)を読みつつ例外が帰る場所(landing pad)**を探すことになります。つまり、例外ハンドラを自作する上で最重要となる関数です。
肝心のCFIですが、これは言語特有データ(Language Specific Data, LSD)の一部で、LSDA(LSD Area)に存在しています。コンパイル時に-Sオプションを付けることで確認できます。以下は出力結果の一部です。
$ gcc raise.cpp -S -O0 -masm=intel -o asm.s
.LLSDA0:
.byte 0xff
.byte 0x3
.uleb128 .LLSDATT0-.LLSDATTD0
.LLSDATTD0:
.byte 0x1
.uleb128 .LLSDACSE0-.LLSDACSB0
.LLSDACSB0:
.uleb128 .LEHB0-.LFB0
.uleb128 .LEHE0-.LEHB0
.uleb128 .L6-.LFB0
.uleb128 0x1
.uleb128 .LEHB1-.LFB0
.uleb128 .LEHE1-.LEHB1
.uleb128 0
.uleb128 0
.uleb128 .LEHB2-.LFB0
.uleb128 .LEHE2-.LEHB2
.uleb128 .L7-.LFB0
.uleb128 0
.uleb128 .LEHB3-.LFB0
.uleb128 .LEHE3-.LEHB3
.uleb128 0
.uleb128 0
.LLSDACSE0:
.byte 0x1
.byte 0
.align 4
.long _ZTI11MyException
この情報は関数の最後にあります。
最初のラベルに .LLSDA0: とある通りこれがLSDです。(最初のLはLocal)
ヘッダとデータの存在がわかりやすいように、LSDの内部構造を書き足してみます。
.LLSDA0: ; LSDA Header
.byte 0xff ; - LSDA encoding
.byte 0x3 ; - Type encoding
.uleb128 .LLSDATT0-.LLSDATTD0 ; - LSDA length
.LLSDATTD0: ; LSDA Call Site Header ------------------------------
.byte 0x1 ; - Call Site encoding |
.uleb128 .LLSDACSE0-.LLSDACSB0 ; - Call Site length |
.LLSDACSB0: ; Call Site----------------- |
; - Call Site 1 | |
.uleb128 .LEHB0-.LFB0 ; -- Start | |
.uleb128 .LEHE0-.LEHB0 ; -- Length | |
.uleb128 .L6-.LFB0 ; -- Landing pad | |
.uleb128 0x1 ; -- Action table index | |
; - Call Site 2 | |
.uleb128 .LEHB1-.LFB0 ; -- Start | |
.uleb128 .LEHE1-.LEHB1 ; -- Length | |
.uleb128 0 ; -- Landing pad | |
.uleb128 0 ; -- Action table index |-Call Site length |
; - Call Site 3 | |
.uleb128 .LEHB2-.LFB0 ; -- Start | |
.uleb128 .LEHE2-.LEHB2 ; -- Length | |-LSDA length
.uleb128 .L7-.LFB0 ; -- Landing pad | |
.uleb128 0 ; -- Action table index | |
; - Call Site 4 | |
.uleb128 .LEHB3-.LFB0 ; -- Start | |
.uleb128 .LEHE3-.LEHB3 ; -- Length | |
.uleb128 0 ; -- Landing pad | |
.uleb128 0 ; -- Action table index | |
.LLSDACSE0: ; Action table ------------- |
.byte 0x1 ; - Type index |
.byte 0 ; - Next index |
.align 4 ; |
.long _ZTI11MyException ; - Type info |
.LLSDATT0: ;---------------------------------------------------------------------------------
.uleb128や.byteは通常の命令とは違い、プログラム上にデータを設置する役割があります。
.byteの場合だと、1バイトのデータを設置し、.longだと、8バイトのデータを設置します。
では**.uleb128**とは何でしょうか。正式には、' unsigned little endian base 128'の略称で、可変長の符号なし整数を表しています。
1byte=8bitの情報のうち、1bitを次の情報の有無、残りの7bitを実際の値の表現に割り当てているので $2^7=128$ が名前にあります。
.sleb128というのもあり、最上位バイトに符号ビットが存在します。.LLSDACSE0: Action table にある情報は本来なら.sleb128と書かれるべきですが、gccのバグか仕様なのか.byteになっています。このせいで死ぬほど詰まった。
この情報を参考に、LSDAを読んでいきます。
可変長整数を読む
.uleb128, .sleb128を読みます。自分で組んでもいいですが、考慮しないといけないことがいくつかありバグの温床となるので、今回は公式の実装を取ってきます。
読みたいアドレスにポインタpと、格納したい値へのポインタvを使って、
p = read_uleb128(p, v);
とすると、「一つ分の可変長配列を読んで進み、vの先に値を入れる」という動作になります。
LSDA Headerを読む
では実際にLSDAを読んでいきます。その前に、LSDAへのポインタを取得しましょう。
__gxx_personality_v0 の引数の中で、context という引数がありました。この中には例外に関数多くの情報があり、LSDAへのポインタもここにあります。次のようにすることで取得できます。
uint8_t* lsda = (uint8_t*)_Unwind_GetLanguageSpecificData(context);
ちなみに、_Unwind_GetLanguageSpecificDataの定義はここにありcontext->lsdaを返すだけになっています。なんでこんなに長くしちゃったんだ...
ヘッダを個別に管理するのは混乱するので、適当な構造体を作り、LSDAへのポインタからデータを読む関数を作ります。
typedef struct {
uint8_t start_encoding;
uint8_t type_encoding;
unsigned long type_table_offset;
void show_header() {
printf("--- LSDA_Header ---\n");
printf(" start_encoding: %x\n", start_encoding);
printf(" type_encodig: %x\n", type_encoding);
printf(" type_table_offset: %lx\n", type_table_offset);
printf("-------------------\n");
}
} LSDA_Header ;
uint8_t* create_LSDA_Header(uint8_t* lsda, LSDA_Header* header) {
header->start_encoding = *lsda++;
header->type_encoding = *lsda++;
lsda = read_uleb128(lsda, &header->type_table_offset);
return lsda;
}
パーソナリティ関数では、
uint8_t* reading_pointer = (uint8_t*)_Unwind_GetLanguageSpecificData(context);
LSDA_Header lsda_header;
reading_pointer = create_LSDA_Header(reading_pointer, &lsda_header);
lsda_header.show_header();
としてみます。実行結果は、
--- LSDA_Header ---
start_encoding: ff
type_encodig: 3
type_table_offset: 19
-------------------
LSDA Headerは
.LLSDA0: ; LSDA Header
.byte 0xff ; - LSDA encoding
.byte 0x3 ; - Type encoding
.uleb128 .LLSDATT0-.LLSDATTD0 ; - LSDA length
でしたので、正常に読めていることがわかります。やったね!
LSDA Call Site Headerを読む
どんどん行きましょう。次はLSDA Call Site Headerを読みます。
typedef struct {
uint8_t encoding;
unsigned long call_site_length;
void show_header() {
printf("--- LSDA Call Site Header ---\n");
printf(" encoding: %x\n", encoding);
printf(" call_site_length: %lx\n", call_site_length);
printf("-----------------------------\n");
}
} LSDA_Call_Site_Header;
uint8_t* create_LSDA_Call_Site_Header(uint8_t* lsda, LSDA_Call_Site_Header* header) {
header->encoding = *lsda++;
lsda = read_uleb128(lsda, &header->call_site_length);
return lsda;
}
パーソナリティ関数は、
LSDA_Call_Site_Header lsda_cs_header;
reading_pointer = create_LSDA_Call_Site_Header(reading_pointer, &lsda_cs_header);
lsda_cs_header.show_header();
実行結果は
--- LSDA Call Site Header ---
encoding: 1
call_site_length: 10
-----------------------------
大丈夫そうですね!
Call Siteを読む。
ここまで来たら練習問題と同じですね。気をつけるべきはすべてuleb128であることでしょうか。次は私の実装です。
typedef struct {
unsigned long start;
unsigned long length;
unsigned long landing_pad;
unsigned long action_index;
void show_call_site() {
printf("--- Call Site ---\n");
printf(" start: %lx\n", start);
printf(" length: %lx\n", length);
printf(" landing_pad: %lx\n", landing_pad);
printf(" action_index: %lx\n", action_index);
printf("-----------------\n");
}
} LSDA_Call_Site;
uint8_t* create_LSDA_Call_Site(uint8_t* lsda, LSDA_Call_Site* header) {
lsda = read_uleb128(lsda, &header->start);
lsda = read_uleb128(lsda, &header->length);
lsda = read_uleb128(lsda, &header->landing_pad);
lsda = read_uleb128(lsda, &header->action_index);
return lsda;
}
パーソナリティ関数は
LSDA_Call_Site cs;
reading_pointer = create_LSDA_Call_Site(reading_pointer, &cs);
cs.show_call_site();
reading_pointer = create_LSDA_Call_Site(reading_pointer, &cs);
cs.show_call_site();
reading_pointer = create_LSDA_Call_Site(reading_pointer, &cs);
cs.show_call_site();
reading_pointer = create_LSDA_Call_Site(reading_pointer, &cs);
cs.show_call_site();
実行結果と、-sの出力を見比べてみます。
--- Call Site ---
start: 20 .LEHB0-.LFB0
length: 5 .LEHE0-.LEHB0
landing_pad: 25 .L6-.LFB0
action_index: 1 0x1
-----------------
--- Call Site ---
start: 2e .LEHB1-.LFB0
length: 5 .LEHE1-.LEHB1
landing_pad: 0 0
action_index: 0 0
-----------------
--- Call Site ---
start: 40 .LEHB2-.LFB0
length: 5 .LEHE2-.LEHB2
landing_pad: 51 .L7-.LFB0
action_index: 0 0
-----------------
--- Call Site ---
start: 5f .LEHB3-.LFB0
length: 5 .LEHE3-.LEHB3
landing_pad: 0 0
action_index: 0 0
-----------------
定数の位置を見る限りあってそうですね。心配な方は、objdumpで出た結果と比較してみると正しいことがわかると思います。
ここでは4つ出力しましたが、本来ならLength分表示すると良さそうですね。Call Siteの長さはCall Site Headerにありました。
それを用いて書き換えましょう
const uint8_t* end_call_site = reading_pointer + lsda_cs_header.call_site_length;
while(reading_pointer < end_call_site) {
LSDA_Call_Site cs;
reading_pointer = create_LSDA_Call_Site(reading_pointer, &cs);
cs.show_call_site();
}
ちなみに、正解の(我々が例外処理で使うべき)Call Siteは最初のものです。
目的のCall Siteを探す
さてヘッダーもデータも読めたので例外をキャッチしたいところですが、ここで少し整理しましょう。
これまでで学んだことは、
- C++コンパイラはthrowやcatchを特定の命令に置換する
- 最終的にパーソナリティ関数が呼び出され、例外から帰る場所や、そのための情報を探す。
- 探す場所はLSDAと呼ばれる場所で、今それを読んでいる。
2つ目の「探す」ためには、どんなものを探すのか明確にする必要があります。
みなさんが想像するのは、「合致する型のあるcatchを探す」ことだと思いますが、それよりも先に「探す」ものがあります。次のコードを見てください。
struct Exception{};
int func() {
try {
throw Exception();
} catch(Exception) {
printf("[First]Caught an exception.\n");
}
try {
throw Exception();
} catch(Exception) {
printf("[Second]Caught an exception.\n");
}
}
最初の方に言いましたが、LSDAは一つの関数につき、一つ存在します。
最初の例外が投げられた時、どのようにして例外をキャッチすればいいのでしょうか。正解はCall Siteの数にあります。
各try文には対応するCall Siteがあり。Call Siteのほうが多いのは、別の情報を保存しておくためで、ここでは省略します。
何が言いたいのかというと、例外が投げられたtry文を探す必要がある、ということです。
一つ前の項目でCall Siteを読むことに成功しました。今度はそれらのメンバについて説明します。
先程、一つのtry文には一つのCall Siteがあると言いました。ということはtry文の情報(場所とか長さとか)はCall Siteにありそうだという予測が立ちます。
Call Siteの定義は
typedef struct {
unsigned long start;
unsigned long length;
unsigned long landing_pad;
unsigned long action_index;
} LSDA_Call_Site;
だったので、どうやらそれであっているようです。しかし、先程表示したときは、
--- Call Site ---
start: 20
length: 5
landing_pad: 25
action_index: 1
-----------------
とあり、startが0x20というのは、アドレス的にはとても小さいです。
これはCall Siteの情報が、関数の先頭からのオフセットになっていることに起因します。つまり、このCall Siteが表すtry文は、関数の先頭から0x20進んだところにあると主張しているのです。確認してみます。
0000000000400842 <main>:
400842: 55 push rbp
400843: 48 89 e5 mov rbp,rsp
400846: 53 push rbx
400847: 48 83 ec 18 sub rsp,0x18
40084b: bf 01 00 00 00 mov edi,0x1
400850: e8 91 00 00 00 call 4008e6 <__cxa_allocate_exception>
400855: ba 00 00 00 00 mov edx,0x0
40085a: be 58 0e 40 00 mov esi,0x400e58
40085f: 48 89 c7 mov rdi,rax
400862: e8 46 00 00 00 call 4008ad <__cxa_throw> ; ここ!!
400867: 48 83 fa 01 cmp rdx,0x1
40086b: 74 08 je 400875 <main+0x33>
40086d: 48 89 c7 mov rdi,rax
ちゃんとthrowする場所を指していて、Lengthの5もcallの命令長となっており、とても良さそうですね!
throwする前になにかした場合は、しっかりとLengthが変化します。
今わかったのは、各try文に対応するCall Siteがあり、Call Siteには関数の先頭からのオフセットとして、開始と長さがある、ということです。
ならば、関数の先頭のアドレス、例外が投げられたアドレス、がわかれば、どのtry文から例外がスローされたのかわかりそうですね。
では例外が投げられた場所と、関数の先頭を取得する方法を紹介します。
それぞれ、_Unwind_GetIPと_Unwind_GetRegionStartという関数にcontextを渡すことで取得できます。使ってみます。
uintptr_t thrown_ip = _Unwind_GetIP(context);
uintptr_t func_start = _Unwind_GetRegionStart(context);
printf("thrown at %lx\n", thrown_ip);
printf("func start at %lx\n", func_start);
実行結果は、
thrown at 4008b7
func start at 400892
となり、ディスアセンブル結果は
0000000000400892 <main>: ; func start
400892: 55 push rbp
400893: 48 89 e5 mov rbp,rsp
400896: 53 push rbx
400897: 48 83 ec 18 sub rsp,0x18
40089b: bf 01 00 00 00 mov edi,0x1
4008a0: e8 91 00 00 00 call 400936 <__cxa_allocate_exception>
4008a5: ba 00 00 00 00 mov edx,0x0
4008aa: be f8 0e 40 00 mov esi,0x400ef8
4008af: 48 89 c7 mov rdi,rax
4008b2: e8 46 00 00 00 call 4008fd <__cxa_throw>
4008b7: 48 83 fa 01 cmp rdx,0x1 ; thrown
4008bb: 74 08 je 4008c5 <main+0x33>
なので、とても良さそうです。
が、一つ注意しないと行けないことがあって、_Unwind_GetIPで帰ってくるアドレスは、例外が投げられた次の命令だと言うことです。
この情報を元に、例外が投げられたCall Siteを探してみます。
uintptr_t thrown_ip = _Unwind_GetIP(context) - 1; // 範囲内としてカウントするため
uintptr_t func_start = _Unwind_GetRegionStart(context);
const uint8_t* end_call_site = reading_pointer + lsda_cs_header.call_site_length; // Call Siteの最後
while(reading_pointer < end_call_site) {
LSDA_Call_Site cs;
reading_pointer = create_LSDA_Call_Site(reading_pointer, &cs); // Call Site 一つ分読む
uintptr_t try_start = func_start + cs.start; // そのtry文の始まる
uintptr_t try_end = try_start + cs.length; // 終わり
if(try_start < thrown_ip && thrown_ip < try_end) { // 投げられたのがその中か
printf("Call site found\n");
printf("start at %lx\n", try_start);
printf("end at %lx\n", try_end);
}
}
このプログラムの動作確認のため、raise.cppを次のように書き換えます。asm("nop"); はただの目印です。
#include<stdio.h>
struct MyException{};
int main() {
try {
asm("nop");
printf("1\n");
} catch(MyException) {
printf("caught\n");
}
try {
asm("nop");
asm("nop");
printf("2\n");
} catch(MyException) {
printf("caught\n");
}
try {
asm("nop");
asm("nop");
asm("nop");
printf("3\n");
throw MyException(); // ここで投げてる!!
} catch(MyException) {
printf("caught\n");
}
try {
asm("nop");
asm("nop");
asm("nop");
asm("nop");
printf("4\n");
} catch(MyException) {
printf("caught\n");
}
}
実行結果は...
Call site found
start at 4008ba
end at 4008db
となり、ディスアセンブル結果は、
400897 sub rsp,0x18
40089b nop ; nopが1つのtry文はここから
40089c mov edi,0x400f84
4008a1 call 400700 <puts@plt>
4008a6 nop
4008a7 nop ; nopが2つのtry文はここから
4008a8 mov edi,0x400f86
4008ad call 400700 <puts@plt>
4008b2 nop
4008b3 nop
4008b4 nop ; nopが3つのtry文はここから
4008b5 mov edi,0x400f88
4008ba call 400700 <puts@plt> ; start
4008bf mov edi,0x1
4008c4 call 400a24 <__cxa_allocate_exception>
4008c9 mov edx,0x0
4008ce mov esi,0x400f98
4008d3 mov rdi,rax
4008d6 call 4009eb <__cxa_throw>
4008db mov eax,0x0 ; end
おお!startとendがちゃんと例外が投げられたtry文を指していますね!!
これで目的のCall Siteを発見することができました!
目的の型を探す
前回まででCall Site、つまり例外がスローされたtry文を見つけることができましたね。
次は型情報を探してみましょう。
Call Siteにはまだ使っていない情報が2つありましたね。landing_padとaction_indexです。型情報に関する情報は action_index です。
action_index はその名の通り、action のインデックスです。actionとは何でしょうか。LSDAの一部を再載します。
.LLSDACSE0: ; Action table ------------- |
.byte 0x1 ; - Type index |
.byte 0 ; - Next index |
.align 4 ; |
.long _ZTI11MyException ; - Type info |
.LLSDATT0: ;---------------------------------------------------------------------------------
.LLSDACSE0 というラベルから始まるのがactionらしいです。うーん。型が一つだけだとわかりづらいですね。増やします。
raise.cppを変えましょう
#include<stdio.h>
struct MyException{};
struct YourException{};
struct HerException{};
struct HisException{};
int main() {
try {
throw MyException();
} catch(YourException) {
printf("Caught YourException\n");
} catch(HerException) {
printf("Caught HerException\n");
} catch(HisException) {
printf("Caught HisException\n");
} catch(MyException) {
printf("Caught MyException\n");
} catch(...) {
printf("Caught Exception\n");
}
}
同じように-Sをつけてコンパイルしてみましょう。LSDAの一部を載せます(gccが正しくないコードを吐くので修正しています)
.uleb128 .LEHB0-.LFB0
.uleb128 .LEHE0-.LEHB0
.uleb128 .L15-.LFB0
.uleb128 0x9
...
.LLSDACSE0:
.byte 0x5
.sleb128 0
.byte 0x4
.sleb128 0x7d
.byte 0x3
.sleb128 0x7d
.byte 0x2
.sleb128 0x7d
.byte 0x1
.sleb128 0x7d
.align 4
.long 0
.long _ZTI11MyException
.long _ZTI12HisException
.long _ZTI12HerException
.long _ZTI13YourException
.LLSDATT0:
Call Siteのaction_indexが変わって、Action tableと型情報が沢山増えましたね。実際の読み方ですが、あまりにも情報が少なかったため、stdlibc++を読んで理解しました。ここの584行目の実装です。これから解説しますが、気になる人は読み込んで見ると楽しいかもしれません。
さて、では読んでいきましょう。
Action tableですが、なんとなく見れば分かる通り、2つの値で1セットになっています。action_indexは、無効なaction_indexであることを示す0と、それ以外の時は実際のインデックス+1が格納されています。
なので、$9-1=8$バイト目の値を見てみましょう。
.uleb128 .LEHB0-.LFB0
.uleb128 .LEHE0-.LEHB0
.uleb128 .L15-.LFB0
.uleb128 0x9
...
.LLSDACSE0:
.byte 0x5
.sleb128 0
.byte 0x4
.sleb128 0x7d
.byte 0x3
.sleb128 0x7d
.byte 0x2
.sleb128 0x7d
.byte 0x1 ; ここ!
.sleb128 0x7d
.align 4
.long 0
.long _ZTI11MyException
.long _ZTI12HisException
.long _ZTI12HerException
.long _ZTI13YourException
.LLSDATT0:
どうやら、1バイト情報:0x1と、謎のsleb128:0x7dがありますね。sleb128は符号付き可変長整数でした、これを整数に直すと-2になります。
先に答えを行ってしまうと、-2 は次の型情報への差分になっています。つまり、$8+(-2)=6$ となり2つ上に移動しますね。これを繰り返すと、
.uleb128 .LEHB0-.LFB0
.uleb128 .LEHE0-.LEHB0
.uleb128 .L15-.LFB0
.uleb128 0x9
...
.LLSDACSE0:
.byte 0x5 ; ここ! 5
.sleb128 0
.byte 0x4 ; ここ! 4
.sleb128 0x7d
.byte 0x3 ; ここ! 3
.sleb128 0x7d
.byte 0x2 ; ここ! 2
.sleb128 0x7d
.byte 0x1 ; ここ! 1
.sleb128 0x7d
.align 4
.long 0 ; ヌルポインタ!
.long _ZTI11MyException
.long _ZTI12HisException
.long _ZTI12HerException
.long _ZTI13YourException
.LLSDATT0:
となります。最後に0があるのでこれ以上は動かず、ここで終了です。
その間に.byteの情報は、0x1→0x2→0x3→0x4→0x5 となります。sleb128の方はインデックスだったので、どうやらこっちが型情報らしいです。
raise.cppを確認すると、
YourException→HerException→HisException→MyException→...
の順で確認してほしいのでこのことを念頭に置くと、LLSDATT0から、逆方向に並んでいるもののインデックスっぽいということがわかります。最後はNULLポインタになっているので、これが ... に該当するということも推測できますね。
.LLSDATT0はLSDAの終わりなので、(LSDAの先頭アドレス) + sizeof(LSDA_Header) + lsda_header.length でわかりますね。わからないという人は、結構上に載せたLSDAの図を見直すと理解しやすいと思います。
今わかったことをまとめます。
- Call Siteのaction_indexはAction tableの最初のアドレスを指している。
- .byteの情報は型へのインデックス+1を、.sleb128は次のAction tableへのindexを表している。
- 型情報は、LSDAの最後から逆順にインデックスされている。
必要な情報は手に入りましたね。では実装していきましょう。
まずsleb128を読んで型インデックスを読むことから始めます。
いままで、reading_pointerをずらしながら実装してきたため、LSDAの最後へのポインタを取るのも順番が大切になっています。一度現在のパーソナリティ関数をすべて載せます。
_Unwind_Reason_Code __gxx_personality_v0(
int version,
_Unwind_Action action,
uint64_t exceptionClass,
_Unwind_Exception *unwind_exception,
_Unwind_Context *context) {
uint8_t* reading_pointer = (uint8_t*)_Unwind_GetLanguageSpecificData(context);
LSDA_Header lsda_header;
// LSDAのヘッダー
reading_pointer = create_LSDA_Header(reading_pointer, &lsda_header);
lsda_header.show_header();
uint8_t* end_of_lsda = reading_pointer + lsda_header.type_table_offset;
// LSDAの最後。ここから型情報が逆順に並んでいる。
LSDA_Call_Site_Header lsda_cs_header;
// Call Siteのヘッダー
reading_pointer = create_LSDA_Call_Site_Header(reading_pointer, &lsda_cs_header);
lsda_cs_header.show_header();
uint8_t* action_table_start = reading_pointer + lsda_cs_header.call_site_length;
// Call Siteの最後。つまりAction tableのはじめ
uintptr_t thrown_ip = _Unwind_GetIP(context) - 1; // 範囲内としてカウントするため
uintptr_t func_start = _Unwind_GetRegionStart(context);
const uint8_t* end_call_site = reading_pointer + lsda_cs_header.call_site_length;
bool found_flag = false; // 合致するCall Siteが存在したか。
LSDA_Call_Site now_call_site; // 合致したときのCall Site
while(reading_pointer < end_call_site) {
LSDA_Call_Site cs;
reading_pointer = create_LSDA_Call_Site(reading_pointer, &cs);
uintptr_t try_start = func_start + cs.start;
uintptr_t try_end = try_start + cs.length;
if(try_start < thrown_ip && thrown_ip < try_end) { // 該当するtry文なのか調べる
printf("Call site found\n");
now_call_site = cs;
found_flag = true;
break;
}
}
if(!found_flag) {
printf("Error occured. not found call site\n");
exit(0);
}
uint8_t* action_pointer = action_table_start + now_call_site.action_index - 1; // 初期位置
// action_index は実際のインデックス+1だから1引く
while(1) {
uint8_t type_index = *action_pointer++; // 型のインデックス
printf("type index is %d\n", type_index);
unsigned long next_action_index; // 次のインデックス
read_sleb128(action_pointer, &next_action_index);
if(next_action_index == 0) { // 最後なら
break;
}
action_pointer += next_action_index;
}
}
実行結果は
Call site found
type index is 1
type index is 2
type index is 3
type index is 4
type index is 5
となり、いい感じに取れているのがわかります。続いて型情報を取得します。
型情報
例外で使う型情報を扱う前に、そもそも型情報とは何なのか説明します。
C++にはデフォルトで、実行時型情報、RTTI(RunTime Type Info)というのがあります。その名の通り、コンパイル時ではなく、実行時に型情報を扱う事がでるものです。
次のコードを見てください。
#include<stdio.h>
#include<typeinfo>
struct MyStruct{};
int main() {
printf("%s\n", typeid(MyStruct).name());
}
このプログラムの実行結果は、
8MyStruct
となります。"8"というマングリングのための情報を追加されていますが、しっかりと名前が取得できています。
typeid(型もしくはそのインスタンス)とすると、その型情報が手に入り、std::type_infoというクラスのインスタンスが返ってきます。その中のnameというメンバ関数によって型の名前を取得できるんですね。リファレンスはここにあります
では次のようにしたらどうなるでしょうか。
#include<stdio.h>
#include<typeinfo>
struct MyStruct{};
int main() {
std::type_info info = typeid(MyStruct);
}
qiita_rtti_test.cpp: In function ‘int main()’:
qiita_rtti_test.cpp:7:40: error: ‘std::type_info::type_info(const std::type_info&)’ is private within this context
7 | std::type_info info = typeid(MyStruct);
| ^
In file included from qiita_rtti_test.cpp:2:
/usr/include/c++/9/typeinfo:178:5: note: declared private here
178 | type_info(const type_info&);
| ^~~~~~~~~
うーん。どうやらダメみたいです。では参照を取るのはどうでしょうか。
#include<stdio.h>
#include<typeinfo>
struct MyStruct{};
int main() {
const std::type_info* info_ref = &typeid(MyStruct);
printf("info_ret at %p\n", info_ref);
}
info_ret at 0x4006c8
おお。なんかでましたね。それにしても0x4006c8って何でしょうか。ローカル変数でもないし...
ということで-Sで出力して確認してみます。
.LC0:
.string "info_ret at %p\n"
.text
.globl main
.type main, @function
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], OFFSET FLAT:_ZTI8MyStruct ; 重要なのはここ!
mov rax, QWORD PTR [rbp-8]
mov rsi, rax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
leave
ret
このように、typeid演算子で帰ってくるインスタンスは実行時にあたらしく取得して作っているのではなく、プログラムにもともと乗っかているのです。_ZTI8MyStructを見てみます。
_ZTI8MyStruct:
.quad _ZTVN10__cxxabiv117__class_type_infoE+16
.quad _ZTS8MyStruct
.weak _ZTS8MyStruct
.section .rodata._ZTS8MyStruct,"aG",@progbits,_ZTS8MyStruct,comdat
.align 8
.type _ZTS8MyStruct, @object
.size _ZTS8MyStruct, 10
_ZTS8MyStruct:
.string "8MyStruct"
.text
同じような名前のデータが二つありますね。長いので_ZTI8MyStructをI、_ZTS8MyStructをSと呼ぶことにします。
typeid はIを返すのに、名前情報はSにあるので、nameというメンバ関数はIから+4か+8とかしてSの位置を知ってそうです。確認してみます。
先ほどのプログラムを少し変えて...
int main() {
const std::type_info* info_ref = &typeid(MyStruct);
printf("info_ret name is %s\n", info_ref->name());
}
としてobjdumpしてみると、
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], OFFSET FLAT:_ZTI8MyStruct
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call _ZNKSt9type_info4nameEv ; これがnameメンバ関数。std::type_infoのポインタはrdiに渡される
mov rsi, rax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
leave
ret
_ZNKSt9type_info4nameEv:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi ; 渡されたポインタ
mov rax, QWORD PTR [rbp-8]
mov rax, QWORD PTR [rax+8] ; 渡されたポインタ+8にアクセスしてる
movzx eax, BYTE PTR [rax]
cmp al, 42 ; 一文字目が * かどうか比較してる
jne .L2
mov rax, QWORD PTR [rbp-8]
mov rax, QWORD PTR [rax+8]
add rax, 1
jmp .L4
.L2:
mov rax, QWORD PTR [rbp-8]
mov rax, QWORD PTR [rax+8]
.L4:
pop rbp
ret
やっぱり+8にアクセスしてるみたいですね。std::type_infoの実装はtypeinfoにあり、
const char* name() const _GLIBCXX_NOEXCEPT {
return __name[0] == '*' ? __name + 1 : __name;
}
となっているのでこれも正しそうです。
最後に、ひとつ前に確認したAction tableと型情報をもう一度載せます。
.LLSDACSE0:
.byte 0x5
.sleb128 0
.byte 0x4
.sleb128 0x7d
.byte 0x3
.sleb128 0x7d
.byte 0x2
.sleb128 0x7d
.byte 0x1
.sleb128 0x7d
.align 4
.long 0
.long _ZTI11MyException
.long _ZTI12HisException
.long _ZTI12HerException
.long _ZTI13YourException
.LLSDATT0:
typeidについて知った今見ると、型情報のところがtypeidで帰ってくる値と同じことがわかりますね!
つまり、ここに順番にアクセスして、それをstd::type_info*と解釈すれば、nameメンバ関数で名前が取得できそうです。
catch文にある型の名前
とりあえず次のようなコードを書いてみます。
while(1) {
uint8_t type_index = *action_pointer++; // 型のインデックス
printf("type index is %d\n", type_index);
std::type_info* catch_type = *((std::type_info**)end_of_lsda - type_index); // 新しく追加したところ
printf("type name is %p\n", catch_type);
unsigned long next_action_index; // 次のインデックス
read_sleb128(action_pointer, &next_action_index);
if(next_action_index == 0) { // 最後なら
break;
}
action_pointer += next_action_index; // 移動
}
しかし、これだとSegmentation fault が出力されてしまいます。どうしてでしょうか。この環境ではAction tableのスタートは0x40164bだったので、gdbを用いて表示してみます。
(gdb) x /16wx 0x40164b
0x40164b: 0x7d040005 0x7d027d03 0x00007d01 0x00000000
0x40165b: 0x4010f000 0x40108800 0x4010a800 0x4010c800
0x40166b: 0x00000000 0x00000000 0x00000000 0x00000000
0x40167b: 0x00000000 0x00000000 0x00000000 0x00000000
出力の一行目の最後のブロックが0になっていて、その次からアドレスっぽいもの(typeidへの参照で出てきたような値)になっているので、型情報はちゃんとあるみたいです。
このブロック一つは32bit整数型なので、どうやら型情報は32bitのアドレスで管理されているみたいですね。.longとあるのにおかしぃなぁと思い確認したところ、このドキュメントの59ページに、.longは.intと同じという旨の記述を見つけました。わかりずらいですね。
とにかく、サイズが分かったので4バイトずらしつつアクセスをしてみます。
std::type_info* catch_type = (std::type_info*)(uintptr_t)*((unsigned int*)end_of_lsda - type_index);
printf("type name is %s\n", catch_type->name());
型のキャストがややこしいですね。まずend_of_lsdaをunsigned intにキャストすることで、ポインタ演算で4バイトずれるようにします。
type_indexを引いて、intのまま参照外しをすると、アドレスが返ってきます。そのままstd::type_info*にキャストすると警告がでるため、一度uintptr_tを介してキャストしています。つぎはその実行結果です。
type index is 1
type name is 13YourException
type index is 2
type name is 12HerException
type index is 3
type name is 12HisException
type index is 4
type name is 11MyException
type index is 5
Segmentation fault (core dumped)
やりました!型の名前までとれましたね!最後にSegmentation faultが出ているのはcatch(...)がNULLポインタを設置するからです。NULLだった場合で処理を分けてやりましょう。
if(catch_type == NULL) {
printf(" catch(...) detect\n");
} else {
printf("type name is %s\n", catch_type->name());
}
type index is 1
type name is 13YourException
type index is 2
type name is 12HerException
type index is 3
type name is 12HisException
type index is 4
type name is 11MyException
type index is 5
catch(...) detect
完璧ですね。
投げられた型
catch文の中にある型はわかりました。しかし、肝心の投げた型の方はどこにあるのでしょうか。__cxa_throw を思い出してください。
std::type_info* thrown_type;
void __cxa_throw(
void* ptr,
std::type_info *tinfo,
void (*dest)(void*)) {
thrown_type = tinfo;
_Unwind_RaiseException(&ptr->unwindHeader);
exit(0);
}
tinfo には投げられた型が入っていて、グローバル変数 thrown_type として保存したんですね。
念の為確認しておきましょう。
printf("thrown type is %s\n", thrown_type->name());
thrown type is 11MyException
投げた型はMyExceptionなので、うまく型情報が取得できていますね。
フェーズ
Search フェーズ
さて、あと一歩で例外ハンドラを自作できそうですが、ここで一度フェーズを理解する必要があります。
といってもそんなに難しいものではありません。ちゃちゃっと理解してしまいましょう。
パーソナリティ関数に引数はまだ一回も使っていない引数がありましたね。_Unwind_Action型のactionもその一つです。
ここの110行目から定義があります。
typedef int _Unwind_Action;
#define _UA_SEARCH_PHASE 1
#define _UA_CLEANUP_PHASE 2
#define _UA_HANDLER_FRAME 4
#define _UA_FORCE_UNWIND 8
#define _UA_END_OF_STACK 16
いくつかのフェーズがあり、ビットごとに違う値が割り当てられているようです。
これだけではさすがにわかりませんね。パーソナリティ関数を呼んでいる部分を読んでみましょう。
_Unwind_RaiseException の118,119行目で呼ばれています。どうやら_UA_SEARCH_PHASEが渡されているようです。確認してみましょう。
printf("parsonality function called\n");
if(action & _UA_SEARCH_PHASE) {
printf("search phase\n");
}
return _URC_NO_REASON;
と関数の先頭に書いてみます。返り値については次回説明します。すると、
personality function called
search phase
と出力されました。予想通りですね!
返り値
二つ目のフェーズを説明する前にパーソナリティ関数の返り値についても触れておきましょう。関数定義は
_Unwind_Reason_Code __gxx_personality_v0(
int version,
_Unwind_Action action,
uint64_t exceptionClass,
_Unwind_Exception *unwind_exception,
_Unwind_Context *context);
でした。返り値の型の _Unwind_Reason_Codeはここの51行目で定義されています。
typedef enum {
_URC_NO_REASON = 0,
_URC_FOREIGN_EXCEPTION_CAUGHT = 1,
_URC_FATAL_PHASE2_ERROR = 2,
_URC_FATAL_PHASE1_ERROR = 3,
_URC_NORMAL_STOP = 4,
_URC_END_OF_STACK = 5,
_URC_HANDLER_FOUND = 6,
_URC_INSTALL_CONTEXT = 7,
_URC_CONTINUE_UNWIND = 8
} _Unwind_Reason_Code;
名前から察するに、パーソナリティ関数による探索の結果と返せばいいらしいです。よく使うものだけ紹介します。
- _URC_HANDLER_FOUND
- Search フェーズにおいて、適切なキャッチが見つかった。
- _URC_INSTALL_CONTEXT
- 飛んでもいい例外情報をセットしたので、そこに飛んでほしい(つまり例外のキャッチ)。
- _URC_CONTINUE_UNWIND
- このスタックには適切な例外ハンドラがなかったから一個上にさかのぼってほしい。
今まで何も返しておらず、ずっと警告が出ていました。先程のコードの返り値を _URC_HANDLER_FOUND にしてみましょう。
return _URC_HANDLER_FOUND;
personality function called
search phase
personality function called
(。´・ω・)ん?パーソナリティ関数が2回呼ばれましたね...。でもSearch フェーズではない。_Unwind_RaiseExceptionをもう少し読んでみましょう。136行目です。
code = _Unwind_RaiseException_Phase2 (exc, &cur_context, &frames);
if (code != _URC_INSTALL_CONTEXT)
return code;
むむ。_Unwind_RaiseException_Phase2なるものが呼ばれているようです。内容を見てみると64行目で、
code = (*fs.personality) (1, _UA_CLEANUP_PHASE | match_handler,
exc->exception_class, exc, context);
今度は _UA_CLEANUP_PHASE を渡していますね。ちょっと確認してみます。
printf("personality function called\n");
if(action & _UA_SEARCH_PHASE) {
printf("search phase\n");
}
if(action & _UA_CLEANUP_PHASE) {
printf("cleanup phase\n");
}
return _URC_HANDLER_FOUND;
search phase
cleanup phase
search phase
cleanup phase
どうやら、Search フェーズとCleanUp フェーズが交互に呼ばれているみたいですね。
では、これらの値はどのようにして使われているのでしょうか。
libc++のコードや挙動を見る限り、全体の流れは次の様になっているみたいです。
- _UA_SEARCH_PHASE が渡されたら
- いい感じ感じの catch文を探す
- 見つかったら保存。_URC_HANDLER_FOUNDを返す
- 見つからなかったら更にスタックを巻き戻ってもらう。_URC_CONTINUE_UNWINDを返す
- いい感じ感じの catch文を探す
- _UA_CLEAUP_PHASE が渡されたら
- その前にSearch フェーズで値が保存されているはずなのでそれを読み込む。
- 値をセットする。
- _URC_INSTALL_CONTEXTを返す。
公式の実装はこれ以外にもいろいろな事を気にしていますが、まぁ骨組みはこんなもんです。関数の引数の方のactionは、パーソナリティ関数にどんな事をしてほしいか使えているわけですね。
最後まであと少しです!頑張りましょう!
パーソナリティ関数を完成させる
さて、前回までで型を見つけたり、パーソナリティ関数の呼び出され方を学んだりしました。
では、実際に例外から戻るのには何が必要なのでしょうか。公式の実装を覗いてみます。721行目からです。一部マクロを展開したりしてます。
_Unwind_SetGR (context, 0, __builtin_extend_pointer (ue_header));
_Unwind_SetGR (context, 1, handler_switch_value);
_Unwind_SetIP (context, landing_pad);
return _URC_INSTALL_CONTEXT;
どうやら、_Unwind_SetGRという関数でcontextに設定をしているようです。第二引数が設定する項目を選択していて、第三引数が設定値です。
ue_header とはパーソナリティ関数の第四引数として渡されるものです。私達の作っている関数では、 unwind_exception という名前のものです。
handler_switch_value は何でしょうか。これは一つのtry文で、どのcatchに飛べばいいのかを示しています。つまり、Action tableの.byteに置いてあり、LSDAの最後から逆順に並んでいる情報にアクセスするときに使った、あのインデックスです。
landing_pad はCall Siteにありましたね。そう。Call Siteの使われていない最後の情報です。しかしCall Siteは関数の最初からのオフセットになっているので、func_startを足した値をセットするのを忘れないようにしましょう。
今学んだ事をまとめましょう。あと少しです!
- パーソナリティ関数はSearch フェーズとCleanUp フェーズの2つがある。
- Search フェーズで型情報を探し、合致する物があれば次の情報を記憶する。
- 型情報へのインデックス
- そのCall SiteのLanding pad
- CleanUp フェーズでは contextに_Unwind_Set--という関数に保存した値をセットする。
最後にこれを実装していきましょう!
まず、合致する型があるかどうか調べます。C++では、継承元のクラスでもキャッチすることができますが、すこし難しいので今回は完全に一致するかどうか調べます。わかりやすいように追加した部分に '+' とつけています。
+int handler_type_index;
+_Unwind_Ptr handler_landing_pad;
...
while(1) {
uint8_t type_index = *action_pointer++; // 型のインデックス
std::type_info* catch_type =
(std::type_info*)(uintptr_t)*((unsigned int*)end_of_lsda - type_index);
if(catch_type == NULL) { // catch(...) なら保存して戻る
+ handler_type_index = type_index;
+ handler_landing_pad = func_start + now_call_site.landing_pad;
return _URC_HANDLER_FOUND;
} else {
+ if(strcmp(catch_type->name(), thrown_type->name()) == 0) { // 型の名前が一致したら
+ handler_type_index = type_index;
+ handler_landing_pad = func_start + now_call_site.landing_pad;
return _URC_HANDLER_FOUND;
}
}
unsigned long next_action_index; // 次のインデックス
read_sleb128(action_pointer, &next_action_index);
if(next_action_index == 0) { // 最後なら
break;
}
action_pointer += next_action_index;
}
// 何も見つからなかったら
return _URC_CONTINUE_UNWIND; // スタックを更に巻き戻る
注意点は、strcmp は文字列一致で 0 が帰ってくることですね。
続いてCleanUp フェーズの実装をします。
if(action & _UA_CLEANUP_PHASE) {
printf("cleanup phase\n");
printf("exception %p\n", unwind_exception);
printf("hander %x\n", handler_type_index);
printf("landing pad %lx\n", handler_landing_pad);
_Unwind_SetGR(context, __builtin_eh_return_data_regno(0), (uintptr_t)unwind_exception);
_Unwind_SetGR(context, __builtin_eh_return_data_regno(1), (uintptr_t)handler_type_index);
_Unwind_SetIP(context, handler_landing_pad);
return _URC_INSTALL_CONTEXT;
}
うまくセットできていますね。
try {
throw MyException();
} catch(YourException) {
printf("Caught YourException\n");
} catch(HerException) {
printf("Caught HerException\n");
} catch(HisException) {
printf("Caught HisException\n");
} catch(MyException) {
printf("Caught MyException\n");
} catch(...) {
printf("Caught Exception\n");
}
raise.cpp はこんな感じだったので、Caught MyException と表示されてほしいですね。では実行します。
cleanup phase
exception 0x161e0d0
hander 4
landing pad 4009b1
Caught MyException
おおおお!!ついに取れました!!!!
raise.cppで投げる型を変えて見てもうまくいくのがわかると思います。新しい型を作って投げたらcatch(...) に飛ぶのをわかりますね。
更に!次のように、
void sub() {
try {
throw MyException();
} catch(YourException) {
printf("Caught YourException\n");
}
}
int main() {
try {
sub();
} catch(MyException) {
printf("Caguth MyException\n");
}
}
関数が違う例外をちゃんとキャッチできます。色々な例外を試してみてください。
まとめ
学んだ事をまとめます。
- C++ コンパイラは、tryやcatchを特定の命令に置き換える。
- 内部でパーソナリティ関数というのが呼ばれ、いくつかのフェーズがある。
- Search フェーズ: キャッチしてくれるcatch節を探す。
- LSDAと呼ばれるデータが渡されるので、いくつかのヘッダーを読みつつCall Site と Action tableを取得する。
- Call Siteは1つのtry文に1つは存在しており、開始地点や長さ、Action tableへの初期インデックスが入っている。
- Action table を読みつつ、LSDAの最後から逆順に並んでいる32bitアドレスを取得する。
- 投げられた型を見て、キャッチできるなら、Action table へのインデックスと、Landing padを保存する。
- CleanUp フェーズ: 飛べる場所を設定する。
- Search フェーズが先に呼ばれるので、その情報を、_Unwind_Set--という関数でcontextに設定する。
- _URC_INSTALL_CONTEXT を返す。
- Search フェーズ: キャッチしてくれるcatch節を探す。
- 例外がキャッチされる!~(=^・ω・^)ノ☆
「C++の例外は、実際に例外が投げられた時だけコストが発生するよ」とよく言われますが、実際に実装してみてそれがよくわかったと思います。
終わりに
随分長くなってしまいましたが、これにて「とりあえず動く例外ハンドラ」は作れました... お疲れさまです。
しかし、これには色々なバグや、対応できていない部分がまだまだたくさんあります。また、一部の関数はどうしてもlibcに頼ってしまっているので、それをすべて手書きのコードに置き換える、というのも面白いかもしれません。
解説していない部分も多いので、一度コードのすべてをGithubのリポジトリに貼っておきます。
途中で言いましたが、公式の実装を見つつ、わからないところがアレば、この解説サイトを見ると実装の助けになるかもしれません。
最後になりますが、駄文に長い間お付き合いいただきありがとうございました!
間違いや、誤字脱字等ありましたら、Twitter@Iwancof_ptrまでご報告ください。
感想や質問なども受け付けております。
それでは。お疲れさまでした。