panda's tech note

令和アドベントカレンダー: 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命令は,RIPRFLAGSレジスタの値をRCXR11にセットし,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命令は,RCXR11レジスタの値をRIPRFLAGSにセットし,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_FMASKIA32_LSTARIA32_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));

今日のまとめと明日の予定

今日はシステムコールを実装しました。平成最後の明日はこれまでユーザプロセスから直接メモリを触っていた文字表示をシステムコール経由で行ってみようと思います。