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日までにタスクスイッチをできたらいいな……。