LoginSignup
79
29

More than 3 years have passed since last update.

x86 ベアメタル湯婆婆 in Rust

Last updated at Posted at 2020-12-07

湯婆婆の記事です。

x86 ベアメタルで湯婆婆を走らせようと思いました。

日数が限られていて慌てて x86 ベアメタル湯婆婆を拵える必要がありました💦 C/C++ でやるなら震えて眠るところでした。でも Rust ならなんとかなりそうでした。

念のため、「x86 ベアメタル湯婆婆」とは OS やランタイムのない剝き出しの x86 ハードウェアの直上で湯婆婆が起動することです。言い換えると、この記事は慌てて作る OS 自作入門 in Rust ざっくり編 (湯婆婆付き) ということになると思います。

コードはこちらに公開してあります 💦 https://github.com/yushiomote/baremetal-yubaba

Rust という言語

神言語のひとつです。非常に強力な memory safety を実現しつつ、 zero-cost abstraction によって C/C++ に追従する性能を備えています。safety ゆえ、その上には強力なエコシステムが育っています。その勢いは低レイヤーでも止まりません。

素晴らしい既存のライブラリを活用して短期間でベアメタルで動く湯婆婆が作れます。

作り方

no_std を使う

Rust の標準ライブラリは OS に依存する std と呼ばれる部分と、 OS 非依存な core と呼ばれる部分から成っています。 Rust ではベアメタルでのコーディングも配慮されており、no_std という設定を有効にすることで std を削ぎ落としたモードでコンパイルをかけることができます。

std.png

有効にするにはメインファイル main.rs(ライブラリなら lib.rs)の先頭に #![no_std] と宣言するだけです。

main.rs
#![no_std]

fn main() {
   ...
}

この瞬間からメモリアロケーションやシステムコールといった OS に依存するパートを使うとコンパイルが通らなくなります。逆に言うと、ここでコンパイルが通るとどこでも動くようになるということです。ベアメタル湯婆婆では、基本的にこのモードでコーティングしていきます。

cargo-xbuild を使う

cargo build という通常の方法を使うと、 Rust はその環境向けのバイナリ(Linux 上で開発していれば Linux 上で動くバイナリ)をビルドしに行きます。しかし、今回は OS のいない x86 ベアメタル上で動作するバイナリをビルドする必要があります。そこで、 cargo-xbuild というそれ用のクロスコンパイルを行うツールを利用します。

(※ 最近では cargo build でも build-std という機能を使うことで、 cargo-xbuild と同様のことができるようになりました。)

ブートローダ

PC の電源投入後、最初に実行される部分のプログラムを用意していきます。ここの部分はブートローダと呼ばれるもので、簡単な初期化と OS (湯婆婆) 本体のディスクからの読み込みを行い、本体に制御を移すという動きをします。

本来ブートローダの実装はアセンブリの知識も必要になったりして結構敷居の高いパートだったりするのですが、 Rust ではこの部分を実装してくれている bootloader といういい感じの既存ライブラリがあるので今回はこれを利用します。このライブラリを使うことで、電源投入後のディスクからのコード読み出し、CPU モード遷移、ページング、セグメンテーションなどの最低限のメモリの初期化をサクッとやって Rust の main 的な関数にジャンプするところまでやってくれてしまいます。

main.rs
// ここがベアメタル版 `main`
#[no_mangle]
pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! {
    // ここから Rust が書き始められる
}

Rust ではこういったライブラリが利用できる仕組みが整っていて、x86 低レイヤーでも先人たちが用意したライブラリをガシガシ組み合わせて開発ができます。

デバッグできるようにする

コードを追加していく前に、まずは最低限のデバッグがしやすいようになんらかの出力を出せるようにします。往々にしてシリアルポートからログをはいたりします。

これも com_logger という既存のライブラリが使えます。

main.rs
use log::*;

#[no_mangle]
pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! {
    // この1行でシリアル出力を有効にして、
    // Rust おなじみの `log` crate 経由でログをはけるようにします。
    com_logger::init();

    info!("Huga");
    warn!("Hage");
    debug!("Fuge");
    trace!("Hoge");
}

ヒープを用意する

前述のとおり no_std をつけることによって、今まで OS が提供してくれていたメモリアロケーションも使えなくなります。そこで自前でメモリアロケーションを用意する必要があります。

自前のアロケーターを有効にするには GlobalAlloc と呼ばれる trait を実装した構造体を用意し、 #[global_allocator] と呼ばれる attribute をくっつけてグローバル変数として定義します。

// 自前のメモリアロケータ
struct MyAllocator {
   ...
}

// GlobalAlloc という
impl alloc::alloc::GlobalAlloc for MyAllocator {
    unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
        // Rust ないでメモリ確保が必要になると常にここが呼ばれる。
        // どこかからメモリ領域を確保する
    }
    ...
}

// こうやって登録する
#[global_allocator]
static ALLOCATOR: MyAllocator = MyAllocator { ... };

では次に実際どこのメモリ領域を割り当てるかを決める必要があります。通常、ファームウェア(BIOS)がメモリマップ (ソフト的にどのメモリ領域を使ってよいのかのマップ) を提供しているので、そこの情報をもとにメモリ領域を割り当てていきます。 bootloader の機能でこの情報が取れるようになっているのでそれを使います。

main.rs
#[no_mangle]
pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! {
    let _ = boot_info.memory_map; // 引数の `BootInfo` にぶら下がっている
    ...
}

ここでアロケータでメモリ確保が必要になるたびに、使ってよい領域を割り当てていきます。ここではページテーブルという物理アドレスと仮想アドレスのマッピングがおこなれますが複雑なので今回は割愛します。

画面に文字を表示する

