湯婆婆の記事です。
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
を削ぎ落としたモードでコンパイルをかけることができます。
有効にするにはメインファイル main.rs
(ライブラリなら lib.rs
)の先頭に #![no_std]
と宣言するだけです。
#![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`
#[no_mangle]
pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! {
// ここから Rust が書き始められる
}
Rust ではこういったライブラリが利用できる仕組みが整っていて、x86 低レイヤーでも先人たちが用意したライブラリをガシガシ組み合わせて開発ができます。
デバッグできるようにする
コードを追加していく前に、まずは最低限のデバッグがしやすいようになんらかの出力を出せるようにします。往々にしてシリアルポートからログをはいたりします。
これも com_logger という既存のライブラリが使えます。
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
の機能でこの情報が取れるようになっているのでそれを使います。
#[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 で動作確認
...慌てていましたので,フォントの取り込みが雑すぎて声がガサガサになってしまいました 💦
日本語入力はすいませんできません 💦
名前入力が空だと死ぬ必要があるのですが残念ながらそのままでは死んでくれませんでした.なので,雰囲気だけ実装しておきました.
実機で動作確認
せっかくベアメタルなので実機で動作確認してみたいところです。しかし、残念ながら手元に空いてる実機がなかったので今回はベアメタルクラウド(物理マシンを提供するクラウドサービス)を利用します。
今回は Vultr VPS の bare-metal instance を使いました。せっかくなのでカリフォルニアからベアメタル湯婆婆します。
最初は OS を選択しなくてはいけないのですが、もちろん直後に湯婆婆で 完全に上書き ました。ディスクには湯婆婆しかいない状態になります。
サーバなので,ちょっと起動に時間がかかりますね.スーパーマイクロシリコンバレーベアメタル湯婆婆です.
参考
https://os.phil-opp.com/ めっちゃ参考になります.
使ったライブラリ一覧:
まとめ
画像も取り込んでみました、適当にやりすぎて色がイッてますね 💦