「ゼロからのOS自作入門」を Rust でやる (第6章 その1)
今日も今日とて「ゼロからのOS自作入門」をRustでやっていきます。
- シリーズ最初の記事: 「ゼロからのOS自作入門」を Rust でやる (第1章~第4章) - gifnksmの雑多なメモ
- 前回: 「ゼロからのOS自作入門」を Rust でやる (第5章) - gifnksmの雑多なメモ
- 次回: 「ゼロからのOS自作入門」を Rust でやる (第8章) - gifnksmの雑多なメモ
- 関連記事一覧: ゼロからのOS自作入門 カテゴリーの記事一覧 - gifnksmの雑多なメモ
6章
MikanOS を USB マウスへ対応させる章です。
マウスカーソルとデスクトップを描画 (day06a)
マウスカーソルとデスクトップを描画する節です。 描画するといっても、ただ表示するだけで操作などはできないものなので簡単です。
デスクトップの描画とマウスカーソルの描画はそれぞれ desktop
, mouse
というモジュールへと分割しています。
デスクトップの描画処理ではスクリーンのサイズ情報が必要です。
FrameBufferInfo
構造体のメンバが使えるのですが、当該構造体メンバの型は usize
であるため、
描画処理で利用している型である i32
への変換が必要です。
スクリーンサイズを必要とする処理は他にも登場することが予想され、それぞれで型変換やそれに伴うエラーハンドリングを実装するのはめんどくさいため、 i32
型でスクリーンサイズ等の情報を格納する ScreenInfo
構造体を用意し、 framebuffer の初期化時に1度だけ初期化するようにしました。
PCI デバイスの探索 (day06b)
IO 命令により PCI コンフィグレーション空間にアクセスし、 USB のホストコントローラ (今回は xHC) に対応するデバイスを探索します。
処理は C++ 版と同じです。工夫した点をいくつか紹介します。
インラインアセンブリを使わない
IO ポートにアクセスするためには通常アセンブリを書く必要があります。
今回は x86_64
クレート の x86_64::instructions::port::Port
を利用することで、インラインアセンブリを手書きせずに済ませました。
アトミックな IO ポートアクセス
PCI コンフィグレーション空間にアクセスするためには
という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 ソースを結合して一つのバイナリ (カーネル) を作ることになるわけですが、以下のような作戦をとりました。
- MikanOS の USB ドライバ一式 (と、Rust から呼び出すためのグルーコード) を含むクレート
mikanos_usb
を作成 mikanos_usb
のbuild.rs
で C++ ソースをコンパイルし、静的ライブラリlibmikanos_usb.a
を作成し、クレートに含める- 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 です。
さて、 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
のコンストラクタに渡してみます。
だめでした。
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章に進むことにしました。次回もお楽しみに。