「ゼロからのOS自作入門」を Rust でやる (第6章 その1)

今日も今日とて「ゼロからのOS自作入門」をRustでやっていきます。

6ç« 

MikanOS を USB マウスへ対応させる章です。

マウスカーソルとデスクトップを描画 (day06a)

マウスカーソルとデスクトップを描画する節です。 描画するといっても、ただ表示するだけで操作などはできないものなので簡単です。

github.com

デスクトップの描画とマウスカーソルの描画はそれぞれ desktop, mouse というモジュールへと分割しています。 デスクトップの描画処理ではスクリーンのサイズ情報が必要です。 FrameBufferInfo 構造体のメンバが使えるのですが、当該構造体メンバの型は usize であるため、 描画処理で利用している型である i32 への変換が必要です。 スクリーンサイズを必要とする処理は他にも登場することが予想され、それぞれで型変換やそれに伴うエラーハンドリングを実装するのはめんどくさいため、 i32 型でスクリーンサイズ等の情報を格納する ScreenInfo 構造体を用意し、 framebuffer の初期化時に1度だけ初期化するようにしました。

PCI デバイスの探索 (day06b)

IO 命令により PCI コンフィグレーション空間にアクセスし、 USB のホストコントローラ (今回は xHC) に対応するデバイスを探索します。

github.com

処理は C++ 版と同じです。工夫した点をいくつか紹介します。

インラインアセンブリを使わない

IO ポートにアクセスするためには通常アセンブリを書く必要があります。 今回は x86_64 クレート の x86_64::instructions::port::Port を利用することで、インラインアセンブリを手書きせずに済ませました。

アトミックな IO ポートアクセス

PCI コンフィグレーション空間にアクセスするためには

  1. CONFIG_ADDRESS レジスタへ、アクセスしたい PCI コンフィグレーション空間の位置を設定
  2. CONFIG_DATA レジスタを読み書きする

という2つの手順が必要です。 この 1 と 2 の間に別のタスクが CONFIG_ADDRESS レジスタの操作を行ってしまうと意図せぬ結果となる可能性があるため、両レジスタへアクセスしている間は spin::Mutex のロックを取得するようにしました。

bit_field::BitField の利用

PCI コンフィグレーション空間からデバイスの情報を取得するためには、ビットフィールド操作が必要になります。 u32 のうち n ビット目から m ビット目までを特定の数値を設定する、といった操作です。

この操作を簡単化するため bit_field クレート の bit_field::BitField 構造体を使いました。 ビットフィールドの読み書きが以下のように書けます。

// 値の設定
let mut value = 0u32;
value.set_bits(0..8, u32::from(reg_addr));
value.set_bits(8..11, u32::from(function));
value.set_bits(11..16, u32::from(device));
value.set_bits(16..24, u32::from(bus));
value.set_bits(24..31, 0);
value.set_bit(31, true);

// 値の取得
let bus = values.get_bits(16..24) as u8;

range の形式でビット範囲が表せるのは非常に分かり易いですね。

USB マウスへの対応 (day06c)

さて、ここが問題のUSBマウス対応です。

実装方針

C++ 版実装では、「ゼロからのOS自作入門」著者の方が開発された USB ドライバ一式をインポートして使っています。 曰く、「USB関連のドライバは本書で説明するには高度で複雑すぎますので、筆者が開発したドライバを使う方法を説明するだけにします」とのこと。 OS自作が主題であるためこの対応はまったく正しいと思います。

では Rust 版を実装するにあたりどうするかですが、私も「ゼロからのOS自作入門」と同じアプローチを取りたいと思います。 つまり、C++のUSBドライバ実装を流用することとします。

USB ドライバを Rust で実装するのも興味深いですし、実際にそのアプローチで Rust 版 MikanOS を実装しようとされている方も、また、 USB ドライバの実装に成功された方もいらっしゃいます。 しかし、私が同じことをやろうとすると間違いなく時間がかかるでしょうし、その間このブログの更新は止まってしまうでしょうし、なにより作業のモチベーションを保つのが難しそうだと考えたので、 USB ドライバの Rust 移植はまずは諦めることとしました。

USB ドライバの移植は、一通り OS 実装が完了し満足した後で次の課題として取り組めたら取り組もうかなというスタンスで進めていきたいと思います。

というわけで C++ ソースと Rust ソースを結合して一つのバイナリ (カーネル) を作ることになるわけですが、以下のような作戦をとりました。

  1. MikanOS の USB ドライバ一式 (と、Rust から呼び出すためのグルーコード) を含むクレート mikanos_usb を作成
  2. mikanos_usb の build.rs で C++ ソースをコンパイルし、静的ライブラリ libmikanos_usb.a を作成し、クレートに含める
  3. 1, 2 で作成したクレートをカーネルのクレート (sabios) から利用する

この中でもポイントとなる build.rs の中身について説明します。

C++ ソースのコンパイル

