6
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【完結】

はじめに

「自作OS」って響き、ワクワクしませんか?

私はものすごくワクワクしました。で、実際に作り始めたら...

環境構築だけで3日溶けました。

この記事では、私が躓いたポイントを全部晒しながら、Rustでx86_64向けのOSを作る環境を構築していきます。

目次

  1. なぜRustで自作OSなのか
  2. 必要なツールのインストール
  3. プロジェクトのセットアップ
  4. no_stdの世界へ
  5. 躓きポイント:ターゲット設定地獄
  6. ベアメタルでHello World
  7. QEMUで動かす

なぜRustで自作OSなのか

自作OSといえばCが定番ですよね。でも私はRustを選びました。理由は:

  1. メモリ安全性: OSでのメモリバグは致命的。Rustなら多くのバグをコンパイル時に防げる
  2. ゼロコスト抽象化: パフォーマンスを犠牲にせずに高レベルな抽象化ができる
  3. モダンなツールチェーン: Cargoが神
  4. 勉強になる: 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::*を使います。

stdcoreの違い:

  • 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-widthtarget-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書くの面倒ですからね...

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

6
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
6
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?