はじめての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 descriptorsseginit関数は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 tablesegdesc構造体は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構造体は、セグメントディスクリプタです。
参考画像:Intel SDM vol3
セグメントディスクリプタは、x86CPUのメモリ保護機構に関するメモ書き(GDTとLDT)で軽く触れたGDTとLDTのエントリとなるデータ構造です。
セグメントディスクリプタは、CPUにセグメントのサイズやアドレス、またアクセス権限や状態を通知します。
x86CPUでは、この仕組みによってメモリ保護を実現していました。
SEG_KCODEなどのセグメントセレクタと、割り当てしている権限についてはブートストラップを参照したときにも確認したので、この記事では割愛します。
参考:xv6OSを真面目に読みこんでカーネルを完全に理解する -ブートストラップ編-
まとめ
カーネル側でセグメントディスクリプタの初期化を行いました。
次回はpicinit関数から。。。