みかん本

ゼロからの OS 自作入門 (みかん本) やっていく
メモしていきたい項目
- 寄り道して調べたこととか
- (サポート外の) 環境構築とか
- 参考になった記事
- 理解を定着させるための内容のメモとか

使っている環境
- M2 Macbook Air (aarch64, MacOS 13.7)
本での想定標準環境は ubuntu 18.04 or 20.04 の x86_64 環境だから、環境構築の時点でだいぶハマりどころがある
Mac でやるならざっと調べた感じ以下のあたりが参考になりそう?

開発環境構築の方針としてだいたい以下のどれかを使うことになる
- MacOS ネイティブ環境
- Docker (for Mac)
- Ubuntu VM (Multipass) 環境
また、Docker, VM なら開発環境の時点で x86_64 のエミュレーション環境を使うのも手だが、ビルドが遅そうなのでできれば割けたい気がする。
MacOS ネイティブ環境もビルドが難しそう。
本で提供されてる Ansible playbook も ubuntu 依存なので MacOS 環境だとそのままでは使えない。
したがって、aarch64 で Docker or VM の ubuntu 環境でやっていきたい
Docker でも良いのだけど、ログインしていろいろ作業する使い方だと VM のほうが使いやすいかなという 印象で、Multipass ubuntu 24.04 環境で進めていこうかというつもり
なお、Docker だったら mikanos-docker というのが便利そうだったが、devcontainer 使うつもり無いしなあというので見送り
なお、ビルド対象の OS も aarch64 ターゲットにしても良いのだけど、そうするとまたいろいろ変わりそう (OVMF のビルドとかがちょっと面倒そう? もしアセンブリ直書きするところとかあると面倒そう) な気がするので、とりあえずターゲットは x86_64 ということにしてクロスコンパイル前提で進めていこうと思う

あとは、QEMU をどこで実行するかという選択肢も一応ある。
VM の中でビルドして、QEMU は full system emulation してくれるはずなのでゲスト OS を動かす上ではホストは Mac だろうと Ubuntu だろうと基本は関係無いはず。
MacOS 側で QEMU を使えば GUI まわりの面倒臭さは少ない。
一方、QEMU 起動するためだけに VM から環境切り替えてくるのがちょっと面倒かも。
(ホスト側のディレクトリをマウントして VM から直接いじればビルド結果をコピーしてくるとかはしなくてもいいはずなので、なんとかなりそうな気もするが)
というわけで、開発環境側の VM で QEMU を使えないか考えてみる。

VM 内で QEMU を使うと開発環境をまとめられるのがうれしいが、画面転送とかがちょっと面倒。
ひとつは、グラフィック出力自体を無効にしてしまえばテキスト入出力だけになるので画面転送を考えなくて良くなるはず。
-nographic
は QEMU の起動オプションで、グラフィック出力を無効にするらしい。
Hello world ぐらいはこれで動作確認しようかなと思ったのだけど、起動しても何も出力が出てこない。
ハングしてしまったように見えるが、Ctrl + a -> x
で終了できる。
UEFI の console output は出てこないの? 正直全然良くわかってないが、いったん見送り。慣れてきたらまた考えてみよう。
いずれにしろ、ピクセル描画あたりの章に入っていったら -nographic
では対応できないはず。
というわけで、やっぱり画面転送をする方法とは向きあう必要がある。

画面転送するなら X11 forwarding を使うというのがまず考えられる。
ホスト側は XQuartz で X server を起動しておき、VM 側は適切に DISPLAY 環境変数を設定してホスト側に X11 転送すれば良い。(DISPLAY が設定されていないと gtk initialization failed
というエラーで起動しない)
この方法は前も QEMU 以外で試したことがあったし良いかなと思ったのだが、試してみるとどうも QEMU にキーボード入力等のイベントをうまく渡せていないような感じがする。
まず、Ctrl + Option + g
で focus を外してくれない。仮想 desktop の移動などで強制的に focus 外すことはできるのだけど、戻ってくるとなんかマウスの動きが不自然...。具体的には QEMU の window の左端/上端に行くと右端/下端から出てきてループする、右端からは普通に window の外に抜けられてしまうなど...
なんだか気持ち悪いけれど、原因調査はかなり大変そうなのでいったん保留。

