コンパイラの解析 (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
ダウンロード

libgcj4