コンパイラ 2011年11月14日 酒居敬一@A468(sakai.keiichi@kochi-tech.ac.jp) http://www.info.kochi-tech.ac.jp/k1sakai/Lecture/COMP/2011/index.html
例外処理 プログラムの実行中に発生した、例外的な事象を取り扱 うこと。 例外的な事象として、例えば次のようなもの。 例外処理の実装について。 例外的な事象に対処するために、大域的なジャンプにより、 例外処理プログラムに処理を移す。 例外的な事象として、例えば次のようなもの。 スタックのオーバーフロー セグメンテーション違反・nullポインタ例外 0除算 例外処理の実装について。 プログラミング言語 OS プロセッサ
原始的な実装 OSが存在していれば、OSの例外処理機構を利用する。 プロセッサの例外処理機構を利用する。 各プロセスにシグナルとして実装されている。 例外の発生により、対応するシグナルハンドラが呼ばれる。 例外発生によりOSに処理が戻るとともにプロセスは停止し、 シグナルハンドラからプロセスの実行を再開する。 OSの機能なので、システムコールで使える。 プロセッサが検出でき、OSが受けた例外の一部しか対応できない。 プロセッサの例外処理機構を利用する。 例外の発生により、発生源に固有のアドレスへジャンプする。 たいていは、最低限のレジスタはスタックに退避してあるので、 原因を除去できれば復帰できる。 高級言語からの言語仕様の範囲内での利用は不可能。 言語拡張かアセンブリ記述を使うしかない。
例外処理の例(110ページ) class Example { public static void main(String[] args){ try{ method1(args); System.out.println("This Line is not executed."); } catch(Exception e){ // Exception (かその派生クラス)に関する例外ハンドラ System.out.println(e.getMessage()); static void method1(Object o){ synchronized(o){ method2(); staic void method2(){ throw (new Exception("Jump To Handler.")); // 例外発生源
例外処理の実行手順 例外発生により、すぐに大域的ジャンプが発生するのではない。 {20行目で例外が生成され投げられたので、20行目を検査すると try ブロッ クの中にはない。よって、} methdo2() から抜けて呼び出し元に戻る。 {呼び出し元の14行目を検査すると、14行目が try ブロックの中にはない。 ただし、synchronized ブロックの中にあるので、いきなり呼び出し元にに戻 るわけにはいかない。13行目で synchronized ブロックに突入する際に、指 定した変数 o が参照するインスタンスの排他制御権を確保したので、例外 処理とはいえども、確保した排他制御権を synchronized ブロックから抜ける ときに開放するべきである。よって、} 呼び出し元の14行目に着いたら排他 制御権を開放し、続いて呼び出し元に戻る。 {呼び出し元の4行目を検査すると、try ブロックの中にあり、後続のcatch 節は7行目にある。よって、}呼び出し元の4行目に着いたら catch 節のある 7行目にジャンプし、投げられた例外が7行目の catch 節において捕捉すべ きインスタンスか検査する。結果として捕捉すべき例外でることから、8行目 にある例外処理を行う。
例外処理のコンパイル法 コンパイル時に実行できる部分 実行時にしか実行できない部分。 中括弧で囲んだ部分。 try ブロックや synchronized ブロックの中にあるかどうか は、構文解析すればわかる。 コンパイル時にできる検査はあらかじめ済ませておき、実行時に しか実行できないコードを出力する。 実行時にしか実行できない部分。 中括弧の外側。 戻る・捕捉するといった分岐・条件分岐。 プログラマが思うところで、例外処理できる。 インスタンスの実行時の型の検査。 プログラマが例外を自由に設計できる。
例外処理のコード生成 ソースコード中の例外の発生源となりうる場所について、 取るべき動作を決める。 決めた動作に基づいてコードを生成する。 ソースコード中の例外の発生源となりうる場所について、 取るべき動作を決める。 発生源としては throw の他にメソッド呼び出しも考慮する。 呼び出した先から捕捉されなかった例外が伝播してくる。 例外発生場所に基づき、取るべき動作を決定する。 try ブロックの中では、対応する catch の検査・分岐。 synchronized ブロックの中では、排他制御権の開放。 メソッドから呼び出し元に例外処理として戻る。 決めた動作に基づいてコードを生成する。 メソッドから戻ったとき、通常処理か例外処理の区別。 通常通り戻るか、例外を捕捉・伝播する。
2返戻値法による実装 メソッドの返戻値を2つとする方法。 通常処理と例外処理のフローが一時重なることが問題 メソッドの定義で明示した戻り値。 例外発生フラグ。 メソッド呼び出し直後に、フラグを調べ、対応する例外処理を行う。 例外の伝播: メソッドから抜けて呼び出し元に戻る。 例外の捕捉: 対応する catch 節にジャンプする。 通常処理と例外処理のフローが一時重なることが問題 呼び出し直後でフラグでフローを分離する処理コスト。 通常処理フローの中から例外処理を分離する非合理さ。 そもそも、例外処理を通常処理フローに合流しなければ発生しない。
説明用プログラムの共通定義 例外を処理する関数へのポインタを定義。 発生した例外に対応する処理関数への参照を そのポインタに代入することで実装する。 2値返戻法では、メソッド(関数)の実行ごとに検査。 表引き法では、戻り番地から例外処理を引き出す。 struct ExecEnv { // スレッド固有の資源を収める構造体 … void *exception; // 例外発生フラグ。発生した例外への参照 }; #define exceptionOccurred(ee) ((ee)->exception != NULL) #define excpetionThrow(ee, obj) ((ee)->exception = obj) #define exceptionClear(ee) ((ee)->exception = NULL)
void Example8_main(ExecEnv *ee, void *args){ Example8_method1(ee, args); if(exceptionOccured(ee)){ goto CATCH; } println(ee, System.out, "This Line is not executed."); NORMAL_EXIT: return; CATCH: { void *object = ee->exception; if(is_instance_of(object, Exception, ee)){ exceptionClear(ee); println(ee, System.out, getMessage(ee, object)); goto EXCEPION_EXIT; goto NORMAL_EXIT; EXCEPTION_EXIT:
void Example8_method1(ExecEnv *ee, void *o){ EnterSynchronizedBlock(o); if(exceptionOccured(ee)){ goto EXCEPTION_EXIT; } Example_method2(ee); goto SYNCHRONIZED_BLOCK_EXCEPTION_EXIT; println(ee, System.out, "This Line is not executed."); ExitSynchronizedBlock(o); return; SYNCHRONIZED_BLOCK_EXCEPTION_EXIT; EXCEPTION_EXIT: void Example8_method2(ExecEnv *ee){ void *object = newInstance(ee, Exception); if(exceptionOccured(ee)){ goto EXCEPTION_EXIT; } Exception_init(ee, object, "Jump to Handler."); exceptionThrow(ee, object); EXCEPTION_EXIT: return;
表引き法による実装 メソッドからの戻りを例外の場合はreturnで処理しない。 例外発生のときの戻り番地をあらかじめ知る方法を用意。 フラグで分離した先へ goto でもってメソッドから戻る。 例外発生のときの戻り番地をあらかじめ知る方法を用意。 通常の戻り先をキーに、例外処理の分岐先を取り出す。 戻り番地 例外発生時の戻り番地 3 6 4 12 15 21 27 22 25 23 24 33 36 34
呼ばれた関数の スタックフレーム 戻り番地 ee 呼んだ関数の スタックフレーム args プログラム中のどこから来たか? という情報よりも どこへ戻るのか? という情報を使う。 それは、戻り番地は暗黙的に渡されているため、 特別に処理しなくても獲得できる情報だから。 ← TOS 呼ばれた関数の スタックフレーム 呼んだ関数の スタックフレーム 戻り番地 ee args void Example8_main(ExecEnv *ee, void *args){ Example8_method1(ee, args); println(ee, System.out, "This Line is not executed."); NORMAL_EXIT: return; CATCH: { void *object = ee->exception; if(is_instance_of(object, Exception, ee)){ exceptionClear(ee); println(ee, System.out, getMessage(ee, object)); goto NORMAL_EXIT; } EXCEPTION_EXIT: goto (lookUpReturnAddressInException(returnAddress());
void Example8_method1(ExecEnv *ee, void *o){ EnterSynchronizedBlock(o); Example_method2(ee); println(ee, System.out, "This Line is not executed."); ExitSynchronizedBlock(o); return; SYNCHRONIZED_BLOCK_EXCEPTION_EXIT; EXCEPTION_EXIT: goto (lookUpReturnAddressInException(returnAddress()); } void Example8_method2(ExecEnv *ee){ void *object = newInstance(ee, Exception); Exception_init(ee, object, "Jump to Handler."); exceptionThrow(ee, object);
Null Pointer Exception 実行時例外のいくらかはプロセッサが検出できる セグメンテーション違反(IA-32では、一般保護例外) ページ不在例外 0除算例外 バスエラー(IA-32では、一般保護例外) プロセッサが検出できるものについては、検査コードを 生成しないで、シグナルなどを経由してプロセッサから 例外を受け取る。 null pointer は特定のアドレス値に設定してある。 その番地を{コード|データ}セグメントに入れないでおく。 ページ管理表で、その番地を含むページを不在状態にしておく。
ページングとセグメンテーションは組み合わされることが多い [Intel, ``Software Developer Manual’’] ページングとセグメンテーションは組み合わされることが多い