画面描画に関しても vga というライブラリが利用できます。画面モードの遷移や文字や図形の描画をサポートする機能が入っています。

let gfx = Graphics640x480x16::new();
gfx.set_mode();

たったこれだけで画面遷移ができて 640 x 480 の広めの画面が利用できるようになります。また set_pixel という関数で簡単にドットを描画できます。

gfx.set_pixel(x, y, color);

今回はこれを使て文字を表示していきます。

フォントを用意する

湯婆婆は日本語を喋るので日本語のフォントを用意する必要があります。今回は愚直にその辺に転がってるフォントファイルをパースしてあらかじめビットマップを用意してそれを描画するようにします。

$ fc-list | grep ゴ
/usr/share/fonts/vlgothic/VL-Gothic-Regular.ttf: VL Gothic,VL ゴシック:style=regular

これも fontdue という既存のライブラリを活用します。

let font = fontdue::Font::from_bytes(
    フォントファイルの中身, fontdue::FontSettings::default()).unwrap();

// '婆' という文字の幅などの情報とビットマップを表すバイト列が取れます。
let (metrics, bitmap) = font.rasterize('婆', 10.0);

これを使って、愚直に 0 から 0x9faf` の Unicode のビットマップを用意しています。

(0u32..0x9faf).for_each(|v| {
        let (metrics, bitmap) = font.rasterize(v.try_into().unwrap(), FONTSIZE);
        // この情報をファイルに保存して湯婆婆プログラムに組み込んで使う
    });

キーボード入力を受ける

続いては湯婆婆の契約書にサインできるようにする必要があります。そのためキーボード入力を受け付けられるようにします。

x86 で起動した状態ではキーボードは I/O ポートから取れる状態になっています。Rust では x86_64 という既存ライブラリの Port という構造体を使えば任意の I/O ポートからの読み書きを簡単に実装できます。
キーボード入力情報は 0x60 番の I/O ポートを読むことで取得できます。

let port = Port::new(0x60);

// キーボード入力があるとここに何らかの情報が入ってくる
let byte = unsafe { port.read() };

ここで、ポートから読めたバイトをデコードしてどのキーが押されたかなどを判定する必要があるのですが、それも pc-keyboard という既存のライブラリ簡単にデコードできます。

// US104 のキーボード入力をデコードする
let kbd = pc_keyboard::Keyboard::new(layouts::Us104Key, ScancodeSet1);

// I/O ポートから取得した `byte` を食わせることで
// キーボードの入力イベントをとれる。
let event = kbd.add_byte(byte).unwrap();

// さらに event をデコードして実際に押されたキーの文字が取得できる
match kbd.process_keyevent(event) {
    Some(DecodedKey::Unicode(c)) => Some(c),
    _ => None,
}

これでキーボード入力された文字列が取得出るので湯婆婆の契約書にサインできます。

乱数?

湯婆婆は適当な名前の1文字を選択します。通常(?)乱数を使いますが、今回は割愛しました。代わりに、TSC と呼ばれる CPU クロックカウンタの値を使って、単純に文字列長でモジュロをとって名前の1文字を選択することを考えます。

x86 では RDTSC という CPU 命令を使うとこのカウンタの値をとることができます。この命令自体は Rust の標準ライブラリで関数として用意されているので単純にそれを利用します。

// CPU のカウンタの値を返す関数を用意
pub fn rdtsc() -> u64 {
    unsafe {
        core::arch::x86_64::_rdtsc()
    }
}

湯婆婆する

以上で準備が整ったので湯婆婆していきます。

#[no_mangle]
pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! {
    // デバッグログを有効に。
    com_logger::init();

    // アロケータを使えるようにする。
    init_heap(boot_info).unwrap();

    // キーボード入力と画面描画はここの `console` にまとめました。
    let mut console = new_console_640x480();
    console.reset();
    console.print("契約書だよ。そこに名前を書きな。\n");

    let input = console.readline();

    // CPU のカウンタから名前1文字を選択。
    let idx = rdtsc() as usize % input.len();
    let nickname = input.chars().nth(idx).unwrap();

    console.print(&format!(
        "\nフン。{}というのかい。\n贅沢な名だねぇ。",
        input
    ));
    console.print(&format!(
        "今からお前の名前は{0}だ。\nいいかい、{0}だよ。分かったら返事をするんだ、{0}!!\n",
        nickname
    ));

    loop {}
}

VM で動作確認

yubaba.gif

...慌てていましたので,フォントの取り込みが雑すぎて声がガサガサになってしまいました 💦

日本語入力はすいませんできません 💦

名前入力が空だと死ぬ必要があるのですが残念ながらそのままでは死んでくれませんでした.なので,雰囲気だけ実装しておきました.

yubaba-failure.gif

実機で動作確認

せっかくベアメタルなので実機で動作確認してみたいところです。しかし、残念ながら手元に空いてる実機がなかったので今回はベアメタルクラウド(物理マシンを提供するクラウドサービス)を利用します。

今回は Vultr VPS の bare-metal instance を使いました。せっかくなのでカリフォルニアからベアメタル湯婆婆します。

Screenshot 2020-12-07 195657.png

最初は OS を選択しなくてはいけないのですが、もちろん直後に湯婆婆で 完全に上書き ました。ディスクには湯婆婆しかいない状態になります。

サーバなので,ちょっと起動に時間がかかりますね.スーパーマイクロシリコンバレーベアメタル湯婆婆です.

yubaba-cloud.gif

参考

https://os.phil-opp.com/ めっちゃ参考になります.

使ったライブラリ一覧:

まとめ

baba.png

画像も取り込んでみました、適当にやりすぎて色がイッてますね 💦

79
29
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
79
29