X11 forwarding できない。さあ困ったというところで調べていたところ VNC というプロトコルで画面転送できるらしいことがわかった。
QEMU の起動オプションに -vnc :0
を付ければ VNC が使えるらしい。
MacOS ホスト側には VNC クライアントが必要になる。全く知らなかったが MacOS 組み込みで VNC クライアントが入っているらしい。Finder から Command + K
で起動する "サーバーに接続" という機能で vnc://<VM の ip address>
のように入力すれば VM に VNC で接続できる。
ここで、VNC クライアントで VM に接続する際にパスワードを入力すると言われる。そんなの設定した覚え無いが...
ということで、調べてみると以下の documentation が見つかる。
どうやら -vnc :0,password=on
で起動して、QEMU monitor で change vnc password
というコマンドを打つとパスワードを設定できるみたいだ。
でも、毎回パスワード手で設定するのはめんどくさい。なんとかならないかな... ということで調べたら secret object というものを使ってパスワードを渡せるらしい
最終的に、
-object secret,id=vncpass0,data=vnc -vnc :0,password-secret=vncpass0
という指定でデフォルトパスワードを設定することができた。
コマンドラインにパスワード直書きはセキュリティ的にはアレだけど、ローカル開発用だしまあいいでしょということで
しばらくこれでやっていこうかなと思う

Ansible 使ってみる
公式のビルド用レポジトリでは開発環境を ansible で構築する方法が用意されている。
とりあえずこれにのってみようということで、ansible playbook を使ってみることにする。
まず、ansible を全く使ったことが無かったのだが、これは要するに ansible playbook に書いてある手順を上から順に実行して環境をつくってくれるというものらしい。
おそらく本格的なユースケースとしてはそこそこ大きいクラスタ構成にまとめて同じ環境を構築するという使いかたっぽい。
環境構築する対象のホストに Python さえ入っていれば ansible 専用のエージェントは必要無く、SSH ログインして Python スクリプト実行で環境をつくってくれるらしい。
あとは、ナイーブに書いたシェルスクリプトと違ってべき等性を多くのケースで保証してくれるとか、スクリプトそのものではなく yaml の設定ファイルだけで書けるとか、yaml だけどそれなりに複雑な変数とか繰り返しとかは使えるとかそのあたりが売りなのかなという感じ
ともかく、今回のみかん本環境構築では ansible-playbook を実行しているホスト自身を環境構築対象にする。
ところでこの構築スクリプト、結構環境を大胆にいじるようで、特に大きいのが llvm 関連の alternatives を全部 llvm-14 に貼りかえる。
あとは、HOME 直下のパスが直接指定されていたりする。まあこのへんは変えてしまってもそんなに問題無さそうだけど
というわけで、VM 使うにしてもみかん本開発専用の VM 用意しといたほうが良さそうかなあという印象
トラブルシューティング
Ubuntu 24.04 だと python3-distutils はパッケージが無くなっているので ansible_provision.yml から消す
あとは、この辺のエラーを踏んだけど、あんまりよくわからず

UEFI? EDK2? OVMF?
OS より上ばっかりやっていると全然馴染みがなく、わからなすぎて悲しくなってくるのでちょっと寄り道してざっくり調べてみる。
UEFI
まず、UEFI は Unified Extensible Firmware Interface
従来 BIOS (Basic Input/Output System) が担っていたブートローダの読みこみとかの役割を標準化しておきかえるのが UEFI らしい。
もともとは Intel が EFI というのをつくっていて、これをベースに UEFI forum が標準化したのが UEFI なんだって
仕様はこれ。とっても分厚い (2301 pages)
EDK2
EDK2 は UEFI firmware や UEFI application を開発するための開発キットみたいだ。
たぶん EFI Development Kit だろうってみかん本に書いてた。
公式の説明は以下
EDK II is a modern, feature-rich, cross-platform firmware development environment for the UEFI and PI specifications.
中身はまだあまりちゃんと把握していないのだが、UEFI アプリケーション用のライブラリ (UEFILib) の実装だったり、後述の OVMF とか入っているみたいだ。
UEFILib は Print()
等含む便利関数を提供してくれるもので、実装は MdePkg
というディレクトリに入っている。
Mde は Module Development Environment らしい
OVMF
OVMF は、Open Virtual Machine Firmware の略。
どうやら、QEMU とかで動かすような VM のための UEFI Firmware らしい。
EDK2 の OVMFPkg のディレクトリに実装が置いてある。
QEMU とかでも OS ブートするには基本的には実ハードウェアと同じように UEFI Firmware やら ブートローダーやら準備する必要があって、QEMU 用の UEFI Firmware として OSS でよくつかわれるのが OVMF らしい。

