Rustで作るx86_64 自作OS入門シリーズ
Part1【環境構築】 | Part2【no_std】 | Part3【Hello World】| Part4【VGA】 | Part5【割り込み】 | Part6【ページング】 | Part7【ヒープ】 | Part8【マルチタスク】| Part9【ファイルシステム】 | Part10【ELFローダー】 | Part11【シェル】 | Part12【完結】
はじめに
「自作OS」って響き、ワクワクしませんか?
私はものすごくワクワクしました。で、実際に作り始めたら...
環境構築だけで3日溶けました。
この記事では、私が躓いたポイントを全部晒しながら、Rustでx86_64向けのOSを作る環境を構築していきます。
目次
なぜRustで自作OSなのか
自作OSといえばCが定番ですよね。でも私はRustを選びました。理由は:
- メモリ安全性: OSでのメモリバグは致命的。Rustなら多くのバグをコンパイル時に防げる
- ゼロコスト抽象化: パフォーマンスを犠牲にせずに高レベルな抽象化ができる
- モダンなツールチェーン: Cargoが神
- 勉強になる: no_stdの世界を知るとRustへの理解が深まる
あと単純に「RustでOS作った」って言いたかった。
必要なツールのインストール
Rust(nightly版)
自作OSにはnightly版のRustが必要です。一部の不安定な機能を使うので。
rustup override set nightly
rustup component add rust-src llvm-tools-preview
info: override toolchain for 'C:\Users\Aqua\Documents\Qiita\my-os' set to 'nightly-x86_64-pc-windows-msvc'
info: component 'rust-src' is up to date
info: component 'llvm-tools' for target 'x86_64-pc-windows-msvc' is up to date
なぜnightlyが必要?
-
#![no_std]: 標準ライブラリなしでコンパイル -
#![no_main]: 通常のmain関数を使わない -
asm!: インラインアセンブリ - その他いくつかの不安定な機能
QEMU
エミュレータがないとOSのテストができません。実機でデバッグとか地獄なので...
Windows の場合は公式サイトからインストーラーをダウンロード:
https://www.qemu.org/download/#windows
qemu-system-x86_64 --version
QEMU emulator version 9.2.0 (v9.2.0-12054-g923cf646f4)
Copyright (c) 2003-2024 Fabrice Bellard and the QEMU Project developers
bootimage
cargo install bootimage
Updating crates.io index
Installing bootimage v0.10.3
Compiling bootimage v0.10.3
Finished release [optimized] target(s) in 15.32s
Installed ~/.cargo/bin/bootimage
プロジェクトのセットアップ
新規プロジェクト作成
cargo new my-os
cd my-os
Creating binary (application) `my-os` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Cargo.tomlの設定
[package]
name = "my-os"
version = "0.1.0"
edition = "2021"
[dependencies]
bootloader = "0.9.23"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
panic = "abort" って何?
通常、Rustはパニック時にスタックを巻き戻し(unwinding)ます。でもOSには巻き戻し機能がないので、即座に中断(abort)するように設定します。
no_stdの世界へ
最初のコード
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
各行の解説
#![no_std]
標準ライブラリを使わない宣言。std::*が全部使えなくなります。代わりにcore::*を使います。
stdとcoreの違い:
-
std: OSの機能(ファイル、ネットワーク、スレッドなど)に依存 -
core: OS非依存。純粋なRustの機能のみ
#![no_main]
通常のfn main()を使わない宣言。OSがないので、mainを呼び出してくれる人がいません。
#[no_mangle]
pub extern "C" fn _start() -> ! {
-
#[no_mangle]: 関数名をそのまま使う(Rustのマングリングを無効化) -
extern "C": C言語の呼び出し規約を使用 -
_start: リンカが探すエントリポイント名 -
-> !: 戻り値なし(発散型)。OSのメイン関数は終わらない
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
パニックハンドラ。stdにはデフォルトのパニックハンドラがあるけど、no_stdでは自分で定義しないといけません。
躓きポイント:ターゲット設定地獄
ここからが地獄の始まりでした...
ターゲット設定ファイルを作る
まず、x86_64向けのカスタムターゲットを作ります。
x86_64-my_os.json:
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
罠1: target-pointer-widthの型
ビルドしてみると...
cargo build
error: error loading target specification: target-pointer-width: invalid type: string "64", expected u16 at line 6 column 32
えっ、文字列じゃなくて数値!?
最近のRustではtarget-pointer-widthとtarget-c-int-widthは数値型になったようです。
"target-pointer-width": 64,
"target-c-int-width": 32,
罠2: soft-floatが使えない
修正して再ビルド...
error: error loading target specification: target feature `soft-float` is incompatible with the ABI but gets enabled in target spec
は? soft-floatがABIと互換性がない...だと...
罠3: SSE2を無効化できない
じゃあsoft-float消してSSE無効化だけにしよう...
"features": "-mmx,-sse,-sse2"
error: error loading target specification: target feature `sse2` is required by the ABI but gets disabled in target spec
x86_64ではSSE2がABIに必須になってる!!
これで1日溶けました。
解決策: rustc-abiを指定する
正解はこれ:
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
rustc-abiフィールドでソフトフロートABIを明示的に指定することで、SSE無効化とsoft-float有効化の両方が許可されます。
.cargo/config.toml
[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]
[build]
target = "x86_64-my_os.json"
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
やっとビルド成功!
cargo build
Compiling compiler_builtins v0.1.160
Compiling core v0.0.0
Compiling bootloader v0.9.33
Compiling my-os v0.1.0 (C:\Users\Aqua\Documents\Qiita\my-os)
Finished `dev` profile [optimized + debuginfo] target(s) in 14.41s
やっと通った!!!
ベアメタルでHello World
無限ループだけじゃつまらないので、画面に文字を出しましょう。
bootloaderのentry_pointマクロを使う
_startを直接定義するのではなく、bootloaderクレートのentry_point!マクロを使います。これでbootloaderが正しくカーネルを呼び出してくれます。
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use bootloader::{entry_point, BootInfo};
entry_point!(kernel_main);
fn kernel_main(_boot_info: &'static BootInfo) -> ! {
let vga_buffer = 0xb8000 as *mut u8;
let message = b"Hello, OS World!";
for (i, &byte) in message.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0x0f;
}
}
loop {}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
VGAテキストバッファとは
x86のVGAテキストモードでは、0xb8000番地から始まるメモリに書き込むと画面に文字が表示されます。
メモリアドレス 0xb8000:
+------+------+------+------+------+------+...
| 文字 | 属性 | 文字 | 属性 | 文字 | 属性 |
+------+------+------+------+------+------+
^ ^
| +-- 色(前景色 + 背景色)
+--------- ASCII文字コード
各文字は2バイト:
- 1バイト目: ASCII文字コード
- 2バイト目: 属性(色)
属性0x0fは「黒背景に白文字」です。
QEMUで動かす
ブートイメージの作成
cargo bootimage
WARNING: `CARGO_MANIFEST_DIR` env variable not set
Building kernel
Finished `dev` profile [optimized + debuginfo] target(s) in 0.11s
Building bootloader
Compiling bootloader v0.9.33
Finished `release` profile [optimized + debuginfo] target(s) in 1.67s
Created bootimage for `my-os` at `C:\Users\Aqua\Documents\Qiita\my-os\target\x86_64-my_os\debug\bootimage-my-os.bin`
実行!
qemu-system-x86_64 -drive "format=raw,file=target\x86_64-my_os\debug\bootimage-my-os.bin"
「Hello, OS World!」が表示された!!
めちゃくちゃ感動しました。たった16文字だけど、自分でOSを作って動かしたという事実がすごい。
デバッグ用:シリアル出力を追加
VGA出力だけだとデバッグが辛いので、シリアルポート出力も追加しておきます。
const SERIAL_PORT: u16 = 0x3F8;
fn serial_init() {
unsafe {
// 割り込み無効化
core::arch::asm!("out dx, al", in("dx") SERIAL_PORT + 1, in("al") 0x00u8);
// DLAB有効化
core::arch::asm!("out dx, al", in("dx") SERIAL_PORT + 3, in("al") 0x80u8);
// ボーレート設定(115200)
core::arch::asm!("out dx, al", in("dx") SERIAL_PORT + 0, in("al") 0x01u8);
core::arch::asm!("out dx, al", in("dx") SERIAL_PORT + 1, in("al") 0x00u8);
// 8ビット、パリティなし、ストップビット1
core::arch::asm!("out dx, al", in("dx") SERIAL_PORT + 3, in("al") 0x03u8);
// FIFO有効化
core::arch::asm!("out dx, al", in("dx") SERIAL_PORT + 2, in("al") 0xC7u8);
// モデム制御
core::arch::asm!("out dx, al", in("dx") SERIAL_PORT + 4, in("al") 0x0Bu8);
}
}
fn serial_print(s: &str) {
for byte in s.bytes() {
unsafe {
loop {
let status: u8;
core::arch::asm!("in al, dx", in("dx") SERIAL_PORT + 5, out("al") status);
if status & 0x20 != 0 { break; }
}
core::arch::asm!("out dx, al", in("dx") SERIAL_PORT, in("al") byte);
}
}
}
QEMUで -serial file:serial.log オプションをつけると、シリアル出力がファイルに保存されます。
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 ===
Initializing...
Writing to VGA buffer at 0xb8000...
VGA write complete!
Entering infinite loop...
これでデバッグがめっちゃ楽になりました。
躓いたポイントまとめ
| 問題 | 原因 | 解決策 |
|---|---|---|
| target-pointer-widthエラー | 文字列で指定していた | 数値64で指定 |
| soft-floatエラー | ABIと非互換 |
rustc-abiフィールドを追加 |
| SSE2無効化エラー | x86_64のABIに必須 |
rustc-abi: "x86-softfloat"で解決 |
| bootloaderがパニック | entry_pointが合ってない |
entry_point!マクロを使用 |
| シリアル出力が出ない | 初期化してなかった | serial_init()を実装 |
まとめ
Part1では以下を達成しました:
- Rust nightly + 必要なコンポーネントのインストール
- QEMUのセットアップ
- カスタムターゲットの作成(罠だらけ)
- no_std環境でのHello World
- VGAテキストバッファへの直接書き込み
- シリアル出力でデバッグ
次回(Part2)では、VGAテキストモードをもっとちゃんと扱えるようにして、println!マクロを実装します。文字を出すたびにunsafe書くの面倒ですからね...
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!