0x19fの日報

なるべく毎日書きます

【メモ】UNIX V6コードリーディング(トラップハンドラ/システムコール, p171〜184)

今回の内容

前回は, 主にタイマー割り込みに関連する箇所を読んでいました.

0x19f.hatenablog.com

今回は, トラップハンドラでの処理を見ていきます. システムコールについてもこの辺りに書かれています.

トラップハンドラtrap

トラップハンドラも割り込みハンドラ同様に呼び出しが行われcsvが実行された後のスタックの状態は以下のようになります.

引数 備考
sp pc csvから戻るのに使用されるだけ(あまり関係ない)
退避されたr2
退避されたr3
退避されたr4
r5 退避されたr5
pc(割り込みハンドラから返る先)
マスクされたPSW dev
割り込まれたプロセスのsp sp
古いr1 r1
PSW nps
古いr0 r0
割り込まれたプロセスのpc pc
割り込まれたプロセスのPSW ps

trap関数の9行目でu.u_ar0に引数r0のアドレスを格納していますが, これはスタック上のアドレスになります. 実際にスタック上の値を操作する際はu.u_ar0[R0]のようにして操作するのですが, R0はただの定数でスタック上のr0が格納されている位置からのオフセットです. 同様にしてR0R7RPS(PSWの操作に用いる)を使ってスタック上の値を書き換えています. トラップハンドラ内ではあまりこの操作は行われませんが, システムコールハンドラ内で行われるのでしょうか?

その後のswitch分でdev(トラップ種別)の値によって処理が分岐していきますが, 基本的にはシグナルを送って処理は終わりです. 例外はillegal instruction, floating exception(カーネルプロセス), segmentation exception, システムコールの時です.

illegal instructionの時はSETDなる謎の命令か否かをチェックしているようですが, 正直なんのためのものなのかあまりわかってないです.

カーネルプロセスでは浮動小数点演算を行わないらしいのですが, PDP11の特性上ユーザープロセスが行った浮動小数点演算によるfloating exceptionがトラップ発生後に引き起こされることがあるらしく, その場合はそのユーザープロセスにシグナルを送って関数から抜けます.

segmentation exceptionの場合, スタックを拡張します. それでもexceptionを回避できない場合は, プロセスにシグナルを送ります. このスタックの拡張はgrowという関数の中で行われるのですが, 仮想メモリ空間の話とかをあまりちゃんと理解していないので一旦飛ばします. ちなみに, growの前にbackupという関数が実行されているのですが, この関数はUNIX V6の中で最難関らしいです. exceptionが起きた時の状態を再現する関数らしいのですが, すべてアセンブリで書かれておりおまけに中でPDP11にはないレジスタの挙動をエミュレートしているようです. 実際にソースコードを読むと/ hard partというコメントが見つけられます. 最難関なのでは仕方ないので飛ばします.

システムコールの場合の処理の前にシステムコールの処理の流れをまとめたいと思います.

システムコールの処理の流れ

システムコールハンドラはsysent構造体によって管理されます. この構造体にはシステムコールの引数の数とハンドラのアドレスが格納されます.

sysent構造体の配列sysentに各システムコールの引数の数とハンドラのアドレスが格納されています. この配列は要素数が64で, 添字はsys命令のオペランド(6 bit)と対応しています.

システムコールの0番は間接呼び出しに割り当てられています. 間接呼び出しというものを自分は初めて知ったのですが, どうやら普通の呼び出しと異なるのは引数の渡し方のようです. 通常の呼び出しは, sys命令の後に引数が埋め込まれます. 一方で, 間接呼び出しの場合は, sys命令の後に引数のアドレスが埋め込まれます. これは推測なのですが, 直接命令の場合は引数が埋め込まれる場所がテキストセグメントになるため, 動的にシステムコールの引数が変わる場合に困ってしまうのでこのような仕組みが用意されているのではないかと考えています. sys命令の次にデータ領域のアドレスを埋め込んでおけば, 複数のプロセスでテキストセグメントを共有しつつ個別に引数を扱うことができます. あるいは, 引数の数が動的に変わる場合などでしょうか.

システムコールの引数は上述のようなsys命令の後に埋め込むものと, r0を経由するものがあります.

トラップハンドラ内でのシステムコールの処理

話はトラップハンドラのswitch文に戻ってくるのですが, システムコールの場合の処理を見ていきます. 基本的にはエラーフラグを初期化, 間接呼び出し/直接呼び出しそれぞれの場合で引数とシステムコールの番号を取得, trap1を呼び出してシステムコールハンドラを実行, エラーフラグの処理という流れです.

特筆すべきなのがtrap1を用いてハンドラを呼び出している点です. trap1の中を見てみるとわかるのですが, フラグを立ててからsavuしてシステムコールハンドラを呼び出しています. これが意味するところなのですが, これはシステムコール中にシグナルを受け取った場合にエラーを起こすためのものらしいです. 詳しくはシグナル処理のあたりで書かれています. このsavuに対応するaretusleep関数の末尾にあります. このエラーはシステムコールハンドラがsleepを実行して処理を中断している間にシグナルを受け取った場合に起こるものらしいです. シグナルを受け取った場合は, trap1が返る場所から処理が継続されます. この場合は, フラグが立ったまま処理が続行されるため, その後のtrapでの処理でユーザープロセスにはエラーが返ります. このエラーを見てユーザープロセスは再度システムコールを実行することができるようになっています.

まとめ

ということで割り込みとトラップを見てきましたが, なかなか複雑だなぁという感想です. もう一度プロセス管理あたりも読み直してみると納得感が増すのかなぁと思います.

次回の予定

シグナル?メモリ管理? 未定です