ZigでOSを作るシリーズ
| Part1 基本 | Part2 ブート | Part3 割り込み | Part4 メモリ | Part5 プロセス | Part6 FS |
|---|---|---|---|---|---|
| ✅ Done | ✅ Done | 👈 Now | - | - | - |
はじめに
前回はx86_64のブートプロセスを実装した。
今回は割り込み処理を実装するよ。CPU例外、タイマー割り込み、キーボード入力などを処理できるようになる。
割り込み処理はOSの中でも特に泥くさい部分だけど、これが動くと感動するよ。頑張ろう!
割り込みの概要
┌────────────────────────────────────────────────────────────────┐
│ x86_64 割り込みシステム │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ デバイス │ ──→ │ PIC │ ──→ │ CPU │ │
│ │ (キーボード等)│ │ /APIC │ │ (割り込みベクタ) │ │
│ └─────────┘ └─────────┘ └────────┬────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ IDT │ │
│ │ (割り込み記述子) │ │
│ └────────┬────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ ISR (ハンドラ) │ │
│ └─────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
IDT(Interrupt Descriptor Table)
x86_64では256個の割り込みベクタが使える。
// idt.zig
const IdtEntry = packed struct {
offset_low: u16,
selector: u16,
ist: u8, // Interrupt Stack Table
type_attr: u8,
offset_mid: u16,
offset_high: u32,
reserved: u32,
};
const IdtPtr = packed struct {
limit: u16,
base: u64,
};
var idt: [256]IdtEntry = undefined;
var idt_ptr: IdtPtr = undefined;
// 割り込みタイプ
const INTERRUPT_GATE: u8 = 0x8E; // Present | DPL=0 | 64-bit Interrupt Gate
const TRAP_GATE: u8 = 0x8F; // Present | DPL=0 | 64-bit Trap Gate
const USER_INTERRUPT: u8 = 0xEE; // Present | DPL=3 | 64-bit Interrupt Gate
pub fn init() void {
// 全エントリをゼロで初期化
for (&idt) |*entry| {
entry.* = makeEntry(0, 0, 0);
}
// CPU例外ハンドラを設定(0-31)
setHandler(0, divisionError);
setHandler(1, debug);
setHandler(2, nmi);
setHandler(3, breakpoint);
setHandler(4, overflow);
setHandler(5, boundRangeExceeded);
setHandler(6, invalidOpcode);
setHandler(7, deviceNotAvailable);
setHandler(8, doubleFault);
setHandler(10, invalidTss);
setHandler(11, segmentNotPresent);
setHandler(12, stackSegmentFault);
setHandler(13, generalProtectionFault);
setHandler(14, pageFault);
setHandler(16, x87FloatingPoint);
setHandler(17, alignmentCheck);
setHandler(18, machineCheck);
setHandler(19, simdFloatingPoint);
// ハードウェア割り込み(IRQ 0-15 → ベクタ 32-47)
setHandler(32, timerInterrupt); // IRQ0: Timer
setHandler(33, keyboardInterrupt); // IRQ1: Keyboard
// IDTをロード
idt_ptr = .{
.limit = @sizeOf(@TypeOf(idt)) - 1,
.base = @intFromPtr(&idt),
};
asm volatile ("lidt (%[idt_ptr])"
:
: [idt_ptr] "r" (&idt_ptr),
);
}
fn makeEntry(handler: u64, selector: u16, type_attr: u8) IdtEntry {
return .{
.offset_low = @truncate(handler & 0xFFFF),
.selector = selector,
.ist = 0,
.type_attr = type_attr,
.offset_mid = @truncate((handler >> 16) & 0xFFFF),
.offset_high = @truncate(handler >> 32),
.reserved = 0,
};
}
fn setHandler(vector: u8, handler: *const fn () callconv(.Interrupt) void) void {
idt[vector] = makeEntry(@intFromPtr(handler), 0x08, INTERRUPT_GATE);
}
pub fn enableInterrupts() void {
asm volatile ("sti");
}
pub fn disableInterrupts() void {
asm volatile ("cli");
}
割り込みハンドラ
Zigではcallconv(.Interrupt)で割り込みハンドラを定義できる。
// handlers.zig
const vga = @import("vga.zig");
const pic = @import("pic.zig");
// 割り込みフレーム構造体
const InterruptFrame = extern struct {
rip: u64,
cs: u64,
rflags: u64,
rsp: u64,
ss: u64,
};
// CPU例外ハンドラ
pub fn divisionError() callconv(.Interrupt) void {
panic("Division Error (#DE)");
}
pub fn debug() callconv(.Interrupt) void {
// デバッグ例外は通常無視
}
pub fn nmi() callconv(.Interrupt) void {
panic("Non-Maskable Interrupt");
}
pub fn breakpoint() callconv(.Interrupt) void {
vga.print("[BREAK] Breakpoint hit\n");
}
pub fn overflow() callconv(.Interrupt) void {
panic("Overflow (#OF)");
}
pub fn boundRangeExceeded() callconv(.Interrupt) void {
panic("Bound Range Exceeded (#BR)");
}
pub fn invalidOpcode() callconv(.Interrupt) void {
panic("Invalid Opcode (#UD)");
}
pub fn deviceNotAvailable() callconv(.Interrupt) void {
panic("Device Not Available (#NM)");
}
pub fn doubleFault() callconv(.Interrupt) void {
panic("Double Fault (#DF)");
}
pub fn invalidTss() callconv(.Interrupt) void {
panic("Invalid TSS (#TS)");
}
pub fn segmentNotPresent() callconv(.Interrupt) void {
panic("Segment Not Present (#NP)");
}
pub fn stackSegmentFault() callconv(.Interrupt) void {
panic("Stack-Segment Fault (#SS)");
}
pub fn generalProtectionFault() callconv(.Interrupt) void {
panic("General Protection Fault (#GP)");
}
pub fn pageFault() callconv(.Interrupt) void {
// CR2にはページフォルトのアドレスが入っている
const fault_addr = asm volatile ("mov %%cr2, %[addr]"
: [addr] "=r" (-> u64),
);
_ = fault_addr;
panic("Page Fault (#PF)");
}
pub fn x87FloatingPoint() callconv(.Interrupt) void {
panic("x87 Floating-Point Exception (#MF)");
}
pub fn alignmentCheck() callconv(.Interrupt) void {
panic("Alignment Check (#AC)");
}
pub fn machineCheck() callconv(.Interrupt) void {
panic("Machine Check (#MC)");
}
pub fn simdFloatingPoint() callconv(.Interrupt) void {
panic("SIMD Floating-Point Exception (#XM)");
}
// パニック関数
fn panic(message: []const u8) noreturn {
vga.setColor(.Red, .Black);
vga.print("\n!!! KERNEL PANIC !!!\n");
vga.print(message);
vga.print("\n");
while (true) {
asm volatile ("cli; hlt");
}
}
PIC(Programmable Interrupt Controller)
IRQをCPUに届けるにはPICの設定が必要。
// pic.zig
const PIC1_COMMAND: u16 = 0x20;
const PIC1_DATA: u16 = 0x21;
const PIC2_COMMAND: u16 = 0xA0;
const PIC2_DATA: u16 = 0xA1;
const ICW1_INIT: u8 = 0x10;
const ICW1_ICW4: u8 = 0x01;
const ICW4_8086: u8 = 0x01;
// PICの再マッピング
// IRQ 0-7 → INT 32-39
// IRQ 8-15 → INT 40-47
pub fn init() void {
// ICW1: 初期化開始
outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);
outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);
wait();
// ICW2: 割り込みベクタオフセット
outb(PIC1_DATA, 32); // IRQ 0-7 → INT 32-39
outb(PIC2_DATA, 40); // IRQ 8-15 → INT 40-47
wait();
// ICW3: カスケード設定
outb(PIC1_DATA, 4); // IRQ2にスレーブPIC
outb(PIC2_DATA, 2); // スレーブPICはIRQ2
wait();
// ICW4: 8086モード
outb(PIC1_DATA, ICW4_8086);
outb(PIC2_DATA, ICW4_8086);
wait();
// 全IRQをマスク(無効化)
outb(PIC1_DATA, 0xFF);
outb(PIC2_DATA, 0xFF);
}
// 特定のIRQを有効化
pub fn enableIrq(irq: u8) void {
if (irq < 8) {
const mask = inb(PIC1_DATA) & ~(@as(u8, 1) << @truncate(irq));
outb(PIC1_DATA, mask);
} else {
const mask = inb(PIC2_DATA) & ~(@as(u8, 1) << @truncate(irq - 8));
outb(PIC2_DATA, mask);
// IRQ2(カスケード)も有効化
const master_mask = inb(PIC1_DATA) & ~@as(u8, 4);
outb(PIC1_DATA, master_mask);
}
}
// EOI(End Of Interrupt)送信
pub fn sendEoi(irq: u8) void {
if (irq >= 8) {
outb(PIC2_COMMAND, 0x20);
}
outb(PIC1_COMMAND, 0x20);
}
fn wait() void {
// I/Oポートへの書き込みで短い遅延
outb(0x80, 0);
}
fn outb(port: u16, value: u8) void {
asm volatile ("outb %[value], %[port]"
:
: [value] "{al}" (value),
[port] "N{dx}" (port),
);
}
fn inb(port: u16) u8 {
return asm volatile ("inb %[port], %[result]"
: [result] "={al}" (-> u8),
: [port] "N{dx}" (port),
);
}
タイマー割り込み
PIT(Programmable Interval Timer)を使ってタイマー割り込みを実装。
// pit.zig
const PIT_CHANNEL0: u16 = 0x40;
const PIT_COMMAND: u16 = 0x43;
const PIT_FREQUENCY: u32 = 1193182;
var tick_count: u64 = 0;
pub fn init(frequency: u32) void {
const divisor = PIT_FREQUENCY / frequency;
// チャンネル0、アクセスモード: lobyte/hibyte、モード3: 方形波
outb(PIT_COMMAND, 0x36);
// 分周値を設定
outb(PIT_CHANNEL0, @truncate(divisor & 0xFF));
outb(PIT_CHANNEL0, @truncate((divisor >> 8) & 0xFF));
}
pub fn getTicks() u64 {
return tick_count;
}
pub fn sleep(ms: u64) void {
const end = tick_count + ms;
while (tick_count < end) {
asm volatile ("hlt");
}
}
// タイマー割り込みハンドラ
pub fn timerInterrupt() callconv(.Interrupt) void {
tick_count += 1;
pic.sendEoi(0);
}
fn outb(port: u16, value: u8) void {
asm volatile ("outb %[value], %[port]"
:
: [value] "{al}" (value),
[port] "N{dx}" (port),
);
}
キーボード入力
PS/2キーボードからの入力を処理。
// keyboard.zig
const vga = @import("vga.zig");
const pic = @import("pic.zig");
const KEYBOARD_DATA: u16 = 0x60;
const KEYBOARD_STATUS: u16 = 0x64;
// スキャンコード→ASCII変換テーブル(US配列)
const scancode_to_ascii = [_]u8{
0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 8, // 0-14
'\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', // 15-28
0, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`', // 29-41
0, '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 0, // 42-54
'*', 0, ' ', // 55-57
};
// キーバッファ
var key_buffer: [256]u8 = undefined;
var buffer_start: usize = 0;
var buffer_end: usize = 0;
// 修飾キーの状態
var shift_pressed: bool = false;
var ctrl_pressed: bool = false;
var alt_pressed: bool = false;
pub fn init() void {
// キーボードを待機
while ((inb(KEYBOARD_STATUS) & 2) != 0) {}
// キーボードを有効化
outb(KEYBOARD_DATA, 0xF4);
}
// キーボード割り込みハンドラ
pub fn keyboardInterrupt() callconv(.Interrupt) void {
const scancode = inb(KEYBOARD_DATA);
// キーリリース(ビット7がセット)
if (scancode & 0x80 != 0) {
const released = scancode & 0x7F;
switch (released) {
0x2A, 0x36 => shift_pressed = false,
0x1D => ctrl_pressed = false,
0x38 => alt_pressed = false,
else => {},
}
} else {
// キープレス
switch (scancode) {
0x2A, 0x36 => shift_pressed = true,
0x1D => ctrl_pressed = true,
0x38 => alt_pressed = true,
else => {
if (scancode < scancode_to_ascii.len) {
var char = scancode_to_ascii[scancode];
if (char != 0) {
// Shiftで大文字に
if (shift_pressed and char >= 'a' and char <= 'z') {
char -= 32;
}
// バッファに追加
buffer_push(char);
// エコー出力
vga.putChar(char);
}
}
},
}
}
pic.sendEoi(1);
}
fn buffer_push(char: u8) void {
const next = (buffer_end + 1) % key_buffer.len;
if (next != buffer_start) {
key_buffer[buffer_end] = char;
buffer_end = next;
}
}
pub fn getChar() ?u8 {
if (buffer_start == buffer_end) {
return null;
}
const char = key_buffer[buffer_start];
buffer_start = (buffer_start + 1) % key_buffer.len;
return char;
}
pub fn readLine(buffer: []u8) usize {
var i: usize = 0;
while (i < buffer.len - 1) {
// キー入力を待つ
while (getChar() == null) {
asm volatile ("hlt");
}
const char = getChar().?;
if (char == '\n') {
buffer[i] = 0;
return i;
} else if (char == 8 and i > 0) { // Backspace
i -= 1;
} else {
buffer[i] = char;
i += 1;
}
}
buffer[i] = 0;
return i;
}
fn inb(port: u16) u8 {
return asm volatile ("inb %[port], %[result]"
: [result] "={al}" (-> u8),
: [port] "N{dx}" (port),
);
}
fn outb(port: u16, value: u8) void {
asm volatile ("outb %[value], %[port]"
:
: [value] "{al}" (value),
[port] "N{dx}" (port),
);
}
簡易シェル
キーボード入力を使って簡単なシェルを実装。
// shell.zig
const vga = @import("vga.zig");
const keyboard = @import("keyboard.zig");
const pit = @import("pit.zig");
pub fn run() void {
vga.print("\nWelcome to Zig OS Shell!\n");
vga.print("Type 'help' for available commands.\n\n");
while (true) {
vga.print("zigOS> ");
var buffer: [256]u8 = undefined;
const len = keyboard.readLine(&buffer);
if (len > 0) {
processCommand(buffer[0..len]);
}
}
}
fn processCommand(cmd: []const u8) void {
if (strEql(cmd, "help")) {
vga.print("\nAvailable commands:\n");
vga.print(" help - Show this help\n");
vga.print(" clear - Clear screen\n");
vga.print(" time - Show uptime\n");
vga.print(" info - System info\n");
vga.print(" panic - Trigger kernel panic\n");
vga.print("\n");
} else if (strEql(cmd, "clear")) {
vga.clear();
} else if (strEql(cmd, "time")) {
const ticks = pit.getTicks();
vga.print("Uptime: ");
printNumber(ticks / 1000);
vga.print(" seconds\n");
} else if (strEql(cmd, "info")) {
vga.print("\nZig OS v0.1.0\n");
vga.print("Architecture: x86_64\n");
vga.print("Written in Zig\n\n");
} else if (strEql(cmd, "panic")) {
// テスト用パニック
const ptr: *u8 = @ptrFromInt(0xDEADBEEF);
_ = ptr.*;
} else {
vga.print("Unknown command: ");
vga.print(cmd);
vga.print("\n");
}
}
fn strEql(a: []const u8, b: []const u8) bool {
if (a.len != b.len) return false;
for (a, b) |ca, cb| {
if (ca != cb) return false;
}
return true;
}
fn printNumber(n: u64) void {
var buf: [20]u8 = undefined;
var i: usize = buf.len;
var num = n;
if (num == 0) {
vga.putChar('0');
return;
}
while (num > 0) {
i -= 1;
buf[i] = @truncate('0' + (num % 10));
num /= 10;
}
for (buf[i..]) |c| {
vga.putChar(c);
}
}
メインの更新
// boot.zig - メイン関数の更新
const gdt = @import("gdt.zig");
const idt = @import("idt.zig");
const pic = @import("pic.zig");
const pit = @import("pit.zig");
const keyboard = @import("keyboard.zig");
const shell = @import("shell.zig");
const vga = @import("vga.zig");
export fn kernel_main() callconv(.C) noreturn {
// 画面クリア
vga.clear();
vga.print("Zig OS Kernel booting...\n\n");
// GDT初期化
vga.print("[OK] GDT initialized\n");
gdt.init();
// IDT初期化
vga.print("[OK] IDT initialized\n");
idt.init();
// PIC初期化
vga.print("[OK] PIC initialized\n");
pic.init();
// タイマー初期化(1000Hz = 1ms)
vga.print("[OK] PIT initialized (1000Hz)\n");
pit.init(1000);
// キーボード初期化
vga.print("[OK] Keyboard initialized\n");
keyboard.init();
// IRQを有効化
pic.enableIrq(0); // Timer
pic.enableIrq(1); // Keyboard
// 割り込み有効化
vga.print("[OK] Interrupts enabled\n\n");
idt.enableInterrupts();
// シェル起動
shell.run();
// ここには来ない
while (true) {
asm volatile ("hlt");
}
}
実行結果
QEMUで実行すると:
Zig OS Kernel booting...
[OK] GDT initialized
[OK] IDT initialized
[OK] PIC initialized
[OK] PIT initialized (1000Hz)
[OK] Keyboard initialized
[OK] Interrupts enabled
Welcome to Zig OS Shell!
Type 'help' for available commands.
zigOS> help
Available commands:
help - Show this help
clear - Clear screen
time - Show uptime
info - System info
panic - Trigger kernel panic
zigOS> time
Uptime: 5 seconds
zigOS>
まとめ
今回実装したこと:
| コンポーネント | 機能 |
|---|---|
| IDT | 256個の割り込みエントリ |
| CPU例外 | Division Error, Page Fault等 |
| PIC | IRQ → 割り込みベクタ変換 |
| PIT | 1msタイマー割り込み |
| キーボード | PS/2スキャンコード処理 |
| シェル | 簡易コマンドインターフェース |
┌──────────────────────────────────────────────────────────────┐
│ 現在のカーネル構造 │
├──────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Shell │ │
│ └────────────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌─────────────┬──────────────┼──────────────┬─────────────┐ │
│ │ Keyboard │ Timer │ VGA │ Serial │ │
│ └──────┬──────┴───────┬──────┴───────┬──────┴──────┬──────┘ │
│ │ │ │ │ │
│ ┌──────┴──────────────┴──────────────┴─────────────┴──────┐ │
│ │ Interrupt Handling (IDT + PIC) │ │
│ └─────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴───────────────────────────┐ │
│ │ GDT / Paging │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
次回はメモリ管理を実装するよ。物理メモリアロケータとページング管理で、動的メモリ確保ができるようになる。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!