All Articles

xv6OSを真面目に読みこんでカーネルを完全に理解する -セグメントディスクリプタ初期化 編-

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみにインスパイアされてxv6 OSを読んでます。

UNIX V6自体はx86CPUでは動作しないため、基本的には、UNIXv6をX86アーキテクチャで動くようにしたxv6 OSのリポジトリをForkしたkash1064/xv6-public: xv6 OSのソースコードを読んでいくことにしました。

前回main関数で実行されるlapicinit関数によるローカルAPICの設定を確認しました。

今回はseginit関数の挙動を追っていきます。

もくじ

seginit関数

seginit関数はCPUにカーネルセグメントディスクリプタを設定します。

seginit();       // segment descriptors

seginit関数はvm.cで以下のように定義されています。

// Set up CPU's kernel segment descriptors.
// Run once on entry on each CPU.
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));
}

ソースコードを読んでいきます。

まず、cpu構造体はproc.hで次のように定義されていました。

// Per-CPU state
struct cpu {
  uchar apicid;                // Local APIC ID
  struct context *scheduler;   // swtch() here to enter scheduler
  struct taskstate ts;         // Used by x86 to find stack for interrupt
  struct segdesc gdt[NSEGS];   // x86 global descriptor table
  volatile uint started;       // Has the CPU started?
  int ncli;                    // Depth of pushcli nesting.
  int intena;                  // Were interrupts enabled before pushcli?
  struct proc *proc;           // The process running on this cpu or null
};

cpu構造体は、配列cpusに格納されています。

これは、マルチプロセッサ 編で見た通りparam.hで定義されたNCPUに設定されている通り、最大8つのCPUをサポートする配列です。

cpus関数

配列cpusからcpu構造体を取得する箇所を見てみます。

struct cpu *c;
c = &cpus[cpuid()];

cpuid関数はproc.cで以下のように定義された関数です。

mycpu関数の戻り値からcpuを引いた値を返します(何やってるんだろう…)。

// Must be called with interrupts disabled
int cpuid() {
  return mycpu()-cpus;
}

// Must be called with interrupts disabled to avoid the caller being
// rescheduled between reading lapicid and running through the loop.
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");
}

mycpu関数の肝は以下のコードです。

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

lapicid関数は、lapic.cで定義されている、ローカルAPICからAPICIDを取得して24bitの右シフトを行ったものを返却する関数です。

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

変数lapicには、マルチプロセッサ 編で見た通りmp.cでローカルAPICのアドレスが格納されています。

Intelのマルチプロセッサ仕様書(5-1)によると、ローカルAPICはベースメモリアドレス0x0FEE00000に置かれ、ローカルAPICIDは0から始まるハードウェアに連続して割り当てされるようです。

参考:INTEL MULTIPROCESSOR SPECIFICATION Pdf Download | ManualsLib

デバッガで確認してみたところ、初回呼び出し時はlapic[ID]の値は0でした。

つまりlapicidの戻り値も0になります。

これによってapicid = lapicid();apicidには0が入ります。

続くループの処理をデバッガで確認したところ、cpus[i].apicidの値も0でapicidと一致するため、&cpus[i]には&cpus[0]が返却さえrました。

for (i = 0; i < ncpu; ++i) {
  if (cpus[i].apicid == apicid) return &cpus[i];
}

つまり、return mycpu()-cpus;も0となり、最初のcpuid関数実行時の戻り値は0となることを確認しました。

これによって、初回起動時のc = &cpus[cpuid()];c = &cpus[0];となります。

GDTのセット

というわけで、続く以下の行では、&cpus[0]gdt[NSEGS]要素に値をセットしていることがわかります。

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

&cpus[0]は前述したcpu構造体であり、gdtは以下のように定義されています。

struct segdesc gdt[NSEGS];   // x86 global descriptor table

segdesc構造体はmmu.hで定義されている構造体です。

#ifndef __ASSEMBLER__
// Segment Descriptor
struct segdesc {
  uint lim_15_0 : 16;  // Low bits of segment limit
  uint base_15_0 : 16; // Low bits of segment base address
  uint base_23_16 : 8; // Middle bits of segment base address
  uint type : 4;       // Segment type (see STS_ constants)
  uint s : 1;          // 0 = system, 1 = application
  uint dpl : 2;        // Descriptor Privilege Level
  uint p : 1;          // Present
  uint lim_19_16 : 4;  // High bits of segment limit
  uint avl : 1;        // Unused (available for software use)
  uint rsv1 : 1;       // Reserved
  uint db : 1;         // 0 = 16-bit segment, 1 = 32-bit segment
  uint g : 1;          // Granularity: limit scaled by 4K when set
  uint base_31_24 : 8; // High bits of segment base address
};

ちなみにNSEGSも、mmu.hで定数6として定義されています。

// cpu->gdt[NSEGS] holds the above segments.
#define NSEGS     6

ここで定義されているsegdesc構造体は、セグメントディスクリプタです。

2022/02/image-4.png

参考画像:Intel SDM vol3

セグメントディスクリプタは、x86CPUのメモリ保護機構に関するメモ書き(GDTとLDT)で軽く触れたGDTとLDTのエントリとなるデータ構造です。

セグメントディスクリプタは、CPUにセグメントのサイズやアドレス、またアクセス権限や状態を通知します。

x86CPUでは、この仕組みによってメモリ保護を実現していました。

SEG_KCODEなどのセグメントセレクタと、割り当てしている権限についてはブートストラップを参照したときにも確認したので、この記事では割愛します。

参考:xv6OSを真面目に読みこんでカーネルを完全に理解する -ブートストラップ編-

まとめ

カーネル側でセグメントディスクリプタの初期化を行いました。

次回はpicinit関数から。。。

参考書籍