OS起動編⑤ seginit() (Xv6を読む~OSコードリーディング~)
前回
jupiteroak.hatenablog.com
トップページ
jupiteroak.hatenablog.com
main.c(一部抜粋)
https://github.com/mit-pdos/xv6-public/blob/master/main.c#L24
int main(void) { ... seginit(); // segment descriptors ...
vm.c
https://github.com/mit-pdos/xv6-public/blob/master/vm.c#L15
void seginit(void) { struct cpu *c; // Map "logical" addresses to virtual addresses using identity map. // Cannot share a CODE descriptor for both kernel and user // because it would have to have DPL_USR, but the CPU forbids // an interrupt from CPL=0 to DPL=3. c = &cpus[cpuid()]; c->gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, 0); c->gdt[SEG_KDATA] = SEG(STA_W, 0, 0xffffffff, 0); c->gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, DPL_USER); c->gdt[SEG_UDATA] = SEG(STA_W, 0, 0xffffffff, DPL_USER); lgdt(c->gdt, sizeof(c->gdt)); }
seginit関数は、この関数を現在実行しているプロセッサが使用するGDT(グローバルディスクリプタテーブル)とGDTR(グローバルディスクリプタテーブルレジスタ)の設定を行います。
具体的には、システム上にあるプロセッサ1つにつき、ユーザモードで動作するプロセスにおいて使用される命令セグメント(ユーザコードセグメント)とデータセグメント(ユーザデータセグメント)、カーネルモードで動作するプロセスにおいて使用される命令セグメント(カーネルコードセグメント)とデータセグメント(カーネルデータセグメント)の4種類のセグメントを便宜的に設定します。
セグメントの設定について
4種類それぞれのセグメント(ユーザーコードセグメント、ユーザーデータセグメント、カーネルコードセグメント、カーネルデータセグメント)は、ベースアドレスが0から始まり、セグメントサイズは4GB(セグメントディスクリプタのリミット値(bit51-48,15-0)が0xf ffff、Gフラグ(bit55)が1)に設定されています。このようなセグメントの使い方を基本フラットモデルと言います。
ベースアドレスを0に設定することで、論理アドレス(機械語命令などでアドレスを指定する時に使用するアドレス)のオフセット値が、リニアアドレス(仮想アドレス)と一致するようになります。
また、ベースアドレスを0、セグメントサイズを4GBに設定することで、ユーザモードであってもカーネルモードであってもすべてのプロセスにおいて、論理アドレス(前述の通り仮想アドレスと一致する)をアドレッシングの限界値(0x0000 0000〜0xffff ffff =4GB)まで使用できます。
CPUがプロテクトモード(32bitモード)で動くためにセグメントディスクリプタを必要とし、セグメント回路を無効化することができないので、便宜的にこのような設定を行なっています。
セグメントディスクリプタの構造
セグメントディスクリプタの構造は、属性値12bit、セグメントベース32bit、リミット値20bitに分かれていますが、実際はこのように綺麗な順番では並んでおらず、次のように少し複雑な順番並んでいます。
(セグメントディスクリプタの構造を属性部、セグメントベース部、リミット部で色分けした図)
属性12bitセグメントベース32bitリミット20bit
↓
セグメントベース63-56属性55-52リミット値51-48属性47-40セグメントベース39-16リミット値15-0
属性12bitはbit55-52の4bitとbit47-40の8bitから構成され、セグメントベース32bitはbit63-56の8bit とbit39-16の24bitから構成され、リミット値20bitはbit51-48の4bitとbit15-0の16bitから構成されています。
セグメントディスクリプタのデータ構造はsegdesc構造体として表現されています。
cpu構造体のメンバgdt
cpu構造体のメンバであるgdtは、グローバルディスクリプタテーブルを表現したsegdesc構造体の配列になっています。
処理の内容
- 現在動作中のプロセッサに対応するcpu構造体を取得する。
- カーネルコードセグメントを設定する
- カーネルデータセグメントを設定する
- ユーザコードセグメントを設定する
- ユーザデータセグメントを設定する
- GDTR(グローバルディスクリプタテーブルレジスタ)を設定する
現在動作中のプロセッサに対応するcpu構造体を取得する。
c = &cpus[cpuid()];
cpuid()を現在実行しているCPUに対応するcpu構造体(を指定するアドレス)を取得しています。
カーネルコードセグメントを設定する
c->gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, 0);
カーネルコードセグメントを設定します。カーネルコードセグメントの設定は、グローバルディスクリプタテーブル(cpu構造体のメンバであるgdt)の2つ目(SEG_KCODE→インデックス1)のセグメントディスクリプタに、SEGマクロを使って記述します。
1つ目の引き数(type)は、セグメントの種類を指定する4bitの値です。
設定値は STA_X | STA_R→0x0a→0b 1010(あるいは、typeの4bitのうち1bit目(Aフラグ)を無視して、0b 101)なので、GDTの2つ目にあるセグメントディスクリプタは、実行と読み出しが可能なコードセグメントに関するセグメントディスクリプタになります。
2つ目の引数(base)は、セグメントベース(コードセグメントの先頭アドレス)となる32bitの値です。
設定値は 0→0x0000 0000です。
3つ目の引数(lim)は、セグメントのサイズを指定するリミット値(セグメントのサイズ-1)を上位20bitに含んだ値です。
設定値は0xffffffffなので、リミット値は0xfffffとなります。
また、SEGマクロ内でセグメントディスクリプタのbit55(Gフラグ)を1に設定しているので、コードセグメントのサイズは4GBとなります。
4つ目の引数(dpl)は、セグメントへのアクセスに必要なCPUの特権レベルを指定する2bitの値です。
カーネルモードで使用されるセグメントなので、設定値は0→0b 00(ディスクリプタ特権レベル0)が設定されます。
カーネルデータセグメントを設定する
c->gdt[SEG_KDATA] = SEG(STA_W, 0, 0xffffffff, 0);
カーネルデータセグメントを設定します。カーネルデータセグメントの設定は、グローバルディスクリプタテーブル(cpu構造体のメンバであるgdt)の3つ目(SEG_KDATA→インデックス2)のセグメントディスクリプタに、SEGマクロを使って記述します。
1つ目の引き数(type)は、セグメントの種類を指定する4bitの値です。
設定値は STA_W→0x2 →0b 0010(あるいは、typeの4bitのうち1bit目(Aフラグ)を無視して、0b 001)なので、GDTの3つ目にあるセグメントディスクリプタは、読み書き可能なデータセグメントに関するセグメントディスクリプタになります。
2つ目の引数(base)は、セグメントベース(データセグメントの先頭アドレス)となる32bitの値です。
設定値は 0→0x0000 0000です。
3つ目の引数(lim)は、セグメントのサイズを指定するリミット値(セグメントのサイズ-1)を上位20bitに含んだ値です。
設定値は0xffffffffなので、リミット値は0xfffffとなります。
また、SEGマクロ内でディスクリプタのbit55(Gフラグ)を1に設定しているので、データセグメントのサイズは4GBとなります。
4つ目の引数(dpl)は、セグメントへのアクセスに必要なCPUの特権レベルを指定する2bitの値です。
カーネルモードで使用されるセグメントなので、設定値は0→0b 00(ディスクリプタ特権レベル0)が設定されます。
ユーザコードセグメントを設定する
c->gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, DPL_USER);
ユーザコードセグメントを設定します。ユーザコードセグメントの設定は、グローバルディスクリプタテーブル(cpu構造体のメンバであるgdt)の4つ目(SEG_UCODE→インデックス3)のセグメントディスクリプタに、SEGマクロを使って記述します。
1つ目の引き数(type)は、セグメントの種類を指定する4bitの値です。
設定値は STA_X | STA_R→0x0a→0b 1010(あるいは、typeの4bitのうち1bit目(Aフラグ)を無視して、0b 101)なので、GDTの4つ目にあるセグメントディスクリプタは、実行と読み出しが可能なコードセグメントに関するセグメントディスクリプタになります。
2つ目の引数(base)は、セグメントベース(コードセグメントの先頭アドレス)となる32bitの値です。
設定値は 0→0x0000 0000です。
3つ目の引数(lim)は、セグメントのサイズを指定するリミット値(セグメントのサイズ-1)を上位20bitに含んだ値です。
設定値は0xffffffffなので、リミット値は0xfffffとなります。
また、SEGマクロ内でセグメントディスクリプタのbit55(Gフラグ)を1に設定しているので、コードセグメントのサイズは4GBとなります。
4つ目の引数(dpl)は、セグメントへのアクセスに必要なCPUの特権レベルを指定する2bitの値です。
ユーザモードで使用されるセグメントなので、設定値はDPL_USER→0b 11(ディスクリプタ特権レベル3)が設定されます。
ユーザデータセグメントを設定する
c->gdt[SEG_UDATA] = SEG(STA_W, 0, 0xffffffff, DPL_USER);
ユーザデータセグメントを設定します。ユーザデータセグメントの設定は、グローバルディスクリプタテーブル(cpu構造体のメンバであるgdt)の5つ目(SEG_UDATA→インデックス4)のセグメントディスクリプタに、SEGマクロを使って記述します。
1つ目の引き数(type)は、セグメントの種類を指定する4bitの値です。
設定値は STA_W→0x2 →0b 0010(あるいは、typeの4bitのうち1bit目(Aフラグ)を無視して、0b 001)なので、GDTの5つ目にあるセグメントディスクリプタは、読み書き可能なデータセグメントに関するセグメントディスクリプタになります。
2つ目の引数(base)は、セグメントベース(データセグメントの先頭アドレス)となる32bitの値です。
設定値は 0→0x0000 0000です。
3つ目の引数(lim)は、セグメントのサイズを指定するリミット値(セグメントのサイズ-1)を上位20bitに含んだ値です。
設定値は0xffffffffなので、リミット値は0xfffffとなります。
また、SEGマクロ内でディスクリプタのbit55(Gフラグ)を1に設定しているので、データセグメントのサイズは4GBとなります。
4つ目の引数(dpl)は、セグメントへのアクセスに必要なCPUの特権レベルを指定する2bitの値です。
ユーザモードで使用されるセグメントなので、設定値はDPL_USER→0b 11(ディスクリプタ特権レベル3)が設定されます。
(ちなみに、グローバルディスクリプタテーブル(cpu構造体のメンバであるgdt)の0番目にあるセグメントディスクリプタは、セグメントレジスタ(CSレジスタ、DSレジスタなど)を無効にするために使用されるので、全て0の値を設定しています。)