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構造体を取得する。

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の値を設定しています。)

GDTR(グローバルディスクリプタテーブルレジスタ)を設定する

lgdt(c->gdt, sizeof(c->gdt));

lgdt関数を呼び出して、GDTの先頭アドレス(グローバルディスクリプタテーブルの先頭アドレス)c->gdtとGDTのサイズ(グローバルディスクリプタテーブルのサイズ)sizeof(c->gdt)を、GDTR(グローバルディスクリプタテーブルレジスタ)に設定します。




次回
jupiteroak.hatenablog.com