GDTとIDT周辺の理解(xv6を例に)

Motivation

Understanding the Linux Kernelを読もうとしたけど、生半可な気持ちでは臨めなさそうなので、xv6で理解の確認しつつ、linux kernel 2.6ではどうなの?という把握をすれば良さそう。

そこで、Understanding the Linux KernelのChapter 2: Memory Addressingに備えてGDTとかIDTってなんやねん、っていうのを抑えておきたい。

Reference

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では、%csljmp $(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
  • %cr3 register: Page Directory Base Register(PDBR)とも言う。page directoryの先頭アドレスを指定する。xv6ではswitchkvmswitchuvmで使用されてる

  • 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)の時のみ使用される。
  • 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の登録の前に以下のことを行う:

  1. TSS Descriptorを(再)構成
  2. TSSを(再)設定(ss0はGDTのSEG_KDATA、esp0はproc->kstackとする)
  3. 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 cpustruct segdesc gdt[NSEGS]というメンバがあるのがそれ。

xv6でのGDT, IDT

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が動き回る。