Open21

みかん本

sankantsusankantsu

ゼロからの OS 自作入門 (みかん本) やっていく

メモしていきたい項目

  • 寄り道して調べたこととか
  • (サポート外の) 環境構築とか
  • 参考になった記事
  • 理解を定着させるための内容のメモとか
sankantsusankantsu

使っている環境

  • M2 Macbook Air (aarch64, MacOS 13.7)

本での想定標準環境は ubuntu 18.04 or 20.04 の x86_64 環境だから、環境構築の時点でだいぶハマりどころがある

Mac でやるならざっと調べた感じ以下のあたりが参考になりそう?

https://qiita.com/yamoridon/items/4905765cc6e4f320c9b5
https://zenn.dev/karaage0703/articles/1bdb8930182c6c
https://blog.yotio.jp/2022/08/19/OS自作入門日記-1-M1-Macに-ゼロからのOS自作入門-の環境導入.html

sankantsusankantsu

開発環境構築の方針としてだいたい以下のどれかを使うことになる

  • 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 使うつもり無いしなあというので見送り

https://zenn.dev/sarisia/articles/6b57ea835344b6
https://github.com/sarisia/mikanos-docker?tab=readme-ov-file

なお、ビルド対象の OS も aarch64 ターゲットにしても良いのだけど、そうするとまたいろいろ変わりそう (OVMF のビルドとかがちょっと面倒そう? もしアセンブリ直書きするところとかあると面倒そう) な気がするので、とりあえずターゲットは x86_64 ということにしてクロスコンパイル前提で進めていこうと思う

sankantsusankantsu

あとは、QEMU をどこで実行するかという選択肢も一応ある。
VM の中でビルドして、QEMU は full system emulation してくれるはずなのでゲスト OS を動かす上ではホストは Mac だろうと Ubuntu だろうと基本は関係無いはず。

MacOS 側で QEMU を使えば GUI まわりの面倒臭さは少ない。
一方、QEMU 起動するためだけに VM から環境切り替えてくるのがちょっと面倒かも。
(ホスト側のディレクトリをマウントして VM から直接いじればビルド結果をコピーしてくるとかはしなくてもいいはずなので、なんとかなりそうな気もするが)

というわけで、開発環境側の VM で QEMU を使えないか考えてみる。

sankantsusankantsu

VM 内で QEMU を使うと開発環境をまとめられるのがうれしいが、画面転送とかがちょっと面倒。

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

というわけで、やっぱり画面転送をする方法とは向きあう必要がある。

sankantsusankantsu

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

sankantsusankantsu

X11 forwarding できない。さあ困ったというところで調べていたところ VNC というプロトコルで画面転送できるらしいことがわかった。

QEMU の起動オプションに -vnc :0 を付ければ VNC が使えるらしい。
MacOS ホスト側には VNC クライアントが必要になる。全く知らなかったが MacOS 組み込みで VNC クライアントが入っているらしい。Finder から Command + K で起動する "サーバーに接続" という機能で vnc://<VM の ip address> のように入力すれば VM に VNC で接続できる。
ここで、VNC クライアントで VM に接続する際にパスワードを入力すると言われる。そんなの設定した覚え無いが...

ということで、調べてみると以下の documentation が見つかる。

https://www.qemu.org/docs/master/system/vnc-security.html

どうやら -vnc :0,password=on で起動して、QEMU monitor で change vnc password というコマンドを打つとパスワードを設定できるみたいだ。
でも、毎回パスワード手で設定するのはめんどくさい。なんとかならないかな... ということで調べたら secret object というものを使ってパスワードを渡せるらしい

https://www.qemu.org/docs/master/system/secrets.html

最終的に、

-object secret,id=vncpass0,data=vnc -vnc :0,password-secret=vncpass0

という指定でデフォルトパスワードを設定することができた。
コマンドラインにパスワード直書きはセキュリティ的にはアレだけど、ローカル開発用だしまあいいでしょということで

しばらくこれでやっていこうかなと思う

sankantsusankantsu

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 から消す

あとは、この辺のエラーを踏んだけど、あんまりよくわからず

https://stackoverflow.com/questions/59282476/error-rpc-failed-curl-92-http-2-stream-0-was-not-closed-cleanly-protocol-erro

sankantsusankantsu

UEFI? EDK2? OVMF?

OS より上ばっかりやっていると全然馴染みがなく、わからなすぎて悲しくなってくるのでちょっと寄り道してざっくり調べてみる。

UEFI

https://uefi.org/

https://wiki.osdev.org/UEFI

