4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustで作るx86_64 自作OS入門シリーズ
Part1【環境構築】 | Part2【no_std】 | Part3【Hello World】| Part4【VGA】 | Part5【割り込み】 | Part6【ページング】 | Part7【ヒープ】 | Part8【マルチタスク】| Part9【ファイルシステム】 | Part10【ELFローダー】 | Part11【シェル】 | Part12【完結】

はじめに

Part4まででVGAテキストモードは完成しました。でも今のOSは「表示するだけ」で、何も入力を受け付けません。

キーボード押しても無反応...かなしみ。

今回は 割り込み処理 を実装して、キーボード入力ができるようにします!

目次

割り込みって何?

CPUが「今やってる処理を中断して、別の処理をする」仕組みです。

[通常の処理] → キーボード押された! → [割り込みハンドラ] → [通常の処理に戻る]

大きく分けて2種類:

種類 発生元
例外(Exception) CPU自身 ゼロ除算、ページフォルト
外部割り込み ハードウェア キーボード、タイマー

x86_64では IDT(Interrupt Descriptor Table) というテーブルに「どの割り込みが来たらどの関数を呼ぶか」を登録しておきます。

必要なクレートを追加

# Cargo.toml
[dependencies]
bootloader = { version = "0.9.23", features = ["map_physical_memory"] }
spin = "0.9"
lazy_static = { version = "1.4", features = ["spin_no_std"] }
x86_64 = "0.15"
pc-keyboard = "0.7"
pic8259 = "0.11"

追加したクレート:

  • x86_64: IDTとかポート操作とか
  • pc-keyboard: キーボードのスキャンコードを文字に変換
  • pic8259: PIC(割り込みコントローラ)の制御
cargo build
    Updating crates.io index
     Locking 8 packages to latest compatible versions
      Adding bit_field v0.10.3
      Adding bitflags v2.10.0
      Adding pc-keyboard v0.7.0
      Adding pic8259 v0.11.0
      Adding x86_64 v0.15.4
   Compiling x86_64 v0.15.4
   Compiling pic8259 v0.11.0
   Compiling pc-keyboard v0.7.0
   Compiling my-os v0.1.0 (C:\Users\Aqua\Documents\Qiita\my-os)
    Finished `dev` profile [optimized + debuginfo] target(s) in 5.23s

フィーチャーフラグ

割り込みハンドラを書くにはnightly機能が必要:

// src/main.rs
#![no_std]
#![no_main]
#![feature(abi_x86_interrupt)]  // これ追加!

IDTを作る

src/interrupts.rsを新規作成:

// src/interrupts.rs
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use crate::{println, print};

lazy_static::lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        idt.breakpoint.set_handler_fn(breakpoint_handler);
        idt.double_fault.set_handler_fn(double_fault_handler);
        idt[InterruptIndex::Timer.as_u8()].set_handler_fn(timer_interrupt_handler);
        idt[InterruptIndex::Keyboard.as_u8()].set_handler_fn(keyboard_interrupt_handler);
        idt
    };
}

pub fn init_idt() {
    IDT.load();
}

割り込みインデックス

PICからの割り込みは32番から始まります(0〜31はCPU例外用):

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
    Timer = PIC_1_OFFSET,     // 32
    Keyboard,                  // 33
}

impl InterruptIndex {
    fn as_u8(self) -> u8 {
        self as u8
    }
}

pub const PIC_1_OFFSET: u8 = 32;
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;

例外ハンドラ

// ブレークポイント(デバッグ用)
extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
    println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
}

// ダブルフォルト(例外ハンドラがエラーになったとき)
extern "x86-interrupt" fn double_fault_handler(
    stack_frame: InterruptStackFrame,
    _error_code: u64,
) -> ! {
    panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame);
}

extern "x86-interrupt"が割り込みハンドラ用の呼び出し規約。これを使うと、レジスタの退避とかを自動でやってくれます。

PICを初期化する

PIC(Programmable Interrupt Controller)はハードウェア割り込みを管理するチップ。昔のPCには2つあって、それがカスケード接続されてます。

use pic8259::ChainedPics;
use spin::Mutex;

pub static PICS: Mutex<ChainedPics> =
    Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });

pub fn init_pics() {
    unsafe {
        PICS.lock().initialize();
    }
}

タイマー割り込み

extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFrame) {
    // print!(".");  // 有効にすると画面がドットだらけになる
    
    unsafe {
        PICS.lock().notify_end_of_interrupt(InterruptIndex::Timer.as_u8());
    }
}

