コンパイラの解析 (4) 例外処理
例外処理 通常のコントロールフローではない 言語の実装からは隠蔽されている 大域脱出をおこなう どこからでもジャンプする可能性がある 何か特殊な仕掛けが必要!
例外処理の実装方法 二返戻値法 setjmp法 表引き法 正常値と例外値の両方を返す 通常のコーリングシーケンス+例外検査 例外が発生したら大域脱出 大域脱出のジャンプ先をあらかじめ指定 表引き法 例外が発生したら表を元にジャンプ 正常実行時にほとんどコストが掛からない
サンプルプログラム – Add public static void main(String[] args) { try { int result = add(args[0], args[1]); System.out.println("result = " + result); } catch (NumberFormatException e) { System.out.println(e.getMessage()); System.out.println("unknown result"); } } static int add(String a, String b) { return Integer.parseInt(a) + Integer.parseInt(b); }
二返戻値法における実装 (概要) 関数は常に2つの値を返す 例外情報が存在(=例外が発生)しているか関数呼び出しのたびに調査 通常の関数の戻り値 (正常値) 例外情報 例外情報が存在(=例外が発生)しているか関数呼び出しのたびに調査 通常のコーリングシーケンスを利用できる 関数コールのたびに例外検査を行う
二返戻値法における実装 (throw) フラグと情報を用意してリターンすればよい int parseInt(const char *s) { char *end; int a = strtol(s, &end, 10); if (end[0] != '\0') { sprintf(exc.message, "parse error: %s", s); exc.occurred = 1; return 0; } return a; }
二返戻値法における実装 (throws) 例外を通過させる場合も明示的に 関数呼び出しのたびに行う int add(const char *a, const char *b) { int ia, ib; ia = parseInt(a); if (exc.occurred) return 0; ib = parseInt(b); if (exc.occurred) return 0; return ia + ib; }
二返戻値法における実装 (catch) 例外が発生していた場合にその処理を行う 正常終了時も検査だけは必須! int main(int argc, char **argv) { int result = add(argv[1], argv[2]); if (!exc.occurred) { printf("result = %d\n", result); } else { puts(exc.message); puts("unknown result"); } return 0; }
二返戻値法における実装の特徴 可搬性が高い 高級言語で明示的に例外パスを記述 遅い 正常終了でも例外検査が必要
setjmp法における実装 (概要) setjmp/longjmpを用いて大域脱出 そもそも、setjmpって有名? 例外情報は別に保管される そもそも、setjmpって有名?
setjmp/longjmpとは (1) longjmpを呼ぶとsetjmpへワープ result = 0 begin jump() 08: int main(int argc, char **argv) { 09: int result = setjmp(jmp); 10: printf("result = %d\n", result); 11: if (result == 0) 12: jump(); 13: return 0; 14: } 16: void jump() { 17: puts("begin jump()"); 18: longjmp(jmp, 6502); 19: puts("end jump()"); 20: } main:09 int result = setjmp(jmp); main:10 printf("result = %d\n", ); main:11 if (result == 0) main:12 jump(); jump:17 puts("begin jump()"); jump:18 longjmp(jmp, 6502); main:10 printf("result = %d\n",…); main:11 if (result == 0) main:13 return 0; result = 0 begin jump() result = 6502
setjmp/longjmpとは (2) setjmpの実装 longjmpの実装 一部、volatileでないレジスタは消失 その時点のレジスタを保存する 最初に呼ばれたときには0を返す longjmpの実装 setjmpで保存したレジスタを復帰 PCやスタックフレーム(SP, FP)なども巻き戻す setjmpの結果として引数の値を返す 一部、volatileでないレジスタは消失
setjmp法における実装 (catch) try~の部分でif (setjmp(…) == 0) int main(int argc, char **argv) { if (setjmp(jmp) == 0) { int result = add(argv[1], argv[2]); printf("result = %d\n", result); } else { puts(message); puts("unknown result"); } return 0; }
setjmp法における実装 (throws) longjmpで大域脱出するのでなにもしない スキップする int add(const char *a, const char *b) { return parseInt(a) + parseInt(b); }
setjmp法における実装 (throw) 値だけ準備してlongjmpすればよい 第2引数で例外の番号を指定できる int parseInt(const char *s) { char *end; int a = strtol(s, &end, 10); if (*end != '\0') { sprintf(message, "parse error: %s", s); longjmp(jmp, 1); } return a; }
setjmp法における実装の特徴 可搬性が高い コンパイラが混乱する 遅い 大抵のC言語はsetjmpをサポートしている volatileの指定がない変数は消失するかも コンパイラによってはsetjmpがあると最適化抑止 遅い tryの度にレジスタを退避する
表引き法における実装 (概要) プログラムは次のものを含む 実行可能な部分 (本来のプログラム) 例外表 ------ ------------ ----- ------ ------------ ----- ------ ------------ ----- ------ ------------ ----- ------ ------------ ----- ------ ------------ -----
表引き法に必要な情報 適用範囲 着陸地点 レジスタ情報 その他 どこで発生した例外に対応するか どのアドレスにジャンプするか(catchの位置) レジスタ情報 Spillしたレジスタはどこに格納されているか その他 どの種類の例外をキャッチできるか、など
表引き法における実装 (throw) 例外を発生させ、キャッチするフレームを探す 自分と呼び出し元の表を参照 キャッチするフレームまで巻き戻す ------ catch(…) { ---- } ------ ------------ ----- ------ raise Exception ----- ------ ------------ ----- ------ ------------ ----- ------ ------------ -----
表引き法における実装 (throws) 表に「例外を通過させる」ことを記述する 何も書かないとthrowsになる実装もある リソースの解放が必要になる場合が多いので、通常はリソース解放コードに着地させる Frap From Trap To Type Landing Point try_begin try_end <any> unwind
表引き法における実装 (catch) 表に「例外をキャッチする」ことを記述する キャッチできる型、ハンドラのアドレスを記述 実装によってはキャッチできる型を記述しない Frap From Trap To Type Landing Point try_begin try_end Exception catch1 Error catch2 <any> <unwind>
表引き法における実装の特徴 高速 可搬性が低い 正常処理時にコストが掛からない ライブラリ/アーキテクチャごとに仕様が異なる (通常は)高級言語レベルで記述できない
gcjの例外 基本的には表引き法を使う 言語ごとに別の記法を取る 高度な記述ができる C++/gccも一部同じ機構を利用 スピルされたレジスタの復帰 インライン関数の擬似フレーム記述
gcjの例外情報 LSDA (Language Specific Data Address) トラップ範囲、着地地点、トラップ型のテーブル FDE (Frame Description Entry) 構築されたフレームに関する情報 退避されたレジスタなどが保存されている位置 詳しくは後述 CIE (Common Information Entry) 幾つかのFDEに共通する情報 FDEと同じような記述もできる
LSDAの情報 Header Call Site Table Action Table Trap Type Table キャッチ開始位置, 範囲, 着地地点 使用するAction Tableのエントリ Action Table Trap Type Tableのエントリを解釈する順序 Trap Type Table キャッチする型の情報
LSDAの記述 例外処理のサンプルプログラム.doc 図 11. bridge関数本体 (i386 - #1) 図 18. LSDAの差分 (i386 - #2) 図 24. LSDA (SPARC - #1) 図 30. LSDAの差分 (SPARC - #2)
CIEの情報 ヘッダ 拡張情報 フレーム情報 (CFA) 使用する拡張情報 コードの整列単位 (i386=1) 戻り値の擬似レジスタ番号 (i386=%eip(8)) 拡張情報 フレーム情報 (CFA) 関数開始時のスタックポインタ、リターンアドレス スタックは上方向に伸びていくので不の値になる
CIEの記述 例外処理のサンプルプログラム.doc 図 13. CIE (i386 - #1) 図 25. CIE (SPARC - #1)
FDEの情報 ヘッダ フレーム情報 (CFA) このFDEを使用する関数の範囲 対応するCIEの位置 対応するLSDAの位置 退避されたレジスタなど、全ての情報
FDEの記述 例外処理のサンプルプログラム.doc 図 14. FDE (i386 - #1) 図 26. FDE (SPARC - #2)
CFA (Canonical Frame Address) フレーム内のレジスタの位置を記述する レジスタごとに擬似レジスタ番号が振られ、それらがフレーム内のどこにあるか記述できる 例外が発生したPCごとに細かく指定できる 記述用のインタープリタが内蔵されている
CFAの記述能力 レジスタの位置を記憶するインタープリタ 現在のPCにおける、スピルされたレジスタの退避先を記述できる // 古いフレームポインタを退避した以降ならば advanve_loc4 .LbridgePrologue1 // フレームアドレスの位置は offset(8) def_cfa_offset offset=8 // レジスタebpをoffset(2)へ退避 offset reg=%ebp(5) offset=2 // プロローグ終了後 advanve_loc4 .LbridgeBody // フレームアドレスの位置は レジスタ%ebp内 def_cfa_register reg=%ebp(5)
資料 http://vtable.rat.cis.k.hosei.ac.jp/nakata/ 解析>例外処理 報告資料 libgcjを用いた例外処理に関する報告.doc 例外処理のサンプルプログラム.doc
二返戻値法ブリッジ (1) 表引き法はコンパイラやアーキテクチャに依存するため、実装が困難 それでも高速に実行できるので利用されている 表引き法のコンパイラを二返戻値法に変換する方法を紹介
二返戻値法ブリッジ (2) 常に例外をハンドルして、第二値として返せばよい ただしThread Local Storageを利用すること public Object bridge(Method m, Object obj, Object[] args) { try { return m.invoke(args); } catch (Throwable t) { exc.occurred = t; return null; } }
二返戻値法ブリッジ (3) メソッドを呼び出す場合は必ずブリッジ経由 戻ったら必ず第二値の検査 // 実際にはJavaではこの書き方はできない Object result = bridge( &Hoge.main, null, new String[]{}); if (exc.occurred != null) { // 例外処理 }