Plan 9 ソースコードでの例外機構 実装編
前に取り上げた「Plan 9 ソースコードでの例外機構 使い方編」の続きです.こんどは実装について.
Plan 9のプロセスでは例外をサポートするために, それを表すProc構造体に特殊なメンバを導入しています.以下に挙げるnerrlabとerrlabがそれにあたります. これらはそれぞれ, 登録されたラベルの個数, 配列です.
/sys/src/9/port/portdat.h : 長いので抜粋 struct Proc { ... int nerrlab; Label errlab[NERR]; ... }
ここでいうラベルとはエラー時に戻るべき, waserror()関数が呼び出された場所を表します.具体的には以下のようなLabel構造体を用いてこれを保持しています.それぞれスタックポインタとリターンアドレスを格納するためのメンバがあります.
/sys/src/9/pc/dat.h typedef struct Label Label; struct Label { ulong sp; ulong pc; };
ここまでくればなんとなく, やっていることが分かるかと思います. waserror()呼び出し時にそのときのスタックポインタやリターンアドレスを登録しておき, error()を呼び出したらその値を読み出してレジスタorスタックに書き戻してやればerror()→waserror()へのジャンプが可能になります.
これはもちろん単発での例外を想定してProc構造体にいっこだけ保持するようにしてもできそうです.しかし, Plan 9では先の例で見たようなnexterror()関数を使った連続逆行を可能にしています.このために, errlabメンバでLabel構造体を配列として保持, 利用中の個数をnerrlabメンバで管理することで, ラベルのスタックのように使っています. なお, NERR=64と決まっています*1.
waserror()関数
waserror()関数から見ていきましょう. 実際にはこれは以下のようなマクロになっています. nerrlabメンバで個数を管理しつつ, errlabメンバにあたる配列中のLabel構造体を利用していきます
/sys/src/9/port/fns.h #define waserror() (up->nerrlab++, setlabel(&up->errlab[up->nerrlab-1]))
setlabel()関数
setlabel関数はアセンブリ言語で書かれており, 以下のような実装になっています.引数としては先ほどみたようなLabel構造体が渡されます. ここではこの時点でのSP(=スタックポインタアドレス)とリターンアドレスを取得し, Label構造体の所定の位置にセットしています. なお, "waserror()関数は通常0が返値"となっているためここでは0を返しています.
/sys/src/9/pc/l.s TEXT setlabel(SB), $0 MOVL label+0(FP), AX MOVL SP, 0(AX) /* store sp */ MOVL 0(SP), BX /* store return pc */ MOVL BX, 4(AX) MOVL $0, AX /* return 0 */ RET
error()関数
error()関数は例外を発生させる関数でした. この関数ではエラーメッセージを現在のプロセス(up)のerrstrメンバにコピーしてnexterror()関数を呼び出しています. spllo()関数はちなみにsti命令を呼ぶような関数. これと途中のsetlabelは何の為にしてるんでしょうね?
void error(char *err) { spllo(); assert(up->nerrlab < NERR); kstrcpy(up->errstr, err, ERRMAX); setlabel(&up->errlab[NERR-1]); nexterror(); }
nexterror()関数
nexterror()関数では. 実際に直前のwaserror()関数が呼び出された場所へのジャンプを可能にしています.先ほどのwaserror()関数ではsetlabel()関数を呼んでいました. これに対してここでは逆操作であるgotolabel()関数を呼んでいます.このとき引数にはerrlabメンバに登録されているモノのうち, 最後尾のLabel構造体へのアドレスを引数として渡しています. これには戻るべきwaserror()関数の場所の情報が入っているのでした.
void nexterror(void) { gotolabel(&up->errlab[--up->nerrlab]); }
gotolabel()関数
gotolabel()関数は, 与えられたLabel構造体からsp, pcメンバの値を取得し, それぞれスタックポインタのアドレスを表すSPとその最上部に位置するリターンアドレスにセットします. これにより, この関数は対応するsetlabel()関数が返った位置(waserror()関数)にもどる事になります. またsetlabel()関数では0を返していましたが, 例外で戻ってきたことを表すためにここでは1を返します.
TEXT gotolabel(SB), $0 MOVL label+0(FP), AX MOVL 0(AX), SP /* restore sp */ MOVL 4(AX), AX /* put return pc on the stack */ MOVL AX, 0(SP) MOVL $1, AX /* return 1 */ RET
poperror()関数
poperror()関数は直前のwaserror()関数によるエラー処理を取り消すものでした. 先ほどみたような形でLabel構造体を管理しているので, nerrlabをデクリメントするだけでできたりします.
#define poperror() up->nerrlab--
*1:が、waserror()関数ではどうにもこれをきちんと管理しているようには見えない・・・