まず、UEFI は Unified Extensible Firmware Interface
従来 BIOS (Basic Input/Output System) が担っていたブートローダの読みこみとかの役割を標準化しておきかえるのが UEFI らしい。
もともとは Intel が EFI というのをつくっていて、これをベースに UEFI forum が標準化したのが UEFI なんだって

仕様はこれ。とっても分厚い (2301 pages)

https://uefi.org/sites/default/files/resources/UEFI_Spec_Final_2.11.pdf

EDK2

https://github.com/tianocore/tianocore.github.io/wiki/EDK-II

https://github.com/tianocore/edk2

https://wiki.osdev.org/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 らしい

https://github.com/tianocore/tianocore.github.io/wiki/MdePkg

OVMF

https://github.com/tianocore/tianocore.github.io/wiki/OVMF

https://wiki.osdev.org/OVMF

OVMF は、Open Virtual Machine Firmware の略。
どうやら、QEMU とかで動かすような VM のための UEFI Firmware らしい。
EDK2 の OVMFPkg のディレクトリに実装が置いてある。

QEMU とかでも OS ブートするには基本的には実ハードウェアと同じように UEFI Firmware やら ブートローダーやら準備する必要があって、QEMU 用の UEFI Firmware として OSS でよくつかわれるのが OVMF らしい。

sankantsusankantsu

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

ビルド方法は本にある通り、

  1. MikanLoaderPkg への symlink を edk2/ の中に作成
  2. source edksetup.sh で環境変数を設定
  3. 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
  1. 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-devlibc6-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)

調べてみると、以下で同じエラーに当たっている。

https://zenn.dev/link/comments/23eab2b6b5bb45

どうやら、/usr/bin/ld (GNU ld) の代わりに ld.lld (LLVM lld) を呼び出せばいいっぽい。
これは、-fuse-ld オプションで制御できるっぽい。

https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html#index-fuse-ld_003dlld

(Clang なのに GCC のマニュアルでいいの? と思うかもしれない。Clang はだいたい GCC とオプション互換になるようにつくってあって、互換なオプションが Clang 側には明示的にドキュメンテーションされてなかったりすることがあるので、GCC のほうを見るのが正解みたいなケースがあったりする)。

