4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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                          │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

次回はメモリ管理を実装するよ。物理メモリアロケータとページング管理で、動的メモリ確保ができるようになる。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?