Advent Calendar 2018: advos
Day 17: Ring 0とRing 3
x86-64では リング という概念で特権命令やメモリを保護します。下図のようにリングの外はどの権限でも実行できる命令,リングの内側が特権を必要とする命令で,実行中の権限であるCPL (Current Privilege Level)よりも内側のリングの命令を実行した場合,General Protection Fault(#GP)の例外が発生します。
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.c
のgdt_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.cのbsp_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を発生させました。明日は,以前実装した物理メモリ管理を拡張して,カーネルメモリ管理機能を充実させようと思います。