令和アドベントカレンダー: advos
Extra Day 3: システムコール
システムコールは,非特権プロセスからメモリ割り当て(ページテーブルの設定)などの特権命令を必要とする機能を呼び出すためのインターフェイスです。提供するシステムコールはOSにより異なりますが,Unix系のOSでは多くのPOSIXに準拠したシステムコールを実装しています。
x86/x86-64におけるシステムコールの実装は,従来はint
命令でCPUにソフトウェア割り込みを送ることで,アドベントカレンダー16日目で扱ったIDTで設定する割り込みハンドラを通じて,Ring 3からRing 0(カーネル)の機能を呼び出す方法が使われてきました。しかし,int
命令は割り込みなので,CPUのパイプラインが止まり,またレジスタをスタックフレームに退避させるなど,オーバーヘッドが大きいため,システムコールの頻繁な呼び出しがプログラムの性能が大きく低下する問題がありました。そこで,x86/x86-64では,システムコールのためのsysenter
命令やsyscall
命令が実装されました。Ring 3とRing 0しか利用できないなどといった制約はありますが,sysenter
命令よりもsyscall
命令の方が高効率な命令なので,今回はsyscall
命令を使ってシステムコールを実装します。
syscallの挙動
syscall
命令はRing 3からRing 0への遷移を伴うcall
命令のようなものです(呼び出し規則はかなり違いますが……)。syscall
命令は,RIP
,RFLAGS
レジスタの値をRCX
,R11
にセットし,IA32_LSTAR
で指定したエントリポイントにジャンプします。IA32_LSTAR
はMachine-Specific Register (MSR)で,後述します。この際に,RFLAGS
レジスタは,IA32_FMASK
で論理ANDを取った値にマスクされます。コードセグメントレジスタおよびスタックセグメントレジスタは,CS=IA32_STAR[47:32]
,SS=IA32_STAR[47:32]+8
と設定されます。このとき,GDTの値に依らず,CPL=0となります。
sysret
命令は,RCX
,R11
レジスタの値をRIP
,RFLAGS
にセットし,64ビットモードならCS=IA32_STAR[63:48]+16
,32ビットモードならCS=IA32_STAR[63:48]
をセットし,
Ring 3(つまりCS = CS | 3
)とします。また,SS=IA32_STAR[63:48]+8
とします。IA32_STAR
はMSRで,後述します。ディスクリプタのLフラグ/Dフラグもオペランドサイズにより再設定されます。16日目でRing 3のGDTを32ビットコードセレクタ,32ビットデータセレクタ,64ビットコードセレクタ,64ビットデータセレクタの順にしたのは,このsysret
の制約に合わせるためです。
syscallの有効化
syscall
命令はCPUID (EAX=0x80000001)
のEDX[29]
(Intel(R) 64 Architecture Available)ビットとEDX[11]
のSYSCALL/SYSRET available in 64-bit modeビットがセットされている場合に有効です。なので,まずCPUIDでcapabilityを確認します。上述した通り,syscallのエントリーポイントの設定やリング(セグメントレジスタ)遷移の設定,有効化はMachine-Specific Register (MSR)で行います。IA32_FMASK
,IA32_LSTAR
,IA32_STAR
およびIA32_EFER
の4つのMSRを使います。
syscall
を有効にするために以下のようなコードを書きました。
void
syscall_init(void *table, int nr)
{
uint64_t val;
uint64_t rax;
uint64_t rbx;
uint64_t rcx;
uint64_t rdx;
/* Check CPUID.0x80000001 for syscall support */
rax = cpuid(0x80000001, &rbx, &rcx, &rdx);
if ( !((rdx >> 11) & 1) || !((rdx >> 29) & 1) ) {
panic("syscall is not supported.");
}
/* EFLAG mask */
wrmsr(MSR_IA32_FMASK, 0x0002);
/* Entry point to the system call */
wrmsr(MSR_IA32_LSTAR, (uint64_t)syscall_entry);
/* Syscall/sysret segments */
val = GDT_RING0_CODE_SEL | ((GDT_RING3_CODE32_SEL + 3) << 16);
wrmsr(MSR_IA32_STAR, val << 32);
/* Enable syscall */
val = rdmsr(MSR_IA32_EFER);
val |= 1;
wrmsr(MSR_IA32_EFER, val);
/* Call assembly syscall_setup() */
syscall_setup((uint64_t)table, (uint64_t)nr);
}
IA32_FMASK
syscall
命令後にRFLAGSの値は,RFLAGSをr11
レジスタに保存した後,IA32_FMASK
の値でマスク(論理AND)をします。IA32_FMASK
は下記のような構造で,マスクするEFLAGS(RFLAGS)のビットを設定します。
31 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SYSCALL EFLAGS Mask |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
63 32
IA32_LSTAR
IA32_LSTAR
は以下の構造で,syscall
のエントリポイントのアドレスを指定します。
31 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Target RIP for 64-bit Mode Calling Program |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
63 32
IA32_STAR
IA32_STAR
は以下の構造体で,syscall
命令およびsysret
命令でセットされるCS/SSの値を設定します。上述した通り,SSの値はここで設定した値+8となります。またsysret
のCSの値は,62ビットモードの場合,ここで設定した値+16となります。
31 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SYSRET CS and SS | SYSCALL CS and SS |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
63 32
IA32_EFER
IA32_EFER
MSRは拡張機能の有効かをするためのMSRです。この0ビット目がSCE (SysCall enable)ビットで,このビットをセットすることでsyscall
命令およびsysret
命令が有効になります。
31 8 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |N|L| |L| Reserved |S|
| |X|M| |M| |C|
| |E|A| |E| |E|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
63 32
SCE: SysCall enable
LME: IA-32e mode enable
LMA: IA-32e mode active
NXE: Execute-disable bit enable
システムコールテーブル
syscall
命令は上述の通り,Ring 0に遷移する命令ですが,呼び出すシステムコールについてはこのエントリポイントの先で扱うことになります。一般にはRAX
レジスタにシステムコール番号をセットします。引数の呼び出し規則についてはOSにより異なると思いますが,今回はSystem V AMD64 ABIの第4引数をR10
にしたものを使います(スタックに積まれる範囲はとりあえず無視しています)。
RAX
レジスタに対応するシステムコールを高速に検索するために,システムコールのリストはテーブルとして保持します。システムコールのエントリポイントはレジスタを直接触るためアセンブリで書きますが,そこでシステムコールテーブルを参照して,対応するシステムコール関数を呼び出します。
今回はシステムコールの初期化時に,以下の通り767番にsys_hlt()
関数をセットしたテーブルを用意しています。
/* Setup system call */
syscall = kmalloc(sizeof(void *) * SYS_MAXSYSCALL);
if ( NULL == syscall ) {
panic("Failed to allocate the syscall table.");
}
for ( i = 0; i < SYS_MAXSYSCALL; i++ ) {
syscall[i] = NULL;
}
syscall[767] = sys_hlt;
syscall_init(syscall, SYS_MAXSYSCALL);
このテーブルとシステムコールの最大番号は上述したsyscall_init()
関数から以下のアセンブリ関数を呼び出すことで,アセンブリ内の.data
セクションに保存します。
/* void syscall_setup(void *, uint64_t) */
_syscall_setup:
pushq %rbx
movabs $syscall_table,%rbx
movq %rdi,(%rbx)
movabs $syscall_nr,%rbx
movq %rsi,(%rbx)
popq %rbx
ret
このシステムコールテーブルへのポインタと最大番号を用い,RAX
レジスタに対応する関数を呼び出す機能をエントリポイントに以下の通り実装します。
/* Entry point to the syscall */
_syscall_entry:
/* N.B., rip and rflags are stored in rcx and r11, respectively. */
pushq %rbp
movq %rsp,%rbp
pushq %rcx
pushq %r11
pushq %rbx
/* Check the max number of the syscall table */
movabs $syscall_nr,%rbx
cmpq (%rbx),%rax
jge 1f
/* Lookup the system call table and call the corresponding to %rax */
movabs $syscall_table,%rcx
movq (%rcx),%rbx
shlq $3,%rax /* 8-byte per pointer */
addq %rax,%rbx
cmpq $0,(%rbx)
je 1f
movq %r10,%rcx /* Replace the 4th argument with %r10 */
callq *(%rbx)
1:
popq %rbx
popq %r11
popq %rcx
popq %rbp
sysretq
実行
今回の実装はsrc/kernel/arch/x86_64/asm.Sおよびsrc/kernel/arch/x86_64/arch.cにあります。
これを実行すると,day 24の内容と同様のカウントアップの画面が表示されますが,task_a()
に以下のシステムコール呼び出しを実装しているため,2つ目のカウンタが割り込みまでhltされ,1つ目と2つ目のカウンタがほぼ同一の値になることが分かるかと思います。
__asm__ ("syscall" :: "a"(767));
今日のまとめと明日の予定
今日はシステムコールを実装しました。平成最後の明日はこれまでユーザプロセスから直接メモリを触っていた文字表示をシステムコール経由で行ってみようと思います。