panda's tech note

Advent Calendar 2018: advos

Day 16: GDTとIDT

一時的なGDTと空のIDTは7日目に設定しましたが,正しくカーネルで使用するGDT/IDTはこれまで設定してきませんでした。今日はGDTとIDTを設定しようと思います。

メモリ領域

GDTとIDTは,1 MiB以下の領域に8 KiBずつ領域を予約していましたので,以下の領域を使います。

Start End Description
00000500 00007bff ブートローダのスタック領域
00007c00 00007dff MBR
00007e00 00007fff 未使用
00008000 00008fff ブート情報(ドライブ番号など)
00009000 0000cfff ブートモニタ (16 KiB)
0000d000 0000ffff 未使用 (ブートモニタ拡張用に予約)
00010000 0002ffff カーネル (最大128 KiB)
00030000 00067fff 予約
00060000 0006ffff グローバル変数領域
00068000 00068fff コアゾーン管理用領域
00069000 0006ffff カーネルのベースページテーブル
00070000 00073fff trampoline (16 KiB)
00074000 00075fff GDT (8 KiB)
00076000 00077fff IDT (8 KiB
00078000 00078fff 予約
00079000 0007ffff ロングモード用ページテーブル (24 KiB = 6 * 4 KiB以上必要)

GDT

これまで使用してきたGDTを当面は使用しても問題ないですが,すべてのエントリがRing 0(CPL=0)なので,ユーザランドの処理(Ring 3)には別のGDTエントリが必要です。GDTのエントリは通常はどのような順序でも良いですが,システムコール処理(syscall/sysret)では,データセレクタはコードセレクタ+8で自動設定されるため,以下のようにコード用とデータ用のGDTエントリは連続した場所に設置する必要があります。

    /* Null descriptor */
    gdt_setup_desc_null(&gdt[0]);
    /* Code and data descriptor for each ring */
    gdt_setup_desc(&gdt[1], 0, 0xfffff, code, 0, 1, 0, 1); /* Ring 0 code */
    gdt_setup_desc(&gdt[2], 0, 0xfffff, data, 0, 1, 0, 1); /* Ring 0 data */
    gdt_setup_desc(&gdt[3], 0, 0xfffff, code, 3, 0, 1, 1); /* Ring 3 code */
    gdt_setup_desc(&gdt[4], 0, 0xfffff, data, 3, 0, 1, 1); /* Ring 3 data */
    gdt_setup_desc(&gdt[5], 0, 0xfffff, code, 3, 1, 0, 1); /* Ring 3 code */
    gdt_setup_desc(&gdt[6], 0, 0xfffff, data, 3, 1, 0, 1); /* Ring 3 data */

これまで設定してきたGDTのCPL=3のものを追加しただけなので,詳しくはsrc/kernel/arch/x86_64/desc.cを参照してください。

IDT

IDTには,次の3種類のディスクリプタエントリがあります。

  • 割り込みゲート:割り込みサービスルーチン (ISR: Interrupt Service Routine) を指定するためのゲート。割り込みハンドラ内では割り込みが自動的に無効化される(EFLAGSのIFがクリアされる)。
  • トラップゲート:割り込みや例外を扱うハンドラを設定するゲート。割り込みゲートとほぼ同じだが,割り込みが自動的に無効化されない。
  • タスクゲート:GDTのTask State Segment (TSS)を指し示すゲート。TSSを使ったハードウェアタスクスイッチをする場合には使用するが,最近のOSでは通常はソフトウェアタスクスイッチをするため,使用しないことが多い(と思われる)。

IDTは,以下のようにすべての割り込みベクタに対してintr_null割り込みハンドラを設定して初期化します。

struct idtr *
idt_init(void)
{
    struct idtr *idtr;
    int i;

    /* Setup the interrupt gates */
    for ( i = 0; i < IDT_NR; i++ ) {
        idt_setup_intr_gate(i, &intr_null);
    }

    idtr = (struct idtr *)(IDT_ADDR + sizeof(struct idt_gate_desc) * IDT_NR);
    idtr->base = IDT_ADDR;
    idtr->size = IDT_NR * sizeof(struct idt_gate_desc) - 1;

    return idtr;
}

これに対して,idt_setup_intr_gate()関数またはidt_setup_trap_gate()関数で割り込みゲートまたはトラップゲートを設定します。以下の例では,I/O APICと組み合わせて,IRQ 1のキーボード割り込みに対して割り込みハンドラを設定します。

I/O APIC

I/O APICのアドレスは通常0xfec00000からです。前回0xfee00000-0xffffffffをリニアマッピングしましたが,今回はさらに2 MiBページ1つ分前までリニアマッピングします。

I/O APICを使う前に,以下のようにIntel 8259 PICを無効化します。

void
ioapic_init(void)
{
    /* Ensure to disable i8259 PIC */
    outb(0xa1, 0xff);
    outb(0x21, 0xff);
}

外部からの割り込みはI/O APICを通してLocal APICにルーティングされます。

I/O APICのレジスタは,MMIOレジスタであるIOREGSELレジスタとIOWINレジスタを通じてアクセスします。これらのMMIOレジスタはI/O APICのベースアドレスから,それぞれ,オフセット0x00 (IOREGSEL),0x10 (IOWIN)でアクセスできます。IOREGSELは,アクセスするレジスタを選択するセレクタです。IOWINはデータレジスタです。つまり,I/O APICのレジスタに書き込むには,IOREGSELに書き込み対象のレジスタのインデックスを書き込んだ後,IOWINに書き込むデータを書き込みます。

以下のコードは,I/O APICへの割り込みをLocal APICにルーティングする設定を行います。I/O APICへの割り込み番号(IRQ)を第二引数,ルーティング先のLocal APICの割り込み番号を指定します。上述したようにIOREGSELでレジスタを選択した後に,IOWINに値を書き込むため,メモリバリアを入れてコンパイラの最適化でコードの実行順序が変わらないようにしています。

void
ioapic_map_intr(uint64_t intvec, uint64_t tbldst, uint64_t ioapic_base)
{
    uint64_t val;

    /*
     * 63:56    destination field
     * 16       interrupt mask (1: masked for edge sensitive)
     * 15       trigger mode (1=level sensitive, 0=edge sensitive)
     * 14       remote IRR (R/O) (1 if local APICs accept the level interrupts)
     * 13       interrupt input pin polarity (0=high active, 1=low active)
     * 12       delivery status (R/O)
     * 11       destination mode (0=physical, 1=logical)
     * 10:8     delivery mode
     *          000 fixed, 001 lowest priority, 010 SMI, 011 reserved
     *          100 NMI, 101 INIT, 110 reserved, 111 ExtINT
     * 7:0      interrupt vector
     */
    val = intvec;

    sfence();
    *(uint32_t *)(ioapic_base + 0x00) = tbldst * 2 + 0x10;
    sfence();
    *(uint32_t *)(ioapic_base + 0x10) = (uint32_t)val;
    sfence();
    *(uint32_t *)(ioapic_base + 0x00) = tbldst * 2 + 0x10 + 1;
    sfence();
    *(uint32_t *)(ioapic_base + 0x10) = (uint32_t)(val >> 32);
}

I/O APICのIOREDTBLレジスタが割り込みルーティングを設定します。IOREDTBL(下位32ビットのIOREDTBL_LOWおよび上位32ビットのIOREDTBL_HIGH)は割り込み番号Nに対して,

IOREDTBL_LOW(N) = 0x10 + N * 2
IOREDTBL_HIGH(N) = 0x11 + N * 2

で決定されます。IOREDTBLの各ビットの意味は上記コードのコメントの通りですが,今回はinterrupt vector(Local APICの割り込み番号)のフィールド以外は0にします。destination fieldも0にしているため,ここでせ設定した割り込みはBSPのLocal APICにルーティングされます。

今回は試しにキーボード割り込み(IRQ 1)をLocal APICの割り込みベクタ0x21に割り当てます。また,この割り込みベクタに対して,src/kernel/arch/x86_64/asm.Sで定義したintr_irqを割り込みハンドラとして設定します。その設定はsrc/kernel/arch/x86_64/arch.cのうち,以下の部分です。

    ioapic_map_intr(0x21, 1, acpi->ioapic_base);

    /* Test interrupt */
    idt_setup_intr_gate(0x21, intr_irq1);

この割り込みハンドラは以下のように80x25のディスプレイの右下に入力したキーを表示するプログラムです。

/* Interrupt handler for IRQ1 */
_intr_irq1:
    pushq	%rax
    pushq	%rcx
    pushq	%rdx
    /* Print the key to the bottom right */
    xorl	%eax,%eax
    inb	$0x60,%al
    testb	$0x80,%al
    jnz	1f		/* Key released */
    movl	$keymap_base,%edx	/* Use base keymap */
    addl	%eax,%edx
    movb	(%edx),%al
    movb	$0x07,%ah
    movw	%ax,(0xb8000+80*25*2-2)
1:
    /* APIC EOI */
    movq	$MSR_APIC_BASE,%rcx
    rdmsr			/* Read APIC info to [%edx:%eax]; N.B., higer */
                /*  32 bits of %rax and %rdx are cleared */
                /*  bit [35:12]: APIC Base, [11]: EN */
                /*  [10]: EXTD, and [8]:BSP */
    shlq	$32,%rdx
    addq	%rax,%rdx
    andq	$0xfffffffffffff000,%rdx        /* APIC Base */
    movl	$0,0x0b0(%rdx)       /* EOI */
    popq	%rdx
    popq	%rcx
    popq	%rax
    iretq

このコードのうち

    /* APIC EOI */
    movq	$MSR_APIC_BASE,%rcx
    rdmsr			/* Read APIC info to [%edx:%eax]; N.B., higer */
                /*  32 bits of %rax and %rdx are cleared */
                /*  bit [35:12]: APIC Base, [11]: EN */
                /*  [10]: EXTD, and [8]:BSP */
    shlq	$32,%rdx
    addq	%rax,%rdx
    andq	$0xfffffffffffff000,%rdx        /* APIC Base */
    movl	$0,0x0b0(%rdx)       /* EOI */

は,Intel 8259 PICのEnd-of-Interruptの通知と同様に,APICに割り込みハンドラの終了を通知する処理です。

まとめと明日の予定

今日はGDTとIDTを設定し,I/O APICと組み合わせてキーボード割り込みハンドラを実装しました。明日は,Ring 3でコードを実行する予定です。24日までにタスクスイッチをできたらいいな……。