また Conf/tools_def.txt をいじる。
今度は DEBUG_CLANG38_X64_DLINK_FLAGS-fuse-ld=lld を足したら問題無さそうだった (たぶん、target.txtTARGET の値が 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 オプション (コードサイズの最適化オプション) に対応してないっぽい?

https://reviews.llvm.org/D63976

仕方ないので、さっきいじった 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

これでようやくビルドが通ったっぽい。
長かった...

sankantsusankantsu

メモ: Rust 実装参考情報

Mikan OS Rust 実装まとめ

sankantsusankantsu

フォント処理

下調べ

MikanOS

本家の MikanOS では東雲フォントを単体でバイナリ化して kernel とリンクすることで実行バイナリに静的データとして埋め込む方法をとっている
Rust で実装するにあたって、同様にリンクする方法はややビルドが難しそう

既存実装を調べたところ、Rust の場合は include_bytes! マクロを使ってコンパイル時にフォントデータをバイト列として読みこんでしまうのが楽そうである。

ttf サポート

ors 実装では東雲フォントではなく、tamazen フォントの ttf を読み込んでいるらしいことがわかった。
ab_glyph クレートを使って ttf をロードしている。
ttf を用いる場合 ttf 処理が複雑で中身がよくわからないという欠点があるが、ユーザーレベルでのフォントの追加などを考えたときに利便性が高そうである。

方針

今回は外部クレートを使って ttf サポートしてみたいと思う。
ab_glyph 以外にも調べたところ、 fontdue が API が使いやすそうで、また性能も良いらしいので今回はこれを使ってみることにする。
https://docs.rs/fontdue/latest/fontdue/

sankantsusankantsu

当面先の話だが、将来的に Ethernet のサポートをやりたい
どこから手をつけていいかよくわからないので、wasabiOS の実装を見てみた。

RTL8139 というネットワークカードに対するドライバを実装しているらしい
とりあえずこのあたりから見てみると良さそう

sankantsusankantsu

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-02rust-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 がいくつか出てくる

https://github.com/rust-lang/rust/issues/107252
https://github.com/rust-lang/rust/issues/105167

正直よくわからないのだが、Mac でビルドするなら古い XCode 使う以外打つ手は無さそう?
XCode 13.4.1 のインストールを試しているが、結構ダウンロードに時間がかかっているので完了したら後日試そうかな
Ubuntu 側でのビルドも試してみてもよいかも

本家の MikanOS も動かせないかやってみる
edk2 がしんどいので Mac でビルドするのは最初から諦めて、Ubuntu でやっている
bootloader は前に調べた手順でビルドする (target.txttoos_def.txt をちまちま書き換えるのがめんどくさいので、いい加減スクリプト化しときたいな...)
kernel ははじめてビルドしてみるが、mikanos-build の案内どおり . buildenv.sh -> ./build.sh run でちゃんと動かせた (Makefileobjcopy だけ 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 の違いが効いてくるとは...

sankantsusankantsu

上記について、まだ解決していないが 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
sankantsusankantsu

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.
sankantsusankantsu

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 を実行している。

https://github.com/uchan-nos/mikanos/blob/b5f7740c04002e67a95af16a5c6e073b664bf3f5/kernel/usb/xhci/xhci.cpp#L379

一方、Mandarin OS では run_stop = 0 を書き込んで host controller の停止を確認した後でこのステップを実行している。

https://github.com/algon-320/mandarin/blob/8ce47c8657ce2a860f071c02d6dc88cb4bc592a2/kernel/src/usb.rs#L466

not-elm/mikanos-rs や WasabiOS では確認した限りではこの handoff は実装していないように思われる。

実装していない OS もあるので必須では無さそうだが、どちらかというと実装したほうが良さそうな感じはする。しかし、 MikanOS と MandarinOS で実行する順番が異なっており、正しい順番に確信が持てない。
(xHCI を読んでも記述が見当たらない。 4.2 の Host Controller Initialization ではこの手順について特に言及が見当たらない。)

現状で比較的信用できそうなのはなんだろうかと思って、 Linux の実装をあたってみることにした。
ソースコードを調べてみると、以下のあたりで Pre-OS to OS handoff を実装していそう。

https://github.com/torvalds/linux/blob/c4b9570cfb63501638db720f3bee9f6dfd044b82/drivers/usb/host/pci-quirks.c#L1181-L1204

これがどのタイミングで呼び出されるのか調べてみると、 PCI fixup という仕組みで hook として登録されているようである。

https://github.com/torvalds/linux/blob/c4b9570cfb63501638db720f3bee9f6dfd044b82/drivers/usb/host/pci-quirks.c#L1296-L1297

DECLARE_PCI_FIXUP_CLASS_FINAL というマクロは include/linux/pci.h で定義されていて、どうやら特定のセクションに hook される関数のアドレスを含めた pci_fixup 構造体の変数を ELF の特定のセクションに宣言するようなものらしい。

https://github.com/torvalds/linux/blob/c4b9570cfb63501638db720f3bee9f6dfd044b82/include/linux/pci.h#L2227-L2232

https://github.com/torvalds/linux/blob/c4b9570cfb63501638db720f3bee9f6dfd044b82/include/linux/pci.h#L2159-L2176

余談だが、 "The world is not perfect and supplies us with broken PCI devices." というコメントがちょっと面白い。このコメントは少なくとも linux-2.6.12-rc2 の時点ではすでに存在しているようである。 commit

おそらく、実際に fixup を呼び出しているのは PCI ドライバの以下のあたり

https://github.com/torvalds/linux/blob/c4b9570cfb63501638db720f3bee9f6dfd044b82/drivers/pci/quirks.c#L277

https://github.com/torvalds/linux/blob/c4b9570cfb63501638db720f3bee9f6dfd044b82/drivers/pci/quirks.c#L187

細かい実行順を追うのは難しいが、おそらく PCI ドライバ側から PCI デバイス初期化時完了時に呼んでいるものと思われ、これは xHCI ドライバの初期化より早く実行されていると思われる。
したがって、おそらく実行順序としては MikanOS と同じ「他のあらゆる xHCI 初期化より先に Pre-OS to OS handoff を実行する」が「現実的には正しそう」である。
しかし、この順番が正しいという根拠は見つけられてはいない。

sankantsusankantsu

xHCI handoff について調べると出てくるのは、だいたい forum のやりとりとかで、 BIOS の設定について議論しているもの

https://www.tenforums.com/performance-maintenance/151062-xhci-handoff-bios-setting-question.html
https://forums.blurbusters.com/viewtopic.php?t=13434
https://www.thewindowsclub.com/what-is-xhci-hand-off-in-bios-of-windows

Handoff を on にしないと USB 3.0 が使えないだとか、いやむしろ Enabled にするのは古い OS 用の互換性のための機能であって新しい OS では Disabled にすべきだとか、いまいちはっきりしない

sankantsusankantsu

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

sankantsusankantsu

割り込みまわりの仕様のメモ

「どこに書いてあったっけ」を参照しやすくするため
参照元は特に明記しない限り 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 の場合は割り込み許可状態を変更しない。