Motivation
Understanding the Linux Kernelを読もうとしたけど、生半可な気持ちでは臨めなさそうなので、xv6で理解の確認しつつ、linux kernel 2.6ではどうなの?という把握をすれば良さそう。
そこで、Understanding the Linux KernelのChapter 2: Memory Addressingに備えてGDTとかIDTってなんやねん、っていうのを抑えておきたい。
Reference
- Intel® 64 and IA-32 Architectures Software Developer’s Manual(SDM) vol.3 (Order Number: 325384-067US May 2018) (公式ドキュメント) Figure, Sectionはこの本からの引用とします
Glosary
未だに用語で混乱するのでまとめておく:
- TSS(Task State Segment): 104 byte. xv6ではint
$T_SYSCALL
が発動したときに用いられる。(ss0とesp0がint呼び出しでロードされることに注意) c.f) Figure7.2 TR(Task Register): TSS Descriptorの先頭ポインタをロードするためのregister(GDTからのoffset[byte]) c.f) Figure 7.5
GDT(Global Descriptor Table): segment descriptorをentryとするtable。
- GDTR(GDT Register): GDTの先頭ポインタを登録するためのregister (
lgdt
命令でロード) c.f) Figure 2-6 segment descriptor: GDTのentry。64bit(8byte). c.f) Figure 3-8
- Code & Data Segment Descriptor: segment descriptorの一種。主にsegmentationの設定。
- TSS Descriptor: 主に、int
$T_SYSCALL
の発動時に用いられる。xv6ではTSSはprocess(struct proc
)に依存するので、switchuvm
で都度TSS Descriptorを設定する必要がある。 c.f) Figure 7-3.- Base address([63:56], [39:32], [31:16]): TSSの先頭アドレスを指定する。
segment register: %cs(code segment), %ds(data segment), %ss(stack segment) registerなど(optionalで%es, %fs, %gs)。xv6では、
%cs
はljmp $(SEG_KCODE<<3), $start32
で定めている。その他のregisterはbootasm.Sのstart32
以降で定めている。- segment selector[15:3]: Segment Registerのhidden part(16bit identifier). GDTの先頭ポインタからの
offset[byte]>>3
を指定する c.f) Figure 3-6 and Figure3-7
- segment selector[15:3]: Segment Registerのhidden part(16bit identifier). GDTの先頭ポインタからの
%cr3
register:Page Directory Base Register(PDBR)
とも言う。page directoryの先頭アドレスを指定する。xv6ではswitchkvm
とswitchuvm
で使用されてるIDT(Interrupt Descriptor Table): Interruptの番号に応じてdispatchされる
- IDTR: IDTの先頭ポインタを登録するためのregister (
lidt
命令でロード) c.f) Figure 2-6 gate descriptor: IDTのentry. c.f.) Figure 6-2
- Interrupt Gate Descriptor: 0~31はSystemで定められてるInterrupt. c.f) Section 6.15
- DPL([46:45]): xv6では3(DPL_USER)としている
- Segment Selector([31:16]): GDTRからのoffset[byte]。これの設定に応じてDescriptorのCPLが定められる
- Offset([63:48], [15:0]): interruptが発生したときのentrypointを設定する。
- Trap gate Descriptor: xv6では64(T_SYSCALL)のentryで指定されてる。
- DPL([46:45]): xv6では0(DPL_KERNEL)としている
- Segment Selector[31:16]: GDTRからのoffset[byte]。これの設定に応じてDescriptorのCPLが定められる
- Offset([63:48], [15:0]): interruptが発生したときのentrypointを設定する。
- Call Gate Descriptor: linux, xv6では未使用
- Task Gate Descriptor: linux, xv6では未使用。linuxではDouble Fault Exception(#8)の時のみ使用される。
- Interrupt Gate Descriptor: 0~31はSystemで定められてるInterrupt. c.f) Section 6.15
Local Descriptor Table (LDT): xv6では未使用。linux2.6では、GDTの内部にあって、すべてのprocessで共有されるものとして使用されてる
準備
GDTとIDT周りの説明をxv6を例にとって行いたいが、ややわかりにくい事柄を先に処理してしまう。馴染みがなければ、一旦、「xv6でのGDT, IDT」の節まで飛ばして良いと思う。
struct taskstateとstruct trapframeの違い
両者はややわかりにくいので、違いを簡単に述べる。
- struct trapframe: processのkernel stack内にある構造体。trapret内の
iret
命令で使われる - struct taskstate: struct cpuの内部にある構造体で、TSSのこと。int
$T_SYCSCALL
発動時にTR(Task Register) -> TSS Descriptor -> TSSとたどることでTSSを発見する。TSSにはkernel stackのpointerであるesp0があるので、esp0を%espにロードすることで、kernel landでの作業ができる。
switchuvm
switchkvmは%cr3 register(Page Directory Base Register(PDBR))にpage directoryの先頭アドレスをさせるだけの処理だが、 switchuvmは%cr3 registerの登録の前に以下のことを行う:
- TSS Descriptorを(再)構成
- TSSを(再)設定(ss0はGDTのSEG_KDATA、esp0はproc->kstackとする)
- TRをセット
switchuvmはuser landからkernel landへの移動の際の道標的な役割を果たす関数である。user landでint $T_SYSCALL
したときに、kernel landに移り、%espもkernel stack上のポインタ(proc->kstack)を指し示す。(一方、kernel land->user landへの移行はiret
命令を用いる。)
switchkvmは初期のboot時に使っていたentrypgdirからkpgdirへの移行をするためだけに用いられるのであんまり気にしなくて良い。kpgdirはsetupkvm
により作成される
その他補足
- GDTは各CPUごとに存在する。xv6では
struct cpu
にstruct segdesc gdt[NSEGS]
というメンバがあるのがそれ。
xv6でのGDT, IDT
- swtchまでは、 https://qiita.com/knknkn1162/items/0bc9afc3ae304590e16c#switching の部分までを読めばわかるかな、と思う。以下、最低限必要そうな部分のみをピックアップする:
main: seginit: - GDTに segment descriptor(4つ: SEG_KCODE, SEG_KDATA, SEG_UCODE, SEG_UDATA)を設定 - GDTRを設定 tvinit: - IDTのすべてのentry(0~255)にinterrupt gate descriptorを設定(DPL: 3, CPL: 0) - IDTに、T_SYSCALL(64) trap gate descriptorを設定(DPL: 0, CPL: 0) user_init: allocproc: - ptableから未使用のprocAを選ぶ - procA->kstackの領域(kstackAとする)をkallocで確保 - inituvm: initcode.Sを実行するためのユーザー領域(ustack_initcodeとする)をkallocで確保(mem)してmappagesでVirt(mem)-Phys(0)マッピング - procA->tf(trapframeA)を埋める(`p->tf->eip = 0`にしてる) - procA->state <~ RUNNABLEに mpmain->scheduler: - RUNNABLEのprocを選ぶ。procAが選ばれるとする - switchuvm: TSS descriptor, TSS(cpu->ts)の設定(TSS_Aとする), TRの設定, CR3の設定 - swtch: 作業領域をkstackAに移す。`ret`で%eipがforkretに移動
- forkretからinitまで
forkret: inodeの初期化(superblockとlogを読み取り) trapret: - trapframeAに格納されていたregisterを復元。 - iretでユーザーランドに降格する。`p->tf->eip = 0`としていたので、ustack_initcodeに飛ぶ initcode.S: int $T_SYSCALL: (hardware): - fetch the n'th descriptor from the IDT, where n is the arguement of `int` - check that CPL in %cs(in IDT): 0 is equal to or less than DPL: 0 => OK - save %esp and %ss in CPU-internal registers only if PL < CPL - load %ss and %sep from TSS(TSS_A) - push %ps, %esp, %eflags, %cs, %eip - clear the IF(Interrupt enable flag) bit in %eflags - set %cs and %eip to the values in the TSS. %eip register tells the computer where to go next to execute the next command and controls the flow of a program. vector60: - pushl $0, which is errcode - push $60, which is trapno - jmp alltraps alltraps: - struct trapframe(trapframeAとする)を構成 trap->syscall: - trapframeAから`curproc->tf->eax`は`SYS_exec`なので、sys_execを実行 sys_exec: exec("init", {"init", NULL})を実行 exec: - namei: path(char*)からinode(inode*)を取得 - readi: inodeからELF headerとELF program header抜き出す - loaduvm: program segmentをpgdirに反映(mapping) - myproc()->tf(trapframeBとする) を構成 (curproc->tf->eip = elf.entry[init programのentry point]) - switchuvm: TSS descriptor, TSS(cpu->ts)の設定(TSS_Bとする), TRの設定, CR3の設定 - Return falls through to trapret... - stack pointerをpopして、trapframeAを崩す - `iret`命令呼び出し。trapframeBから、%eipは[init programのentry point]となる。user landに降格。 init.c->main: openとかforkとかexecとかで`int $T_SYSCALL`が都度呼ばれる(usys.Sを参照)
最後の部分のinit processは基本的にはuser landだが、open, fork, execなどが呼ばれると、int $T_SYSCALL
が呼ばれ、kernel landで作業することになる。
TSS_Bで定めたkernel stack(proc->kstack)上でstack pointerが動き回る。