panda's tech note

Advent Calendar 2018: advos

Day 17: Ring 0とRing 3

x86-64では リング という概念で特権命令やメモリを保護します。下図のようにリングの外はどの権限でも実行できる命令,リングの内側が特権を必要とする命令で,実行中の権限であるCPL (Current Privilege Level)よりも内側のリングの命令を実行した場合,General Protection Fault(#GP)の例外が発生します。

Ring Protection

GDTのCPLのフィールドおよびコードセレクタの下位2ビットがCPLを表します。昨日設定したGDTのうち,インデックス0x28が64ビットコードのCP=3のGDTエントリです。コードセレクタをこの0x28+3(Ring 3)にすることで,プログラムはRing 3で動作します。以下はこのRing 3でプログラムを動かすための準備を行っています。

例外の補足

Ring 3で不正な命令を実行した際に発生するGeneral Protection Faultを捕捉しなければ,正しくプロテクションが働いているかを確認できないので,例外ハンドラとしてGeneral Protection Faultのハンドラを以下のように定義します。このハンドラは,src/kernel/arch/x86_64/arch.cに定義されたisr_exception_werror()関数を呼び出します。

/* General protection fault */
_intr_gpf:
    /* Save registers*/
    pushq	%rbp
    movq	%rsp,%rbp
    pushq	%rax
    pushq	%rdi
    pushq	%rsi
    pushq	%rdx
    pushq	%rcx
    pushq	%r8
    pushq	%r9
    pushq	%r10
    pushq	%r11
    pushq	%rbx
    /* Call isr_exception_werror() */
    movq	$13,%rdi
    movq	8(%rbp),%rsi	/* error code */
    movq	16(%rbp),%rdx	/* rip */
    movq	24(%rbp),%rcx	/* cs */
    movq	32(%rbp),%r8	/* rflags */
    movq	40(%rbp),%r9	/* rsp */
    /* 48(%rbp): ss */
    call	_isr_exception_werror
    /* Restore registers */
    popq	%rbx
    popq	%r11
    popq	%r10
    popq	%r9
    popq	%r8
    popq	%rcx
    popq	%rdx
    popq	%rsi
    popq	%rdi
    popq	%rax
    popq	%rbp
    addq	$0x8,%rsp
    iretq

isr_exception_werror()関数の中身は,以下の通り,Exceptionの中身を表示して画面に出力し,CPUを止める処理です。

void
isr_exception_werror(uint32_t vec, uint64_t error, uint64_t rip, uint64_t cs,
                     uint64_t rflags, uint64_t rsp)
{
    char buf[4096];

    ksnprintf(buf, sizeof(buf), "Exception: vec=%llx, error=%llx, rip=%llx, "
              "cs=%llx, rflags=%llx, rsp=%llx", vec, error, rip, cs, rflags,
              rsp);
    panic(buf);
}

これを昨日のキーボード割り込みと同様に(ただし例外なのでトラップ)IDTにセットします。#GPの割り込みベクタは13番なので以下のようになります。

    idt_setup_trap_gate(13, intr_gpf);

TSSの設定

Ring 3で例外や割り込みが発生した場合はRing 3の処理に移ります。昨日説明した通り,最近のOSはソフトウェアタスクスイッチを使うため,TSSのハードウェアタスクスイッチは使いません。割り込みが発生した場合はRing 0で割り込みハンドラを実行します。しかし,例外や割り込みのハンドラの呼び出し時にIPなどがスタックに積まれるため,このスタックポインタはユーザスタックとは別に特権のスタック領域にある必要があります。このスタックポインタを設定するためにTSSを使います。

TSSはGDTの一部に設定します。論理プロセッサ(Local APICごとに)TSSを設定する必要がありますが,今回はとりあえずBSPにだけ設定します。今回TSSは0x00078000に臨時で置きます。

src/kernel/arch/x86_64/desc.cgdt_init()関数内に以下の処理を追加します。

    tss = (struct gdt_desc_tss *)(GDT_ADDR + GDT_TSS_SEL_BASE);
    gdt_setup_desc_tss(&tss[0], 0x00078000, sizeof(struct tss) - 1,
                       TSS_INACTIVE, 0, 0);

また,論理プロセッサからこのTSSをタスクレジスタに読み込みます。上記で設定したnr番目のtss[nr]エントリを読み込むtr_load()関数をsrc/kernel/arch/x86_64/desc.cに以下のように実装しました。

void
tr_load(int nr)
{
    int tr;

    tr = GDT_TSS_SEL_BASE + nr * sizeof(struct gdt_desc_tss);
    ltr(tr);
}

TSSの初期化はsrc/kernel/arch/x86_64/arch.c内で以下のように行っています。

    tss_init();
    tr_load(0);

ページテーブルのUser Accessの許可

10日目に設定したページテーブルは特権モードからのアクセスのみを許可していました。0-1 GiBの範囲とカーネルを読み込んだ領域はユーザアクセスを許可するようにします。具体的には,ページテーブルについてページフラグ情報を0x83=R/Wページとしていましたが,U/Sフラグをセットして0x87とします。

本来はカーネル領域にユーザ領域からのアクセスを許可すべきではないですが,今回はRing Protectionのテストを命令について行うため,臨時でこのような設定をしています。明日以降は元に戻します。 

Ring 3への遷移

Ring 3の遷移はiretq命令で行います。iretq命令は,CPLが変わる場合は,以下の5つのレジスタをスタックからpopしてセットするのと同等の処理をします。つまり,スタックにこれらのデータを積んでおけば,iretq命令でRing 3のCS/SSに切り替えることができます。この方法はマルチタスキングのタスクスイッチにも使います。

struct {
    uint64_t rip;
    uint64_t cs;
    uint64_t rflags;
    uint64_t rsp;
    uint64_t ss;
}

Ring 3に切り替えるための処理として以下のアセンブリで定義した関数を書きました。

_chcs:
    /* Setup SP0 in TSS */
    movq	$0x00010000,%rax
    movq	$0x00078000,%rdx
    movq	%rax,TSS_SP0(%rdx)
    /* Prepare stack for iretq */
    movq	%rsp,%rdx
    movq	%rdi,%rax
    addq	$8,%rax
    pushq	%rax
    pushq	%rdx
    movq	$0x202,%rax
    pushq	%rax
    pushq	%rdi
    movabs	$1f,%rax
    pushq	%rax
    iretq
1:
    ret

これを上から順に説明します。まず,TSSのSP0領域,つまりRing 3で割り込みや例外が発生した時に使用するスタックポインタの値を0x00010000にします。この値は後日正しくメモリ割り当てを行う予定です。

    movq	$0x00010000,%rax
    movq	$0x00078000,%rdx
    movq	%rax,TSS_SP0(%rdx)

次にiretq命令のためにスタックに各種レジスタの値を積みます。まず,

    movq	%rsp,%rdx

で現在のスタックポインタの値を%rdxに保存します。次に,

    movq	%rdi,%rax
    addq	$8,%rax
    pushq	%rax

で,スタックセグメントレジスタをデータセレクタの値(コードセレクタ+8)にします。

    pushq	%rdx

により,先ほど保存したスタックポインタの値をセットします。今回はiretqの後にこのchcs関数の最後から実行を再開させるため,同じスタックポインタを使います。

    movq	$0x202,%rax
    pushq	%rax

で,RFLAGSの値を0x202になるようにします。つまりIFがセットされた状態です。

    pushq	%rdi

は,単純にコードセレクタをスタックに積む処理です。

    movabs	$1f,%rax
    pushq	%rax

この処理は少し複雑ですが,このルーチンの最後のラベル1の絶対アドレスを%raxに保存し,これをスタックに積みます。つまり,この後のiretq命令でラベル1から実行を再開するための処理です。ラベル1はret命令が書かれているため,iretq後,関数呼び出し直後のスタックポインタと同じものがセットされているため,この関数から適切に抜けます。

実行結果

Ring 3の保護が正しく動作するかを検証するために,src/kernel/arch/x86_64/arch.cbsp_start()関数の最後に以下のコードを追加しました。

    chcs(GDT_RING3_CODE64_SEL + 3);
    print_str(base, "Testing ring 3, and cause #GP");
    __asm__ __volatile__ ("movq %cr0,%rax");
    __asm__ __volatile__ ("1: jmp 1b");

3行目の__asm__ __volatile__ ("movq %cr0,%rax");が特権が必要なコードです。これを実行すると,

Exception: vec=d, error=0, rip=c00125d7, cs=2b, rflags=246, rsp=7b88

と表示されると思います。3行目のコードをコメントアウトするとエラー画面は表示されなくなります。(hlt命令も特権が必要なので,4行目は単純な無限ループにしています。)

まとめと明日の予定

今日はRing 3でコードを実行し,特権が必要な命令でGeneral Protection Faultを発生させました。明日は,以前実装した物理メモリ管理を拡張して,カーネルメモリ管理機能を充実させようと思います。