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

ちょっと間が空いてしまいましたが、第6章の続きです。 今回はついにマウスカーソルを動かせるようになります!

第6章

第6章 その1 の記事では C++ の USB ドライバをコンパイル & カーネルにリンクした上で Rust 側コードから呼び出すようにしましたが、XHC との通信を行う MMIO 領域へアクセスするとページフォールトが発生してしまったのでした。

今回はページテーブル等の設定を行う事で USB マウスでマウスカーソルを動かせるようにします。

MMIO 領域に仮想アドレスを割り当てる

第8章 で用意した部品を使って、 xhc_mmio_base から 64KiB 分のフレームを仮想アドレスにマッピングします。 マッピングするアドレスは物理アドレスと同じアドレスとします (アイデンティティマッピング)。 アイデンティティマッピングとすることでアイデンティティマッピングを前提としている USB ドライバ実装の変更が不要になります。

github.com

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 トレイト を使い、 pageframeマッピングしています。 ページテーブルを配置するための物理メモリを割り当てる必要があるため allocator を引数に渡しています。

この対応により無事ページフォールトは発生しなくなりました! やったね!

続きの部分の実装

残りの処理も実装しました。

github.com

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 の実装を見てみましょう。 ざっと読むと、以下のような処理を行っているようです。

  1. AllocArray を呼び出し、 buf_erst_ の領域を確保
  2. ERSTSZ レジスタerstsz (erst のサイズ?) をセット
  3. ERDP レジスタbuf_ のアドレスをセット
  4. ERSTBA レジスタerst_ のアドレスをセット

ここで ERDPERSTBA にセットされるアドレスはどのようなものなのでしょうか。 AllocArray の実装 を見てみると、グローバル変数として静的に獲得された領域のようです。 C++プログラム上で取り扱っているアドレスをそのままレジスタにセットしているため、設定される値は当然仮想アドレスです。 一方、XHC 側が必要とするアドレスは物理アドレスだと想像されます。 仮想アドレスが物理アドレスと一致しないために、 EventRing は正しく動作しないのではないでしょうか。

仮想アドレスと物理アドレスが一致していないかどうか確認するため、 bootloader のソースを確認してみましょう。 bootloader のカーネルをロードする処理 では、セグメントをロードする物理アドレスはファイル中のセグメント位置に固定値 (self.kernel_offset) を足したもの、仮想アドレスはセグメントで指定された仮想アドレスにロードしているようです。 self.kernel_offset の値は、 カーネルと同じサイズ (?) の static 変数の先頭アドレス でした。 この実装を見る限り、仮想アドレスと物理アドレスが必ず一致するようなソースにはなっていないようですね。

対処

どうやら、XHC のレジスタに渡すアドレスが適切な物理アドレスになっていないところに問題がありそうです。 他にも問題はあるかもしれませんが、まずはこの問題に対処してみましょう。以下の方針が考えられそうです。

  1. ブートローダーのカーネルのロード処理を改造し、カーネルをロードした範囲では仮想アドレスと物理アドレスが一致するようにする
  2. USB ドライバーを改造し、メモリマップドIOに利用するバッファ領域を仮想アドレスと物理アドレスが一致している領域に配置する
  3. USB ドライバーを改造し、アドレスをレジスタにセットする処理で仮想アドレスを物理アドレスへ変換した上でセットするようにする

1, 3 は大変そうなので、簡単な2を採用しました。

github.com

static 変数で 32 * 4096 バイトの領域を確保していたところを、 FrameAllocator 32フレーム獲得した上でアイデンティティマッピングを設定し、その仮想アドレスをバッファのアドレスとしてセットするようにしました。

ドキドキしながら実行してみたところ、無事マウスカーソルがマウスの動きに連動して動作するようになりました。やったーーー!!!

まとめ

紆余曲折ありましたが、無事USBドライバを動作させマウスの動きをキャプチャできるようになりました。 C++とのFFIに苦戦するかと思っていたのですが、そこは思ったよりすんなりとできました。 苦労したのは bootloader のメモリの使い方の差異への対応でしたが、なんとか解決できて良かったです。 また、C++のUSBドライバ実装にも少しだけ詳しくなれた気がします。

一つ大きな山を越えられたので、この先もどんどん実装していきたいと思います。