5.6. seginit関数

この関数はGDTを作成し、ロードする。

ここまでは「3.1. bootasm.S」で作成した3つのエントリを持つGDTを使用してきた。
ここではユーザ空間用のセグメントディスクリプタを持った新しいGDTを作成する。

セグメントディスクリプタの構造は「Intel 64 and IA-32 architectures software developer's manual combined volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4」(リンク8)の「Vol.3 3.4.5 Segment Descriptors」に書いてある。

関数を実行しているcpu構造体のgdtフィールドに以下の4つのセグメントディスクリプタを作成する。0番目はNULLディスクリプタなので初期化は不要。

  1番目: カーネル用のコードセグメントディスクリプタ
  2番目: カーネル用のデータセグメントディスクリプタ
  3番目: ユーザ用のコードセグメントディスクリプタ
  4番目: ユーザ用のデータセグメントディスクリプタ

4つともベースアドレスは0x0、セグメントリミットは0xFFFFF。
Gフラグが1なので、セグメントリミットはページサイズ倍(4kB)され0xFFFFF000(4GB)となる。
タイプは4つとも読み書き可能、コードセグメントディスクリプタのみ実行可能にもなっている。
DPL(Descriptor Privilege Level)はカーネル用は0、ユーザ用は3。

全てのセグメントの範囲が0から4GBまでで重なっている。 メモリ管理にはページング機構を使用するが、セグメント機構は無効化できないため、こうして4GBまでをセグメント機構的になんにでも使えるようにしている。
cpu構造体のgdtフィールドは要素数が6(定数NSEGS)に定義されているため、GDTにはエントリが6つあることになる。 0から4番目まではここで作成するが、5番目はプロセスのコンテキストスイッチを行う際にTSS(タスク状態セグメント)ディスクリプタとして作成することになる。

作成したGDTをlgdt関数でgdtレジスタにロードする。
このとき、ecsレジスタの値は更新不要。 「3.1. bootasm.S」ではファージャンプを行うことでecsの設定を行っているが、コードセグメントディスクリプタのインデックスがここで作成したGDTでも変わらず1番目(8バイト目)であるため、再設定が不要。

mmu.h

#define SEG_KCODE 1  // kernel code
#define SEG_KDATA 2  // kernel data+stack
#define SEG_UCODE 3  // user code
#define SEG_UDATA 4  // user data+stack
#define SEG_TSS   5  // this process's task state

/* 略 */

#define SEG(type, base, lim, dpl) (struct segdesc)    \
{ ((lim) >> 12) & 0xffff, (uint)(base) & 0xffff,      \
  ((uint)(base) >> 16) & 0xff, type, 1, dpl, 1,       \
  (uint)(lim) >> 28, 0, 0, 1, 1, (uint)(base) >> 24 }

/* 略 */

#define STA_X       0x8     // Executable segment
#define STA_W       0x2     // Writeable (non-executable segments)
#define STA_R       0x2     // Readable (executable segments)

vm.c

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));
}

作成されたセグメントディスクリプタをgdbで確認すると以下のようになっている。

$1 = {lim_15_0 = 0x0, base_15_0 = 0x0, base_23_16 = 0x0, type = 0x0, s = 0x0, dpl = 0x0, p = 0x0, lim_19_16 = 0x0, avl = 0x0, rsv1 = 0x0, db = 0x0, g = 0x0, base_31_24 = 0x0}
$2 = {lim_15_0 = 0xffff, base_15_0 = 0x0, base_23_16 = 0x0, type = 0xa, s = 0x1, dpl = 0x0, p = 0x1, lim_19_16 = 0xf, avl = 0x0, rsv1 = 0x0, db = 0x1, g = 0x1, base_31_24 = 0x0}
$3 = {lim_15_0 = 0xffff, base_15_0 = 0x0, base_23_16 = 0x0, type = 0x2, s = 0x1, dpl = 0x0, p = 0x1, lim_19_16 = 0xf, avl = 0x0, rsv1 = 0x0, db = 0x1, g = 0x1, base_31_24 = 0x0}
$4 = {lim_15_0 = 0xffff, base_15_0 = 0x0, base_23_16 = 0x0, type = 0xa, s = 0x1, dpl = 0x3, p = 0x1, lim_19_16 = 0xf, avl = 0x0, rsv1 = 0x0, db = 0x1, g = 0x1, base_31_24 = 0x0}
$5 = {lim_15_0 = 0xffff, base_15_0 = 0x0, base_23_16 = 0x0, type = 0x2, s = 0x1, dpl = 0x3, p = 0x1, lim_19_16 = 0xf, avl = 0x0, rsv1 = 0x0, db = 0x1, g = 0x1, base_31_24 = 0x0}

cpuid関数

この関数は、この関数を実行しているプロセッサのcpuidを返す。

ここで言うcpuidとは、cpus配列のインデックスのこと。
各プロセッサのcpu構造体はcpus配列のエントリとして保持しており、mycpu関数はそれを実行しているプロセッサのエントリを返してくれる。
そのエントリのアドレスからcpus配列の先頭アドレスを引けば、cpus配列内でのインデックスになる。

proc.c

> 30 int
> 31 cpuid() {
> 32   return mycpu()-cpus;
> 33 }

mycpu関数

この関数は、この関数を実行しているプロセッサのcpu構造体をcpus配列から探して返す。

eflagsレジスタの値を取得し、Interrupt enableビット(9bit)を確認して、割り込みが無効になっていることを確認する。 0が無効で1が有効。 eflagsレジスタの各bitの役割は「Intel 64 and IA-32 architectures software developer's manual combined volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4」(リンク8)の「Vol.3 2.3 SYSTEM FLAGS AND FIELDS IN THE EFLAGS REGISTER」に書いてある。

cpus配列をfor文で走査し、apicidフィールドの値が関数を実行しているプロセッサのLAPIC IDと等しいエントリを探す。 各cpu構造体のapicidフィールドは「5.4. mpinit関数」で設定済み。

mmu.h

#define FL_IF           0x00000200      // Interrupt Enable

proc.c

struct cpu*
mycpu(void)
{
  int apicid, i;
  
  if(readeflags()&FL_IF)
    panic("mycpu called with interrupts enabled\n");
  
  apicid = lapicid();
  // APIC IDs are not guaranteed to be contiguous. Maybe we should have
  // a reverse map, or reserve a register to store &cpus[i].
  for (i = 0; i < ncpu; ++i) {
    if (cpus[i].apicid == apicid)
      return &cpus[i];
  }
  panic("unknown apicid\n");
}

readeflags関数

この関数はeflagsレジスタの値を取得する。
pushfl命令はeflagsレジスタの内容をスタックに積む命令。

x86.h

static inline uint
readeflags(void)
{
  uint eflags;
  asm volatile("pushfl; popl %0" : "=r" (eflags));
  return eflags;
}

lapic関数

この関数は、この関数を実行しているプロセッサのLAPIC IDを取得する。

LAPIC IDは 「Intel 64 and IA-32 architectures software developer's manual combined volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4」(リンク8)の「Vol.3 10.4.6 Local APIC ID」によると、Local APIC ID Register(0x20)の24~31bitから得られる。
LAPICへのアクセスについては「5.4. mpinit関数」に書いた。

lapic.c

int
lapicid(void)
{
  if (!lapic)
    return 0;
  return lapic[ID] >> 24;
}