「ゼロからのOS自作入門」を Rust でやる (第6章 その2)
ちょっと間が空いてしまいましたが、第6章の続きです。 今回はついにマウスカーソルを動かせるようになります!
- シリーズ最初の記事: 「ゼロからのOS自作入門」を Rust でやる (第1章~第4章) - gifnksmの雑多なメモ
- 前回: 「ゼロからのOS自作入門」を Rust でやる (第8章) - gifnksmの雑多なメモ
- 次回: 「ゼロからのOS自作入門」を Rust でやる (第7章) - gifnksmの雑多なメモ
- 関連記事一覧: ゼロからのOS自作入門 カテゴリーの記事一覧 - gifnksmの雑多なメモ
第6章
第6章 その1 の記事では C++ の USB ドライバをコンパイル & カーネルにリンクした上で Rust 側コードから呼び出すようにしましたが、XHC との通信を行う MMIO 領域へアクセスするとページフォールトが発生してしまったのでした。
今回はページテーブル等の設定を行う事で USB マウスでマウスカーソルを動かせるようにします。
MMIO 領域に仮想アドレスを割り当てる
第8章 で用意した部品を使って、 xhc_mmio_base
から 64KiB 分のフレームを仮想アドレスにマッピングします。
マッピングするアドレスは物理アドレスと同じアドレスとします (アイデンティティマッピング)。
アイデンティティマッピングとすることでアイデンティティマッピングを前提としている USB ドライバ実装の変更が不要になります。
64KiB マッピングしているのは、 Intel のリファレンスのページ にそう書いてあるためです (This gives 64 KB of relocatable memory space aligned to 64 KB boundaries.)。
マッピングをしているのは以下処理です。
{ // Map [xhc_mmio_base..(xhc_mmio_base+64kib)] as identity map use x86_64::structures::paging::PageTableFlags as Flags; let base_page = Page::from_start_address(VirtAddr::new(xhc_mmio_base))?; let base_frame = PhysFrame::from_start_address(PhysAddr::new(xhc_mmio_base))?; let flags = Flags::PRESENT | Flags::WRITABLE; let mut allocator = memory::lock_memory_manager(); for i in 0..16 { let page = base_page + i; let frame = base_frame + i; unsafe { mapper.map_to(page, frame, flags, &mut *allocator) }?.flush(); } }
x86_64
クレート の x86_64::structures::paging::mapper::Mapper
トレイト を使い、 page
を frame
にマッピングしています。
ページテーブルを配置するための物理メモリを割り当てる必要があるため allocator
を引数に渡しています。
この対応により無事ページフォールトは発生しなくなりました! やったね!
続きの部分の実装
残りの処理も実装しました。
C++ 実装と大きな差異はありません。
一通り実装して動かしてみたのですが... 動きません。 なぜでしょうか。 ページフォールトなどの例外も発生していないようです。
デバッグ
どうやら C++ のコードがうまく動作していないようです。 それなりの規模のコードをデバッガなしでデバッグするのはつらいので、 gdb でデバッグを試してみましょう。
まず、 QEMU の起動オプションに以下を追加します。
$ qemu-system-x86_64 ... -gdb tcp::1234
これにより localhost のポート 1234 で gdb の接続を受け付けるようになります。 別の端末から以下のように gdb を起動するとアタッチできます。
$ gdb ./target/x86_64-sabios/debug/sabios -ex "target remote localhost:1234"
実行中のコード行やバックトレースも表示され、 ユーザーランドの普通のプログラムをデバッグしているかのようですね。
gdb でステップ実行などして調べてみると usb::xhci::ProcessEvent
関数 の振る舞いがおかしいようです。
当該関数ではどこからか通知されたイベントを処理しているようですが、イベントがひとつもキューイングされてきていないようです。
イベントをキューイングする処理が正しく動作していないのかもしれません。
次に、イベントをキューイングしているのは誰かを調べてみましょう。
PrimaryEventRing()
で返ってくるのは usb::xhc::EventRing
型の参照なので、 EventRing
の実装を調べるのが良さそうです。
ProcessEvent
関数で呼び出している EventRing::HasFront
メソッドでは ReadDequeuePointer
というメソッドを呼び出しています。
ReadDequeuePointer
の実装 では interrupter_->ERDP.Read().Pointer()
を呼び出しています。
interrupter_
は、EventRing::Initialize
呼出し時 に渡されるもので、 InterrupterRegisterSets()
で返される配列の先頭へのポインタです。
InterruptRegisterSets
の定義 より、これは xhc_mmio_base
に一定のオフセットを加算した領域のようです。また、当該関数の戻り値型である InterrupterRegisterSetArray
は メモリマップされたレジスタを意味する構造体 (の配列) のようです。
interrupter_->ERDP.Read().Pointer()
の意味するところは、 ERDP
というメモリマップドレジスタの値を読み込んでいるようですね。
まとめると、EventRing
ではメモリマップドレジスタの値を読み込み、値が特定の条件を満たしている場合にイベントが発生したと判断するようです。
イベントが発生していないということは、このメモリマップドレジスタの値の更新が (XHC により?) 行われていないというのではないでしょうか。
もしそうであれば、 XHC の初期化処理が正しく行われていないのではないかと想像されます。
この観点でもう少し調べて見ましょう。
手始めに EventRint::Initialize
の実装を見てみましょう。
ざっと読むと、以下のような処理を行っているようです。
AllocArray
を呼び出し、buf_
とerst_
の領域を確保ERSTSZ
レジスタにerstsz
(erst
のサイズ?) をセットERDP
レジスタにbuf_
のアドレスをセットERSTBA
レジスタにerst_
のアドレスをセット
ここで ERDP
と ERSTBA
にセットされるアドレスはどのようなものなのでしょうか。
AllocArray
の実装 を見てみると、グローバル変数として静的に獲得された領域のようです。
C++プログラム上で取り扱っているアドレスをそのままレジスタにセットしているため、設定される値は当然仮想アドレスです。
一方、XHC 側が必要とするアドレスは物理アドレスだと想像されます。
仮想アドレスが物理アドレスと一致しないために、 EventRing
は正しく動作しないのではないでしょうか。
仮想アドレスと物理アドレスが一致していないかどうか確認するため、 bootloader のソースを確認してみましょう。
bootloader のカーネルをロードする処理 では、セグメントをロードする物理アドレスはファイル中のセグメント位置に固定値 (self.kernel_offset
) を足したもの、仮想アドレスはセグメントで指定された仮想アドレスにロードしているようです。
self.kernel_offset
の値は、 カーネルと同じサイズ (?) の static 変数の先頭アドレス でした。
この実装を見る限り、仮想アドレスと物理アドレスが必ず一致するようなソースにはなっていないようですね。
対処
どうやら、XHC のレジスタに渡すアドレスが適切な物理アドレスになっていないところに問題がありそうです。 他にも問題はあるかもしれませんが、まずはこの問題に対処してみましょう。以下の方針が考えられそうです。
- ブートローダーのカーネルのロード処理を改造し、カーネルをロードした範囲では仮想アドレスと物理アドレスが一致するようにする
- USB ドライバーを改造し、メモリマップドIOに利用するバッファ領域を仮想アドレスと物理アドレスが一致している領域に配置する
- USB ドライバーを改造し、アドレスをレジスタにセットする処理で仮想アドレスを物理アドレスへ変換した上でセットするようにする
1, 3 は大変そうなので、簡単な2を採用しました。
static 変数で 32 * 4096 バイトの領域を確保していたところを、 FrameAllocator
32フレーム獲得した上でアイデンティティマッピングを設定し、その仮想アドレスをバッファのアドレスとしてセットするようにしました。
ドキドキしながら実行してみたところ、無事マウスカーソルがマウスの動きに連動して動作するようになりました。やったーーー!!!
まとめ
紆余曲折ありましたが、無事USBドライバを動作させマウスの動きをキャプチャできるようになりました。 C++とのFFIに苦戦するかと思っていたのですが、そこは思ったよりすんなりとできました。 苦労したのは bootloader のメモリの使い方の差異への対応でしたが、なんとか解決できて良かったです。 また、C++のUSBドライバ実装にも少しだけ詳しくなれた気がします。
一つ大きな山を越えられたので、この先もどんどん実装していきたいと思います。