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