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