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は「表示するだけ」で、何も入力を受け付けません。
キーボード押しても無反応...かなしみ。
今回は 割り込み処理 を実装して、キーボード入力ができるようにします!
目次
- はじめに
- 目次
- 割り込みって何?
- 必要なクレートを追加
- フィーチャーフラグ
- IDTを作る
- PICを初期化する
- タイマー割り込み
- キーボード割り込み
- main.rsの更新
- 動作確認
- 割り込みの流れ図
- まとめ
割り込みって何?
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());
}
}
処理の流れ
- ポート0x60からスキャンコードを読む
-
pc-keyboardクレートで文字に変換 - 画面に表示
- 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)はページングを実装して、仮想メモリの世界に入ります。メモリ管理は複雑だけど、ここを超えればヒープが使える...!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!