システムコールの実装
今までopen(2)やdup(2)といったシステムコール関数を見てきたけど,それらのエントリに至る過程は無視してきたので,今日はその辺のコードを読んでみよう.
Plan9のシステムコール数にも書いたけど,システムコール番号は,libc/9syscall/sys.hで定義されている.まずは,ユーザプロセスがシステムコールを発行する箇所から読むことにする.386だったらソフトウェア割込み(int命令)を発行しているはずだとあたりをつけてみるが,見つからない.う〜ん,と思ってlibc/9syscall/mkfileを見たら,その中でシステムコールを発行するスタブルーチンを生成,コンパイルして,libc.aに組み込んでいた*1.386の場合,システムコール番号をAXレジスタに設定し,引数をスタックに積んでint 64命令を発行している*2.
libc/9syscall/mkfile SYS=`{sed '/^#define._X[123]/d; s/#define.([A-Z0-9_]*).*/\1/' sys.h} for(I in $SYS) { i=`{echo $I|tr A-Z a-z} n=`{sed -n '/[ ]'$I'[ ]/s/.* //p' sys.h} if(~ $i exits) i=_exits {switch($objtype){ : case 386 echo TEXT $i'(SB)', 1, '$0' echo MOVL '$'$n, AX echo INT '$'64 if(~ $i seek) { echo 'CMPL AX,$-1 JNE 4(PC) MOVL a+0(FP),CX MOVL AX,0(CX) MOVL AX,4(CX)' } echo RET :
例えば,open(2)の場合は,次のようなコードが生成される.アセンブリについては以前簡単に書いた.open(SB)の次の1はプロファイル関係のビットらしいが,ここでは無視しよう.
TEXT open(SB), 1, $0 MOVL $14, AX INT $64 RET
C言語の関数呼出し規則では,引数はスタックに積まれるので,システムコールだからといって特別の処理を行う必要はない.Linuxでは引数をレジスタ渡しするのでインラインアセンブラを使って引数をレジスタにセットしているが,FreeBSDではPlan9と同じくスタック渡しを採用している.
一転して,カーネル内から見ると,sysopenやsysdupなどのシステムコール本体の処理関数は,システムコール番号をインデックスにした表引きで呼ばれるのが,普通のOS実装方法だ.斜め読みしているとsystabがそれらしいとわかるが,定義している箇所を見つけることができない.これもまたlibc/9syscall/sys.hから自動生成されるのだ.ビルド時にport/mksystabスクリプトによってsys.hからsystabの定義を含むsystab.hが生成され,(後述する)9/pc/trap.cからincludeされる.こんな作りになっているのは,カーネルとlibcにおけるコードの一貫性を保つ工夫かな*3.
さて,システムコール呼出しの入口(int命令)と出口(systab[])がわかったので,その間をつなげていく.この部分のコードはアーキテクチャ依存になので,大半はアセンブリで記述される.9/pcディレクトリ以下のl.s,plan9l.s,trap.cが該当するファイルだ.386の場合,割込みや例外を処理するトラップハンドラはIDT(Interrupt Descripter Table)に設定する必要がある.IDTの初期化はtrapinit0関数で行われている.トラップハンドラはvectortableにまとめられており,各エントリは_strayintr,_strayintrx,_syscallintrのいずれかのルーチンに対するcall命令(5バイト)とトラップタイプを示す1バイトの値の組になっている.call命令を実行すると,戻り番地(つまりトラップタイプが配置されたアドレス)がスタックに積まれるので,参照することができる.システムコールだけは例外でトラップタイプは参照されない.
9/pc/l.s TEXT vectortable(SB), $0 CALL _strayintr(SB); BYTE $0x00 /* divide error */ CALL _strayintr(SB); BYTE $0x01 /* debug exception */ CALL _strayintr(SB); BYTE $0x02 /* NMI interrupt */ CALL _strayintr(SB); BYTE $0x03 /* breakpoint */ : CALL _syscallintr(SB); BYTE $0x40 /* VectorSYSCALL */ :
システムコールハンドラは_syscallintrで,それ以外のトラップのハンドラは_strayintr(x)である*4.基本的な処理は同じだが,今回はシステムコールに着目しているので,_syscallintrを見ていこう.システムコールが発行されると,特権モードに遷移し,EFLAGSやPCなどがスタックに退避され,IDTに登録された_syscallintrが実行される._syscallintrは,まずシステムコールを発行したユーザプロセスのコンテキストをカーネルスタックに退避する.このデータは後からstruct Uregとして参照される*5.
/386/include/ureg.h struct Ureg { ulong di; /* general registers */ ulong si; /* ... */ ulong bp; /* ... */ ulong nsp; ulong bx; /* ... */ ulong dx; /* ... */ ulong cx; /* ... */ ulong ax; /* ... */ ulong gs; /* data segments */ ulong fs; /* ... */ ulong es; /* ... */ ulong ds; /* ... */ ulong trap; /* trap type */ ulong ecode; /* error code (or zero) */ ulong pc; /* pc */ ulong cs; /* old context */ ulong flags; /* old flags */ union { ulong usp; ulong sp; }; ulong ss; /* old stack segment */ };
_syscallintrは,コンテキストの退避を終えたら,C言語で書かれたsyscall関数を呼ぶ.ここで,引数としてスタックに積まれたコンテキスト(struct Ureg)へのポインタを渡すために,call命令の直前にSPをpushしている.
9/pc/plan9l.s TEXT _syscallintr(SB), $0 PUSHL $VectorSYSCALL /* trap type */ PUSHL DS PUSHL ES PUSHL FS PUSHL GS PUSHAL MOVL $(KDSEL), AX MOVW AX, DS MOVW AX, ES PUSHL SP CALL syscall(SB) POPL AX POPAL POPL GS POPL FS POPL ES POPL DS ADDL $8, SP /* pop error code and trap type */ IRETL
syscall関数では,システムコール番号をインデックスにsystab[]に登録された関数を実行する.冒頭に書いたように,ユーザプロセスがシステムコールを発行するとき,AXレジスタにシステムコール番号を設定し,引数をスタックに積んでソフトウェア割込み(int 64)を発行する.そこで,syscall関数では,ureg->axからシステムコール番号を,ureg->sp + BY2WDから引数を取得する.BY2WDはbyte / wordの意味で,スタックの頭のPC分ずらすためにSPにBY2WDを加えている.引数領域用にstruct Sargsが定義されている.なお,もっとも引数の多いシステムコールはmount(2)の5つなので,MAXSYSARGは5に定義されている.
void syscall(Ureg* ureg) { scallnr = ureg->ax; sp = ureg->usp; up->s = *((Sargs*)(sp+BY2WD)); ret = systab[scallnr](up->s.args);
ユーザプロセスへの復帰処理は,戻り値をAXレジスタに格納して,iret命令を発行するという手順になるが,詳細は,また今度ということで.
*1:rcの構文を知らないとちょっとmkfileが読みにくいかもしれないが,大体見当は付くだろう.`{command}はコマンドの実行結果をリストとして返す.shの`command`と同じ.あとif文で使われている~は引数がマッチングするか判定するtest([)コマンドに近い組込みコマンド.
*2:LinuxやFreeBSDはシステムコールの発行にint 128を使っている.
*3:Linuxのようにカーネルとlibcのメンテナンスが別々になされていると一貫性を保つことは問題になるけど,BSDはどうなっているんだろう?
*4:トラップタイプによってプロセッサがエラーコードをスタックに積むかどうか異なるので,その違いを吸収するために_strayintrと_strayintrxが使い分けられる.