build.rs の中で C++ ソースをコンパイルするために、 cc クレート を使いました。 コンパイラは gcc/g++ ではなく clang/clang++ を使わせたいため、少し行儀が悪いですが build.rs 内で環境変数 CC と CXX に clang / clang++ を設定しています。

なお、 C++ の標準ヘッダファイルも一式用意する必要があります。 当初はソースコード中にヘッダファイルを全て含めていたのですが、後に方針変更しています (後述)。

依存ライブラリの同梱

C++ ソースは newlib の libc および LLVM の libc++, libc++abi に依存しているため、 libmikanos_usb.a をカーネルにリンクする場合は、 libc.a および libc++.a, libc++abi.a もリンクする必要があります。

C++ 版では作者の方がコンパイルしたバイナリを使っています。 Rust 版では本来はこれらのライブラリについてもソースからビルドするのが良いのですが、コンパイル時間が長くなりそうだったので、作者の方がコンパイルしたバイナリを使うことにしました。 具体的には、 build.rs の中で GitHub からライブラリを含んだアーカイブをダウンロードし OUT_DIR 配下に展開、 println!("cargo:rustc-link-lib=static={}", lib); により cargo にライブラリをリンクすることを指示するというやり方をしました。 buiild.rs の中でウェブにアクセスするのは禁じ手ですが、あくまでの学習用のプロジェクトということでご容赦頂きたく...

副作用として、 GitHub からダウンロードしたアーカイブにはヘッダファイル一式も含まれていたため、C++ のビルド時にもこのヘッダファイルを利用することとしました。 リポジトリに大量のヘッダファイルを登録しなくても済むようになったところは嬉しいですね。

さて、後は Rust から C++ コードを呼び出せるように、また、 C++ から Rust を呼び出せるように、グルーコードを書きます。 適当に extern "C" な関数を定義すれば OK です。

github.com

さて、 C++ で定義されたクラスのコンストラクタを呼び出すところまで実装できました。 まだすべては実装できていませんが、できたところから動作確認していくのが良いでしょう。

動作確認

C++ ライブラリをリンク & 呼び出すようにし、さっそく実行してみたところ、 OS が reset され、 QEMU 上で再起動を繰り返すようになってしました。 なんででしょうか。

デバッグのため QEMU の起動オプションに -d int --no-reboot --no-shutdown を追加します。 これにより、割り込み発生時に割り込みの情報が標準出力に吐き出されます。 また、OS リセット時に仮想マシンが再起動せず止まった状態のままになります。

再実行したところ、どうやら例外 0xe が発生しているようです。 OSDev Wiki によると、 0xe は Page Fault とのこと。 どうやら不正なアドレスへのアクセスが起きているようですね。

QEMU の出力から、例外発生時の RIP も分かります。 QEMU のコンソールから x /5i 0x<RIPの値> を実行することで RIP 周辺の実行命令が分かります。 レジスタの値など合わせて判断するに、 xhc_mmio_base のアドレスにアクセスしようとして Page Fault が発生しているようです。 この値は今回呼び出しを追加した C++ の usb::xhc::Controller クラスのコンストラクタに渡している値でもあり、つじつまが合いますね。

すべての物理メモリをマッピングする

今回利用しているブートローダーの bootloader クレートはページテーブルを更新しているようです。 xhc_mmio_base の値は物理アドレスのはずですが、そのアドレスに向けた仮想アドレスがマッピングされていないのではないかと考えました。 bootloader の設定 を参照したところ、 map-physical-page という設定があり、デフォルト値は false でした。 これを true にすることで物理メモリがマッピングされ、 xhc_mmio_base のアドレスへもアクセスできるようになるのではないかと考え試してみました。

このオプションでは物理アドレス0以降のメモリを physical_memory_offset だけずらした仮想メモリアドレスにマッピングするため、 xhc_mmio_base のアドレスに physical_memory_offset だけ足したものを usb::xhc::Controller のコンストラクタに渡してみます。

github.com

だめでした。

bootloader の map-physical-page 有効時の処理 では、アドレス 0 から物理メモリサイズ分の領域を仮想アドレスにマッピングしています。 今回 xhc_mmio_base の値は 0x8_0000_0000 だったのですが、これは 0x0 から 32GiB 分だけ離れた領域になります。 QEMU 仮想マシンにはのメモリ容量はもっと小さいはずなので、 xhc_mmio_base の領域が仮想アドレスにマッピングされないのは当然です。

どうやら自分で xhc_mmio_base の物理アドレスを仮想アドレスにマッピングする必要がありそうですね。

しかし、6章時点ではまだページング関連処理は実装されていません。 6章を進めるのは一旦ここでやめ、先にページングを実装することとしました。

まとめ

USB ドライバを利用しようとしたところ、C++プログラムのコンパイルとリンクはうまくいきましたが、 bootloader クレートの仕様により xhc_mmio_base へのアクセスができませんでした。 対処のためにはページテーブルを更新して当該アドレスへアクセスできるようにする必要があります。

一旦6章は中断し、先に8章に進むことにしました。次回もお楽しみに。