重要: notify_end_of_interruptを忘れると次の割り込みが来なくなる!これで1時間溶かしました...

キーボード割り込み

いよいよ本命!

extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    use x86_64::instructions::port::Port;
    use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};
    use spin::Mutex;

    lazy_static::lazy_static! {
        static ref KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> =
            Mutex::new(Keyboard::new(
                ScancodeSet1::new(),
                layouts::Us104Key,
                HandleControl::Ignore
            ));
    }

    let mut keyboard = KEYBOARD.lock();
    let mut port = Port::new(0x60);  // キーボードデータポート

    let scancode: u8 = unsafe { port.read() };
    if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
        if let Some(key) = keyboard.process_keyevent(key_event) {
            match key {
                DecodedKey::Unicode(character) => print!("{}", character),
                DecodedKey::RawKey(key) => print!("{:?}", key),
            }
        }
    }

    unsafe {
        PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}

処理の流れ

  1. ポート0x60からスキャンコードを読む
  2. pc-keyboardクレートで文字に変換
  3. 画面に表示
  4. EOI(End Of Interrupt)を送る

main.rsの更新

#![no_std]
#![no_main]
#![feature(abi_x86_interrupt)]

mod vga;
mod interrupts;

// ... 省略 ...

fn kernel_main(_boot_info: &'static BootInfo) -> ! {
    serial_init();
    serial_print("=== My OS Booting ===\n");
    
    vga::init();
    serial_print("VGA initialized\n");
    
    interrupts::init_idt();
    serial_print("IDT initialized\n");
    
    interrupts::init_pics();
    serial_print("PICs initialized\n");
    
    // 割り込みを有効化!
    x86_64::instructions::interrupts::enable();
    serial_print("Interrupts enabled\n");
    
    println!("Hello, OS World!");
    println!("Interrupts enabled! Try pressing keys...");
    
    loop {
        x86_64::instructions::hlt();  // 割り込みが来るまで省電力待機
    }
}

動作確認

cargo bootimage
WARNING: `CARGO_MANIFEST_DIR` env variable not set
Building kernel
warning: method `as_usize` is never used
  --> src\interrupts.rs:32:8
   |
27 | impl InterruptIndex {
   | ------------------- method in this implementation
...
32 |     fn as_usize(self) -> usize {
   |        ^^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

warning: `my-os` (bin "my-os") generated 1 warning
    Finished `dev` profile [optimized + debuginfo] target(s) in 0.17s
Building bootloader
    Finished `release` profile [optimized + debuginfo] target(s) in 0.05s
Created bootimage for `my-os` at `target\x86_64-my_os\debug\bootimage-my-os.bin`

警告は無視してOK(as_usizeを定義したけど結局as_u8しか使わなかった)。

qemu-system-x86_64 -drive format=raw,file=target\x86_64-my_os\debug\bootimage-my-os.bin -serial file:serial.log

シリアルログ:

=== My OS Booting ===
VGA initialized
IDT initialized
PICs initialized
Interrupts enabled
println! test complete
Entering infinite loop...

そしてQEMUウィンドウでキーボードを押すと...

文字が出る!!!

Hello, OS World!
Interrupts enabled! Try pressing keys...
hello world

めちゃくちゃ感動した。自分で作ったOSでキーボードが動くって、なんかこう...グッとくるものがありますね。

割り込みの流れ図

┌─────────────┐
│ キーボード   │ ← 物理的に押す
└──────┬──────┘
       │ IRQ1
       ▼
┌─────────────┐
│    PIC      │ ← 割り込みを管理
└──────┬──────┘
       │ ベクタ33
       ▼
┌─────────────┐
│    CPU      │ ← 処理を中断
└──────┬──────┘
       │
       ▼
┌─────────────┐
│    IDT      │ ← ハンドラを探す
└──────┬──────┘
       │
       ▼
┌─────────────────────────┐
│ keyboard_interrupt_handler │ ← 私たちのコード!
└─────────────────────────┘

まとめ

Part5では以下を達成しました:

  • IDT(割り込み記述子テーブル)の作成
  • PIC(割り込みコントローラ)の初期化
  • 例外ハンドラ(ブレークポイント、ダブルフォルト)
  • タイマー割り込み
  • キーボード入力が動いた!

次回(Part6)はページングを実装して、仮想メモリの世界に入ります。メモリ管理は複雑だけど、ここを超えればヒープが使える...!

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

4
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?