30日OSをRustで書く(14日目まで)

川合秀実先生著の「30日でできる!OS自作入門」を買った.

と同時に,先日からRustの勉強も行っていたので,本のC言語の部分をRustに書き換えてやってみようとしたところ,意外と難しく...

自力では無理そうだったので,参考になるような記事を見つけて,それをかなり真似して14日目まで到達した.コードの内容は独自性がほぼ無いので,OSやRustの初学者とし気づきを記事にできたらいいなと思う.

環境

macOS Catalina 10.15.4

qemu 4.2.0

アセンブラ

1日目から本の中では「nask」という著者の自作アセンブラが使われているんですが,macOS Catalina 10.15では32bitのアプリケーションが動作しないため,自分はnaskの元ネタである「NASM」を使用した.

NASMとnaskは違いはほとんどなく(http://hrb.osask.jp/wiki/?tools/nask),多少修正すれば動作するようになった.

Rust登場(3日目から)

3日目からRustを使ったOS開発を行っていくことになるが,そのための下準備が必要.
まずcargoを使ってプロジェクトを作成.

% cargo new --lib my30daysOS_in_rust

また,nightlyの機能を使えるようにしておく.

% rustup override set nightly
Rustのコードを書く

実際に書いていく.ここでは4日目の白画面を表示させるコードを例に説明していく.

#![no_std]
#![feature(asm)]
#![feature(start)]

use core::panic::PanicInfo;

#[no_mangle]
fn hlt() {
    unsafe {
        asm!("hlt");
    }
}

#[no_mangle]
fn show_white(i: u32) {
    let a: u8 = 15;
    let ptr = unsafe { &mut *(i as *mut u8) };
    *ptr = a 
}

#[no_mangle]
#[start]
pub extern "C" fn HariMain() -> ! {
    for i in 0xa000..0xaffff {
      show_white(i);
    }
    loop {
        hlt()
    }
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    // println!("{}", info);
    loop {
        hlt()
    }
} 
#![no_std]

Rustを始めとするいろんな言語には標準ライブラリというものがあります.しかし標準ライブラリはOSが提供する機能を使って動作するため,OSそのものを開発しようとするときは使用することができません.通常暗黙的に標準ライブラリがリンクされているので,#![no_std]をコードの上のほうに加えてそれを無効にします.

core library

no_std環境の時,core libraryというものが代わりにリンクされる.これはrustのみで書かれたライブラリでno_stdでも動作する.

panic_handler

パニックが起きたときコンパイラはパニックハンドラを呼び出さなければなりません.通常では標準ライブラリがこれを定義しているのですが,標準ライブラリが使えないため,自分で定義しないといけません.panic()の引数のPanicinfoはパニック時のメッセージとソースコードのどこでパニックが起きているかが含まれたパラメータである.まだこの時点ではそれを出力することはできないので,代わりにhlt()を使って停止している.

asm!()

asm!()はRustのインラインアセンブリのためのマクロである.#![feature(asm)]を使って,さらにunsafeブロックで囲む必要がある.

unsafeブロック

rustにおいて未定義動作が起こる恐れのあるプログラムを動作させる必要があるとき,コンパイラに制約を緩めてそれを容認するように伝えなければならない.それが「unsafe」.上のコードでもasm!()と生ポインタの参照外しの時に使用している.

Harimain

通常コンパイラは,各関数のシンボルを生成する時暗号のような名付けを行い,それぞれの関数に固有なシンボル名が行き渡るようにする.これをname manglingというらしい.しかし,今回はHariMain()をエントリポイントとしてリンカに伝えたいので,名前を勝手に変えられると困る.ということで#[no_mangle]をつけた.また,extern "C" としてこの関数にCの呼び出し規則を使うように明記しないといけないらしい.

大体自分がよくわからない疑問はこれで解けて,このコードでは0xa000~0xafff番地までのメモリに1バイトごとに15を格納していることがわかった.

コンパイル

既存のcargoがサポートするターゲットトリプルではコンパイルできないため,自分たちでカスタムターゲットを設定する必要がある.
ファイル名は"i386-haribote.json" にした.

{
  "arch": "x86",
  "data-layout": "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128",
  "llvm-target": "i386-unknown-none",
  "features": "",
  "target-endian": "little",
  "target-pointer-width": "32",
  "target-c-int-width": "32",
  "os": "none",
  "code-model": "kernel",
  "relocation-model": "static",
  "archive-format": "gnu",
  "target-env": "gnu",
  "no-compiler-rt": false,
  "linker-flavor": "ld",
  "linker-is-gnu": true,
  "disable-redzone": true,
  "eliminate-frame-pointer": false
}

これを.cargo/config に

[build]
target = "i386-haribote.json"

とすることで,設定したターゲットに向けたコンパイルを行ってくれるらしい.

また,Cargo.toml も設定が必要.

[profile.dev]
opt-level = 2
lto = true
panic = "abort"

[profile.release]
opt-level = 2
lto = true
panic = "abort"

[lib]
name = "haribote_os"
crate-type = ["staticlib"]

ビルドにはcargo xbuildを使った.これはcargo buildのラッパで,coreや他の組み込みライブラリを自動的にクロスコンパイルしてくれる.

cargo install cargo-xbuild

ビルド&実行

RustでOSを書いてみる(環境構築編) - Qiita
こちらの記事を参考にNASM,binutilsQEMU,mformat,mcopyのインストールを行い,Makefileを作成した.
そして実行,無事に白い画面が表示された.

現在

現在は14日目まで到達し,タイマ,マウス,キー入力などができるようになっている.
f:id:udon-yuya:20200507011258p:plain

f:id:udon-yuya:20200507011343p:plain
ウィンドウを動かすこともできる

終わりに

OSをRustで書くというのは思っていたよりも難しく,つまずく場面が多いがなんとか根気よく頑張っていきたいと思う.