MikanLoader をビルドしてみる
ビルドの動作確認を込めて以下の環境でビルドしてみる。
該当バージョンの mikanos-build に含まれる Ansible Playbook で環境構築した場合 LLVM や edk2 は以下のバージョンになっているはず。
- arch: aarch64
- Ubuntu: 24.04 (Multipass VM on M2 Mac)
- LLVM: 14.0.6
- edk2: (tag) edk2-stable202208
- mikanos-build: f8516d5e742cfa3268995bc95375a7ba368ddbd4
- mikanos: b5f7740c04002e67a95af16a5c6e073b664bf3f5
ビルド方法は本にある通り、
-
MikanLoaderPkg
への symlink をedk2/
の中に作成 -
source edksetup.sh
で環境変数を設定 -
Conf/target.txt
を以下のように編集 (中身は本の通り)
ACTIVE_PLATFORM = MikanLoaderPkg/MikanLoaderPkg.dsc
TARGET = DEBUG
TARGET_ARCH = X64
TOOL_CHAIN_CONF = Conf/tools_def.txt
TOOL_CHAIN_TAG = CLANG38
BUILD_RULE_CONF = Conf/build_rule.txt
-
build
を実行
だが、やはり素直にはビルドが通ってくれない。
まずは次のエラー
In file included from /home/ubuntu/edk2/MikanLoaderPkg/Main.c:13:
In file included from /home/ubuntu/edk2/MikanLoaderPkg/frame_buffer_config.hpp:3:
In file included from /usr/lib/llvm-14/lib/clang/14.0.6/include/stdint.h:52:
/usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found
#include <bits/libc-header-start.h>
^~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
bits/libc-header-start.h
が無いらしい。
Ubuntu Packages で探してみると、libc6-dev
や libc6-dev-amd64-cross
に入ってるらしい。
今回は x86_64 向けにクロスコンパイルしてるわけだし、libc6-dev-amd64-cross
の方なのかなあということで、とりあえず入れてみる。
入れただけだとパスを認識してくれないので、include path に足しておく。
コンパイルフラグの設定などは Conf/tools_def.txt
にあるっぽい。
というわけで、CLANG38_ALL_CC_FLAGS
というやつの末尾に -I /usr/x86_64-linux-gnu/include
を足してみる。
DEFINE CLANG38_ALL_CC_FLAGS = DEF(GCC48_ALL_CC_FLAGS) DEF(CLANG38_WARNING_OVERRIDES) -fno-stack-protector -mms-bitfields -Wno-address -Wno-shift-negative-value -Wno-unknown-pragmas -Wno-incompatible-library-redeclaration -fno-asynchronous-unwind-tables -mno-sse -mno-mmx -msoft-float -mno-implicit-float -ftrap-function=undefined_behavior_has_been_optimized_away_by_clang -funsigned-char -fno-ms-extensions -Wno-null-dereference -I /usr/x86_64-linux-gnu/include
もう一回 build
してみるとビルドでさっき止まっていた箇所は通過しているっぽいが、今度は別の場所でエラー
/usr/bin/ld: unrecognised emulation mode: elf_x86_64
Supported emulations: aarch64linux aarch64elf aarch64elf32 aarch64elf32b aarch64elfb armelf armelfb aarch64linuxb aarch64linux32 aarch64linux32b armelfb_linux_eabi armelf_linux_eabi
clang: error: linker command failed with exit code 1 (use -v to see invocation)
調べてみると、以下で同じエラーに当たっている。
どうやら、/usr/bin/ld
(GNU ld) の代わりに ld.lld
(LLVM lld) を呼び出せばいいっぽい。
これは、-fuse-ld
オプションで制御できるっぽい。
(Clang なのに GCC のマニュアルでいいの? と思うかもしれない。Clang はだいたい GCC とオプション互換になるようにつくってあって、互換なオプションが Clang 側には明示的にドキュメンテーションされてなかったりすることがあるので、GCC のほうを見るのが正解みたいなケースがあったりする)。
また Conf/tools_def.txt
をいじる。
今度は DEBUG_CLANG38_X64_DLINK_FLAGS
に -fuse-ld=lld
を足したら問題無さそうだった (たぶん、target.txt
の TARGET
の値が DEBUG
だからだと思う)。
DEBUG_CLANG38_X64_DLINK_FLAGS = DEF(GCC5_IA32_X64_DLINK_FLAGS) -flto -Wl,-Oz -Wl,-melf_x86_64 -Wl,--oformat=elf64-x86-64 -Wl,-pie -mcmodel=small -fuse-ld=lld
しかし、まだエラーが出る。
ld.lld: error: -Oz: number expected, but got 'z'
clang: error: linker command failed with exit code 1 (use -v to see invocation)
調べて出てきたのは以下。(少なくとも LLVM 14 だと) lld
が -Oz
オプション (コードサイズの最適化オプション) に対応してないっぽい?
仕方ないので、さっきいじった DEBUG_CLANG38_X64_DLINK_FLAGS
に入っている -Oz
を -O3
に変えてみる。
DEBUG_CLANG38_X64_DLINK_FLAGS = DEF(GCC5_IA32_X64_DLINK_FLAGS) -flto -Wl,-Oz -Wl,-melf_x86_64 -Wl,--oformat=elf64-x86-64 -Wl,-pie -mcmodel=small -fuse-ld=lld
まだ通らない...
"objcopy" /home/ubuntu/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/MikanLoaderPkg/Loader/DEBUG/Loader.dll
objcopy: Unable to recognise the format of the input file `/home/ubuntu/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/MikanLoaderPkg/Loader/DEBUG/Loader.dll'
GNU Binutils 版の objcopy が使われているっぽいので、また Conf/tools_def.txt
を書き換える。
*_CLANG38_X64_OBJCOPY_PATH = llvm-objcopy
*_CLANG38_X64_RC_PATH = llvm-objcopy
これでようやくビルドが通ったっぽい。
長かった...

メモ: Rust 実装参考情報
Mikan OS Rust 実装まとめ
-
ors
- 2021年9月 から 2022年3月 ごろまで
- MikanOS と blog_os からそれぞれ部分的に実装した成果物
- usb ドライバ: 無し (blog_os 相当の PS/2 keyboard 実装)
- font: ttf サポートあり(tamzen font + ab_glyph crate)
- 関連記事
-
mandarin
- 2021年5月ごろまで
- usb ドライバ: フルスクラッチ
- font: shinonome font バイナリ埋め込み (include_bytes!)
-
sabios
- 2021年8月 ごろまで
- usb ドライバ: FFI (MikanOS 実装呼び出し)
- font: shinonome font を txt からソース生成 (配列宣言)
- 関連記事
-
not-elm/mikanos-rs
- 2023年6月 ごろまで
- usb ドライバ: xhci crate 利用した独自実装
- font: FFI
- 関連記事
-
rusty-mikanos
- 2022年6月 ごろまで
- usb ドライバ: FFI
- 関連記事

フォント処理
下調べ
-
ab-glyph
- ors で使っているやつ
-
ttf-parser
- 高機能っぽい。が、rasterize はないかも?
- 記述のサンプルなどが無い
-
https://zenn.dev/kazatsuyu/scraps/4dc63eeaf3d090
- スクラップ
MikanOS
本家の MikanOS では東雲フォントを単体でバイナリ化して kernel とリンクすることで実行バイナリに静的データとして埋め込む方法をとっている
Rust で実装するにあたって、同様にリンクする方法はややビルドが難しそう
既存実装を調べたところ、Rust の場合は include_bytes!
マクロを使ってコンパイル時にフォントデータをバイト列として読みこんでしまうのが楽そうである。
ttf サポート
ors 実装では東雲フォントではなく、tamazen フォントの ttf を読み込んでいるらしいことがわかった。
ab_glyph
クレートを使って ttf をロードしている。
ttf を用いる場合 ttf 処理が複雑で中身がよくわからないという欠点があるが、ユーザーレベルでのフォントの追加などを考えたときに利便性が高そうである。
方針
今回は外部クレートを使って ttf サポートしてみたいと思う。
ab_glyph
以外にも調べたところ、 fontdue
が API が使いやすそうで、また性能も良いらしいので今回はこれを使ってみることにする。

当面先の話だが、将来的に Ethernet のサポートをやりたい
どこから手をつけていいかよくわからないので、wasabiOS の実装を見てみた。
- https://github.com/hikalium/wasabi/blob/8e23542da41be26f37d52f2be1b728c06c53fffa/Makefile#L26
- https://github.com/hikalium/wasabi/blob/8e23542da41be26f37d52f2be1b728c06c53fffa/os/src/rtl8139.rs
- https://wiki.osdev.org/RTL8139
RTL8139 というネットワークカードに対するドライバを実装しているらしい
とりあえずこのあたりから見てみると良さそう

USB ドライバを書いている
が、USB コントローラの初期化の一番最初の最初にやるレジスタの取得で OS が落ちてしまって進まない... (下の部分)
OS が落ちると再起動が走って、繰り返し同じ場所で落ちる
let mut r = unsafe { xhci::Registers::new(mmio_base, mapper) };
mmio_base
を直に *mut u32
にして dereference 試してみても同じように落ちるのでたぶん xhci crate の問題というわけではない。
ヒントがほしいので、既存実装を動かしたい。
そこで not-elm/mikanos-rs や mandarin OS を動かしてみようとしてみたが、どっちもビルドが通らない。
Rust でもやっぱりこういう系統のソフトウェアは結構ビルド苦労するんだな...
mandarin は結構古いので、最新の Rust の nightly だと削除されている feature (const_fn
) が uefi crate で使われていたりしてそのままだとビルドが通らない。
uefi crate のバージョン上げるのはありな気もするけど、これまでの経験上かなり interface が変わりそうで移植が面倒なのであまりやりたくない...
なので、ソースコードは変えずに古いコンパイラでビルドしたい。
commit 履歴の日付を見て nightly-2021-05-02
を rust-toolchains.toml
で指定してみる。
const_fn
feature のエラーとかは消えてくれたのだが、 kernel のビルドで以下が解決できない。
= note: ld: in /Users/godai/.rustup/toolchains/nightly-2021-05-02-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/lib/libpanic_unwind-2ab7e3be3fe881eb.rlib(lib.rmeta), archive member 'lib.rmeta' with length 35720 is not mach-o or llvm bitcode file '/Users/godai/.rustup/toolchains/nightly-2021-05-02-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/lib/libpanic_unwind-2ab7e3be3fe881eb.rlib' for architecture arm64
エラーメッセージで調べると rust 本体の issue がいくつか出てくる
正直よくわからないのだが、Mac でビルドするなら古い XCode 使う以外打つ手は無さそう?
XCode 13.4.1 のインストールを試しているが、結構ダウンロードに時間がかかっているので完了したら後日試そうかな
Ubuntu 側でのビルドも試してみてもよいかも
本家の MikanOS も動かせないかやってみる
edk2 がしんどいので Mac でビルドするのは最初から諦めて、Ubuntu でやっている
bootloader は前に調べた手順でビルドする (target.txt
と toos_def.txt
をちまちま書き換えるのがめんどくさいので、いい加減スクリプト化しときたいな...)
kernel ははじめてビルドしてみるが、mikanos-build の案内どおり . buildenv.sh
-> ./build.sh run
でちゃんと動かせた (Makefile
の objcopy
だけ llvm-objcopy
に書き換えた)
さすがちゃんとメンテされててすごい
ログ仕込んだりはまだできてない
MikanOS を動かせたので、 QEMU Monitor で PCI の情報を確認してみる。
そこで、顕著に違ったのが BAR に入っているアドレス
- MikanOS で動かしたとき
Bus 0, device 4, function 0:
USB controller: PCI device 1033:0194
PCI subsystem 1af4:1100
IRQ 11, pin A
BAR0: 64 bit memory at 0x800000000 [0x800003fff].
id "xhci"
- 開発中の blog_os で動かしたとき
Bus 0, device 4, function 0:
USB controller: PCI device 1033:0194
PCI subsystem 1af4:1100
IRQ 11, pin A
BAR0: 64 bit memory at 0xfebf0000 [0xfebf3fff]
id ""
QEMU のオプションで -device qemu-xhci
を -device nec-usb-xhci
に変えてみたり、ついでに -device usb-mouse
で USB デバイスを追加でくっつけてみたりしたけど変化無し
BAR って、たぶん OS 起動前にファームウェアで設定されてる気がするので、 pre-boot 環境の問題なんじゃないかと思い始める。
そういえば、 Mikan OS は UEFI だけど、 blog_os は legacy BIOS だ。
SeaBIOS だとこのへんちゃんと初期化してくれてないってことなんだろうか...
よくわからないけど、とりあえず次はブートローダを UEFI ベースにした環境で改めて試してみよう。
こんなところでまた BIOS の違いが効いてくるとは...

上記について、まだ解決していないが QEMU モニタからの見え方について追記
info mtree
というコマンドでメモリマップっぽいものが見えるらしい
これで確認してみると、bootimage 環境では確かに febf0000
の周辺が xHCI に予約されているらしいことは確認できる。
memory-region: pci
0000000000000000-ffffffffffffffff (prio -1, i/o): pci
00000000000a0000-00000000000bffff (prio 1, i/o): vga-lowmem
00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
00000000000e0000-00000000000fffff (prio 1, rom): alias isa-bios @pc.bios 0000000000020000-000000000003ffff
00000000fd000000-00000000fdffffff (prio 1, ram): vga.vram
00000000febc0000-00000000febdffff (prio 1, i/o): e1000-mmio
00000000febf0000-00000000febf3fff (prio 1, i/o): xhci
00000000febf0000-00000000febf003f (prio 0, i/o): capabilities
00000000febf0040-00000000febf043f (prio 0, i/o): operational
00000000febf0440-00000000febf044f (prio 0, i/o): usb3 port #1
00000000febf0450-00000000febf045f (prio 0, i/o): usb3 port #2
00000000febf0460-00000000febf046f (prio 0, i/o): usb3 port #3
00000000febf0470-00000000febf047f (prio 0, i/o): usb3 port #4
00000000febf0480-00000000febf048f (prio 0, i/o): usb2 port #1
00000000febf0490-00000000febf049f (prio 0, i/o): usb2 port #2
00000000febf04a0-00000000febf04af (prio 0, i/o): usb2 port #3
00000000febf04b0-00000000febf04bf (prio 0, i/o): usb2 port #4
00000000febf1000-00000000febf121f (prio 0, i/o): runtime
00000000febf2000-00000000febf281f (prio 0, i/o): doorbell
00000000febf3000-00000000febf30ff (prio 0, i/o): msix-table
00000000febf3800-00000000febf3807 (prio 0, i/o): msix-pba
00000000febf4000-00000000febf4fff (prio 1, i/o): vga.mmio
00000000febf4000-00000000febf417f (prio 0, i/o): edid
00000000febf4400-00000000febf441f (prio 0, i/o): vga ioports remapped
00000000febf4500-00000000febf4515 (prio 0, i/o): bochs dispi interface
00000000febf4600-00000000febf4607 (prio 0, i/o): qemu extended regs
00000000fffc0000-00000000ffffffff (prio 0, rom): pc.bios
しかし、 x/1x 0xfebf0000
など適当なコマンドでメモリの内容を読もうとしてみると、 "Cannot access memory" のエラーになってしまう。
(qemu) x/1x 0xfebf0000
00000000febf0000: Cannot access memory
一方、 MikanOS を動かして同様に info mtree
の出力を確認してみると、アドレス範囲以外はほぼ同様の出力
memory-region: pci
0000000000000000-ffffffffffffffff (prio -1, i/o): pci
00000000000a0000-00000000000affff (prio 2, ram): alias vga.chain4 @vga.vram 0000000000000000-000000000000ffff
00000000000a0000-00000000000bffff (prio 1, i/o): vga-lowmem
00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
00000000000e0000-00000000000fffff (prio 1, rom): isa-bios
0000000080000000-0000000080ffffff (prio 1, ram): vga.vram
0000000081000000-000000008101ffff (prio 1, i/o): e1000-mmio
0000000081020000-0000000081020fff (prio 1, i/o): vga.mmio
0000000081020000-000000008102017f (prio 0, i/o): edid
0000000081020400-000000008102041f (prio 0, i/o): vga ioports remapped
0000000081020500-0000000081020515 (prio 0, i/o): bochs dispi interface
0000000081020600-0000000081020607 (prio 0, i/o): qemu extended regs
0000000800000000-0000000800003fff (prio 1, i/o): xhci
0000000800000000-000000080000003f (prio 0, i/o): capabilities
0000000800000040-000000080000043f (prio 0, i/o): operational
0000000800000440-000000080000044f (prio 0, i/o): usb3 port #1
0000000800000450-000000080000045f (prio 0, i/o): usb3 port #2
0000000800000460-000000080000046f (prio 0, i/o): usb3 port #3
0000000800000470-000000080000047f (prio 0, i/o): usb3 port #4
0000000800000480-000000080000048f (prio 0, i/o): usb2 port #1
0000000800000490-000000080000049f (prio 0, i/o): usb2 port #2
00000008000004a0-00000008000004af (prio 0, i/o): usb2 port #3
00000008000004b0-00000008000004bf (prio 0, i/o): usb2 port #4
0000000800001000-000000080000121f (prio 0, i/o): runtime
0000000800002000-000000080000281f (prio 0, i/o): doorbell
0000000800003000-00000008000030ff (prio 0, i/o): msix-table
0000000800003800-0000000800003807 (prio 0, i/o): msix-pba
一方、こちらでは x
コマンドでメモリの内容を読むことができている。
(qemu) x/1x 0x800000000
0000000800000000: 0x01000040
(qemu) x/4x 0x800000000
0000000800000000: 0x01000040 0x08001040 0x0000000f 0x00000000

bootloader を UEFI ベースのものに変えたら自作 USB ドライバでも xhci レジスタの初期化 (BAR アドレスへのアクセス) ができた
Legacy BIOS でなんで落ちるのかは謎だが、ようやく第一歩
[ INFO]: bootloader/src/main.rs@084: Hello, bootloader!
[ INFO]: bootloader/src/main.rs@075: Read kernel file: size=3046080
[ INFO]: bootloader/src/main.rs@097: Successfully loaded kernel!
[ INFO]: bootloader/src/main.rs@099: Exiting boot services...
Hello, serial port!
0.0.0: vendor 8086, Host Bridge (06.00.00), header type 00
0.1.0: vendor 8086, ISA Bridge (06.01.00), header type 80
0.1.1: vendor 8086, IDE Controller (01.01.80), header type 00
0.1.3: vendor 8086, Bridge (06.80.00), header type 00
0.2.0: vendor 1234, VGA Compatible Controller (03.00.00), header type 00
0.3.0: vendor 8086, Ethernet Controller (02.00.00), header type 00
0.4.0: vendor 1b36, USB Controller (0c.03.30), header type 00
PCI Bus enumeration done.
xHCI controller: PCIAddress { bus_num: 0, device_num: 4, function_num: 0 }
mmio_base: 800000000
Succesfully initialized xhci registers!
All done.

USB ドライバ開発用のリポジトリをつくった

Pre-OS to OS handoff について
xHCI 仕様書の 4.22 に "Pre-OS to OS handoff Synchronization" という項目がある。
詳細はよく理解できていないのだが、 BIOS 等の Pre-OS 環境から OS に host controller の所有権を移すという作業が必要らしい。
Extended capability なので host controller によっては実装されていないこともあるかもしれない。
MikanOS の実装ではこの handoff を実装しているようだ。
MikanOS では host controller の reset より前に一番はじめにこの handoff を実行している。
一方、Mandarin OS では run_stop = 0
を書き込んで host controller の停止を確認した後でこのステップを実行している。
not-elm/mikanos-rs や WasabiOS では確認した限りではこの handoff は実装していないように思われる。
実装していない OS もあるので必須では無さそうだが、どちらかというと実装したほうが良さそうな感じはする。しかし、 MikanOS と MandarinOS で実行する順番が異なっており、正しい順番に確信が持てない。
(xHCI を読んでも記述が見当たらない。 4.2 の Host Controller Initialization ではこの手順について特に言及が見当たらない。)
現状で比較的信用できそうなのはなんだろうかと思って、 Linux の実装をあたってみることにした。
ソースコードを調べてみると、以下のあたりで Pre-OS to OS handoff を実装していそう。
これがどのタイミングで呼び出されるのか調べてみると、 PCI fixup という仕組みで hook として登録されているようである。
DECLARE_PCI_FIXUP_CLASS_FINAL
というマクロは include/linux/pci.h
で定義されていて、どうやら特定のセクションに hook される関数のアドレスを含めた pci_fixup
構造体の変数を ELF の特定のセクションに宣言するようなものらしい。
余談だが、 "The world is not perfect and supplies us with broken PCI devices." というコメントがちょっと面白い。このコメントは少なくとも linux-2.6.12-rc2 の時点ではすでに存在しているようである。 commit
おそらく、実際に fixup を呼び出しているのは PCI ドライバの以下のあたり
細かい実行順を追うのは難しいが、おそらく PCI ドライバ側から PCI デバイス初期化時完了時に呼んでいるものと思われ、これは xHCI ドライバの初期化より早く実行されていると思われる。
したがって、おそらく実行順序としては MikanOS と同じ「他のあらゆる xHCI 初期化より先に Pre-OS to OS handoff を実行する」が「現実的には正しそう」である。
しかし、この順番が正しいという根拠は見つけられてはいない。

xHCI handoff について調べると出てくるのは、だいたい forum のやりとりとかで、 BIOS の設定について議論しているもの
Handoff を on にしないと USB 3.0 が使えないだとか、いやむしろ Enabled にするのは古い OS 用の互換性のための機能であって新しい OS では Disabled にすべきだとか、いまいちはっきりしない

xHCI handoff について、とりあえず extended capbility list の走査まで実装したのだが、 QEMU 環境 (qemu-xhci) だと結局 USB legacy support capability を実装していなさそうであることがわかった。つまり、QEMU で動かすだけなら特に handoff は実装しなくても良さそう...。
実機では試していないので、そのうちなにがしかの実機で試したい。

割り込みまわりの仕様のメモ
「どこに書いてあったっけ」を参照しやすくするため
参照元は特に明記しない限り Intel SDM を指す。
End of Interrupt
- Vol. 3 12.8.5 Signaling Interrupt Servicing Completion
For all interrupts except those delivered with the NMI, SMI, INIT, ExtINT, the start-up, or INIT-Deassert delivery mode, the interrupt handler must include a write to the end-of-interrupt (EOI) register (see Figure 12-21). This write must occur at the end of the handler routine, sometime before the IRET instruction. This action indicates that the servicing of the current interrupt is complete and the local APIC can issue the next interrupt from the ISR.
(NMI, SMI 等の例外を除き) Interrupt handler は IRET
による復帰前に EOI register に書き込まなければならない。
この書き込みは APIC に対して割り込み処理の終了を伝え、次の割り込みの発行を許可する。
- EOI register の仕様 (Figure 12-21)
Address: 0FEE0 00B0H
Value after reset: 0H
Interrupt gate と Trap gate の違いについて
- Vol. 1 6.5.1 Call and Return Operation for Interrupt or Exception Handling Procedures
The difference between an interrupt gate and a trap gate is as follows. If an interrupt or exception handler is called through an interrupt gate, the processor clears the interrupt enable (IF) flag in the EFLAGS register to prevent subsequent interrupts from interfering with the execution of the handler. When a handler is called through a trapgate, the state of the IF flag is not changed.
Interrupt gate の場合ハンドラ実行中に追加の割り込みは許可しないが、 trap gate の場合は割り込み許可状態を変更しない。