システムコールの実装

今まで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:LinuxFreeBSDシステムコールの発行にint 128を使っている.

*3:Linuxのようにカーネルとlibcのメンテナンスが別々になされていると一貫性を保つことは問題になるけど,BSDはどうなっているんだろう?

*4:トラップタイプによってプロセッサがエラーコードをスタックに積むかどうか異なるので,その違いを吸収するために_strayintrと_strayintrxが使い分けられる.

*5:Linuxカーネルを知っている人はstruct pt_regsと同じだと思えばよい.