みかん本

ゼロからの 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 の場合は割り込み許可状態を変更しない。

PCI Configuration Space へのアクセスにおける alignment
MSI 割り込み (PCI 2.2 Specification §6.8) の実装を確認している際に、 enable bit を含めた Message Control ([31:16]) フィールドへの書き込みのアラインメントについて気になった。
MikanOS の実装を確認すると Capability Pointer 先頭からの 4 byte 領域をまとめて読み書きしている。
なお、このようなアクセスで readonly である CAP_ID や NXT_PTR のようなフィールドへの書き込みは無視される (何も起こらない) ことは仕様で決まっているようだ (6.8)。
Read-only registers return valid data when read and write operations have no effect.
Message control の位置している上位 16 bit のみを WORD 単位でアクセスするのは不正なのだろうか?
Intel SDM Vol2 によれば、 port I/O を実行する in/out 命令自体は 8bit/16bit/32bit いずれも対応しているようである。
OSDev Wiki の PCI のページには以下の記述がある (出典は明記なし)。
The least significant byte selects the offset into the 256-byte configuration space available through this method. Since all reads and writes must be both 32-bits and aligned to work on all implementations, the two lowest bits of CONFIG_ADDRESS must always be zero, with the remaining six bits allowing you to choose each of the 64 32-bit words. If you don't need all 32 bits, you'll have to perform the unaligned access in software by aligning the address, followed by masking and shifting the answer.
PCI specification を探すと、ソフトウェアによる PCI Configuration Space へのアクセスについては §3.2.2.3.2 Software Generation of Configuration Transactions に記述があった。
Two DWORD I/O locations are used to generate configuration transactions for PC-AT compatible systems. The first DWORD location (CF8h) references a read/write register that is named CONFIG_ADDRESS. The second DWORD address (CFC'h) references a read/write register named CONFIG_DATA.
以上から CONFIG_ADDRESS, CONFIG_DATA の I/O space におけるアドレスや、それらが 32 bit の register として機能することが読み取れる。
また、 以下から CONFIG_ADDRESS の下位 8 bit (ひとつの function の Configuration Space 内のアドレス) に指定するアドレスは 4 byte alignment が要求されることが読み取れる。
Bits 7 through 2 choose a DWORD in the device's Configuration Space. Bits 1 and 0 are read-only and must return 0's when read.
これらはあくまで PC-AT 互換機に対する仕様ということで、現代の x86, x86_64 システムがこれに従うことは PCI 仕様では指定されないのだが、有力な根拠にはなるだろう。
参考
- PCI Local Bus Specification Revision 2.2
- https://wiki.osdev.org/PCI#Disclaimer

MSI の Message Data で Delivery Mode と Trigger Mode の設定がよくわからない。
Fixed Mode では edge trigger と level trigger のいづれも許可しているようだ (Intel SDM Vol 3 12.11.2)。
Deliver the signal to all the agents listed in the destination. The Trigger Mode for
fixed delivery mode can be edge or level.
Edge trigger か Level trigger のどちらを用いるか、どちらを用いるかどのように決めればよいのだろうか?
MikanOS では Fixed Mode かつ Level Trigger としているようだ。
判断の根拠は調べた範囲ではわからなかった。
次に見るなら Linux の実装あたり?

(MSI) Message Address の Destination ID について
MSI の割り込みメッセージ送信先について
Intel SDM Vol 3 12.11.1 Message Address Register Format では Destination ID について以下のように説明している
Destination ID — This field contains an 8-bit destination ID. It identifies the message’s target processor(s). The destination ID corresponds to bits 63:56 of the I/O APIC Redirection Table Entry if the IOAPIC is used to dispatch the interrupt to the processor(s).
I/O APIC 関連の記述は一応あるが destination ID として指定できる値の候補、ありうる解釈が明記されているわけではないように思われる。
少なくとも "Processor を指定する値" が Local APIC ID だとは書いていない。
関連する記述を探すと、 Vol 3 12.6.2 Determining IPI Destination 以下の 12.6.2.1 Physical Destination Mode に以下の記述がある。
In physical destination mode, the destination processor is specified by its local APIC ID (see Section 12.4.6, “Local
APIC ID”).
少なくとも IPI (inter-processor interrupts) では destination processor の指定として Local APIC ID が使われることがわかる。
MSI でも (I/O APIC ではなく) processor を宛先とする場合には同じ仕様に基づいていそうな雰囲気はある。

割り込みハンドラデバッグ記録 - その 1 -
MSI を設定し (たつもりになっ) ても、 USB host controller からの割り込みが発生している様子がいっこうにない。
いろいろデバッグに苦労しているので、その記録
MSI configuration の確認
自分の実装とオリジナルの MikanOS で PCI Configuration 空間上の MSI capability 関連の書き込み内容を比較してみる。
MikanOS では multipass VM 上でビルドしている環境で Log(kInfo, ...)
を適当に埋めて観察している。
一通り書き込み内容を確認したが、全く同じに見える。
念の為、 MSI の設定を書き込み終わったあとで同箇所のレジスタの読み込みも試してみたが、読んでくる内容は見たところ同じ。
ということでいったん問題なさそうという結論
USB host controller 側の割り込み設定の確認
もしかすると MikanOS 側から借りている USB ドライバで host controller の割り込み設定をするのに Initialize()
以外の呼び出しが要るのでは、と思い一応ソースを確認
uchan 著の USB 3.0 ホストドライバ自作入門 を確認すると、 xHCI での割り込み設定は以下の操作が必要
1. IMOD.IMODI に4000 を書く
3. IMAN.IP とIMAN.IE に1 を書く
4. USBCMD.IE に1 を書く
5. Message Address,Message Data の値を決める
6. MSI レジスタにMessage Address とMessage Data を設定する
IMODI の設定だけ見当たらないが、ソースでは以下の箇所のあたりで IMAN, USBCMD の必要な設定はやっていそう
これが正常に完了しているか完全には確認していないが、状況証拠としては自分の実装以外ではそのまま動いているようなので、ここに問題がある可能性は少ないとみてスキップ
Interrupt Discriptor Table の確認
次に怪しいのは、 IDT まわりの設定ということになる。
とりあえず、 sidt 命令で IDTR の内容を読み込んでみて、ちゃんと設定されてそうかどうか見てみる。
すると、意図通り limit = 256, base = <Address of first byte of IDT> になっていることが確認できたので lidt の実行自体は意図どおりできていそうに見える。
いきなりユーザー定義割り込みで検証するのも大変なので、適当な zero division error (#DE = 0) の割り込みハンドラを追加して、ゼロ除算発生で exception handling されるか確認してみる。
なお、 Rust だと言語側でゼロ除算がチェックされて panic!()
が呼ばれるようなので、チェックされないように inline assembly で以下のように書いて CPU で直接ゼロ除算が出るようにした。
let x = 0;
unsafe {
core::arch::asm!("div {}", in(reg) x);
}
結果 exception handler が呼ばれず再起動を繰り返す挙動になったので、 IDT の設定が何かおかしそう。
ソースを眺めると、 InterruptDescriptor の struct で memory layout が陽に指定されていない (repr(Rust)
) なのがどうも怪しそうに見えた。
padding はつかないだろうということで repr(packed)
はつけずにとりあえず repr(C)
だけ書いてみる。 (一応、 packed
も指定しておいたほうがいいのかな、どうせ align は IDT の配列側で指定しているのだし)
Layout 設定後、動作確認してみると #DE の handler が呼ばれるようになった。
ということはやはり repr(Rust)
は何か悪さしていたようだ。
詳細には原因調査していないが、例えば _reserved
のフィールドが最適化で飛んだとかはありうるかも?
また、 #DE に続いて #DF (Double Fault) もきちんと呼ばれることが確認できた。
その後 USB の割り込みが発生するか再度確認してみるが、こちらはまだダメ。
まだ何か問題がありそうだ...

割り込みハンドラデバッグ記録 - その 2 -
INT 命令によるソフトウェア割り込み
今のところまだ一度も xHCI handler (vector = 0x40) が呼ばれるのを確認できていない。
この部分について IDT 側の設定がうまくいっているか確認したい。
INT 命令を使えば割り込み vector を指定して任意の割り込みを発生させられるのではないかと思いついた。
int 0x00
をやってみると、 zero division の際と同様に 0 番の handler が呼ばれるのが確認できた。
しかし、 int 0x40
を実行してみると xHCI handler は呼ばれず double fault が発生してしまった。
なお、 double fault を登録していないと再起動が起こる。
これは予想外の挙動であり何かおかしい気がする...
IDTR の limit の設定値
INT 命令の reference をチェックしていて気になったのは、以下の部分
IF ((vector_number « 4) + 15) is not in IDT limits
or selected IDT descriptor is not an interrupt-, or trap-gate type
THEN #GP(error_code(vector_number,1,EXT));
(* idt operand to error_code set because vector is used *)
FI;
limit のチェックがあるのだが、 byte offset と limit を比較しているように見える。
sidt で確認した通り、現在の limit の設定は 256 になっていて、これは byte count ではなくテーブルの entry 数である。
0x40 * 16 + 15 > 256
なので、このチェックに引っかかって #GP が発生しているとすると、 #GP の handler がなくて double fault になるというのは辻褄が合う気がする。
limit の設定を間違えている気がしてきたので SDM を読み直すと、 Vol 3, 7.10 に以下の記述がある。
The limit value is expressed in bytes and is added to the base address to get the address of the last valid byte.
やはり limit は byte offset の最大値を設定するようだ。
limit = 256 * 16 - 1 (= 4095) に設定し直したところ、 int 0x40
で xHCI handler が呼ばれるようになった。
しかし、以前として host controller 経由での外部割り込みでは xHCI handler が呼ばれない...
あとは何がおかしいのだろう...

割り込みハンドラデバッグ記録 - その 3 -
MikanOS 実装の再確認
なにか MikanOS との差異がないか改めて確認していたところ sti
という命令の実行があった。
__asm__("sti");
IF (Interrupt Flag)
これが気になったので、調べてみると以下のような命令らしい (Intel SDM Vol2)
STI—Set Interrupt Flag
Set interrupt flag; external, maskable interrupts enabled at the end of the next instruction.
いかにも割り込みに関係ありそうだ。
Interrupt flag というのは、 EFLAGS レジスタの第 9 bit で "maskable hardware interrupts" に対してプロセッサが反応するかどうかを切り替えるフラグらしい (Vol3, 2.3 SYSTEM FLAGS AND FIELDS IN THE EFLAGS REGISTER)。
Interrupt enable (bit 9) — Controls the response of the processor to maskable hardware interrupt
requests (see also: Section 7.3.2, “Maskable Hardware Interrupts”). The flag is set to respond to maskable hardware interrupts; cleared to inhibit maskable hardware interrupts.
なお、EFLAGS レジスタは 64bit 環境では 64 bit に拡張されて RFLAGS と呼ばれるようだ (Vol1, 3.4.3.4 RFLAGS Register in 64-Bit Mode または Vol3, 2.3.1 System Flags and Fields in IA-32e Mode)。
Maskable Hardware Interrupts
Maskable hardware interrupts という用語についてもう少し詳しく確認しておく。
Vol 3, 7.3.2 Maskable Hardware Interrupts より引用:
Any external interrupt that is delivered to the processor by means of the INTR pin or through the local APIC is called a maskable hardware interrupt.
Local APIC 経由の割り込みは maskable hardware interrupt であるらしい。
Vol3, 7.8.1 Masking Maskable Hardware Interrupts には以下のように説明がある。
The IF flag can disable the servicing of maskable hardware interrupts received on the processor’s INTR pin or through the local APIC (see Section 7.3.2, “Maskable Hardware Interrupts”). When the IF flag is clear, the processor inhibits interrupts delivered to the INTR pin or through the local APIC from generating an internal interrupt request; when the IF flag is set, interrupts delivered to the INTR or through the local APIC pin are processed as normal external interrupts.
IF flag の説明に書いてあったところとほぼ同様だが、以下のような内容
- IF を clear することで local APIC 経由で配送される割り込み要求を禁止する
- IF を set することで local APIC 経由の割り込みを通常の外部割り込みとして処理させる
以下の説明から、電源投入後の初期状態では IF = 0 であることが示唆されるように思われる。
As with the other flags in the EFLAGS register, the processor clears the IF flag in response to a hardware reset.
IF の状態は STI, CLI 命令で操作できるとある。
The IF flag can be set or cleared with the STI (set interrupt-enable flag) and CLI (clear interrupt-enable flag) instructions, respectively.
現在の状態の確認
EFLAGS register の状態の確認には以下の方法がありそうだ。
- PUSHF 命令を用いて EFLAGS レジスタの内容を stack に書き出す
- (QEMU 環境) QEMU monitor の
info registers
コマンドを使用してレジスタの状態をダンプする
実機環境なら PUSHF を使うのが良さそうだが、 QEMU 環境であれば後者の方法のほうが追加コードも必要なく手軽そうである。
というわけで、現在の (USB 割り込みが配送されない状態の) mikanos-rs で info registers
コマンドを叩いてみる。
出力の最初のほうだけ抜き出すと以下のような内容だった。
CPU#0
RAX=00000000001ff830 RBX=0000000000000000 RCX=0000000000203000 RDX=0000000000000001
RSI=0000000000000000 RDI=0000000000245c28 RBP=000000000679b4c8 RSP=00000000001ff8e0
R8 =0000000000000000 R9 =fefefefefefefeff R10=00000000002410b8 R11=00000000001ff3c2
R12=0000000007227218 R13=0000000007227e98 R14=8000000000000002 R15=0000000000000132
RIP=000000000020e479 RFL=00000006 [-----P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
RFL というのが RFLAGS らしいように思われる。
取得したタイミングでは PF (Parity Flag) というものしか set されていない状態だったようだ。
予想どおり、 IF は clear されていると読み取れる。
IF を set
以下のようなコードを足して、きちんと IF が set されるか確認
unsafe {
core::arch::asm!("sti");
}
loop {}
RFL の bit 9 (0x00000200
の位置) が set されているから、問題なさそう。
CPU#0
RAX=00000000002006d0 RBX=0000000000000000 RCX=0000000000202700 RDX=0000000000000001
RSI=0000000000000000 RDI=0000000000237d68 RBP=000000000679b4c8 RSP=00000000002008b0
R8 =0000000000000000 R9 =0000000000000007 R10=0000000000233888 R11=000000000020035e
R12=0000000007227218 R13=0000000007227e98 R14=8000000000000002 R15=0000000000000132
RIP=0000000000209114 RFL=00000202 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0
IF を set した状態で改めて USB 割り込みの動作確認をすると、ようやく MSI 経由で割り込みが起こって mouse, keyboard イベントが処理されることが確認できた。
長かった...

IA-32e mode での segmentation
Intel SDM Vol 3. 3.2.4 Segmentation in IA-32e Mode:
In 64-bit mode, segmentation is generally (but not completely) disabled, creating a flat 64-bit linear-address space. The processor treats the segment base of CS, DS, ES, SS as zero, creating a linear address that is equal to the effective address.
Note that the processor does not perform segment limit checks at runtime in 64-bit mode.
要するに、以下のような解釈という理解
- base は設定しようとしまいと 0 扱い (effective address (offset) == linear address)
- limit も実質的に意味無し (runtime check は無効)
offset, linear address, physical address については Fig. 3-1 がわかりやすい。
FS, GS は例外、明示的に設定すれば base address が有効
The FS and GS segments are exceptions. These segment registers (which hold the segment
base) can be used as additional base registers in linear address calculations.
Vol 3. 3.4.4 Segment Loading Instructions in IA-32e Mode によれば以下のような記述もある。
Because ES, DS, and SS segment registers are not used in 64-bit mode, their fields (base, limit, and attribute) in segment descriptor registers are ignored.
Address calculations that reference the ES, DS, or SS segments are treated as if the segment base is zero.
The processor performs linear-address pre-processing (Chapter 4) instead of performing limit checks.
SS は mikanos では一応設定しているように見えるけど、全く使われない?
DS も 0 (null descriptor) でも問題無いのかな?
Segmentation の limit check の代わりに linear address preprocessing というのが動くらしい
Vol 3. Chapter 4 LINEAR-ADDRESS PRE-PROCESSING 冒頭:
In IA-32e mode (if IA32_EFER.LMA = 1), linear addresses may undergo some pre-processing before being translated through paging.
以下の 3 つの check があるらしい
- Linear-address-space separation (LASS). This is a feature that, when enabled by software, may limit the linear addresses that are accessible by software, generating faults for accesses out of range.
- Linear-address masking (LAM). This is a feature that, when enabled by software, masks certain linear-address bits.
- Canonicality checking. As will be detailed in Chapter 5, paging does not translate all 64 bits of a linear address. Each linear address must be canonical, meaning that the untranslated bits have a fixed value. Memory accesses using a non-canonical address generate faults.
Canonicality checking 以外は software で有効化しない限りは動かないっぽい雰囲気

Segment register の設定
lgdt
で GDT をロードした後、 segment register を設定しないと sti
(set interrupt) の実行で落ちて再起動してしまうことがわかった。
この場合 double fault すら発生しない。
なんとなく、 priviledge level の確認あるいは EFLAGS 操作のためのチェックで CS (code segment register) を参照しようとして segment selector (GDT の offset) が不正であるために落ちるのではないかもしれない?
Vol 3. 3.4.3 Segment Registers に以下の記述があるが、 sti
については言及無し。
sti
で落ちるのは QEMU の独自仕様絡みである可能性もあり
Two kinds of load instructions are provided for loading the segment registers:
- Direct load instructions such as the MOV, POP, LDS, LES, LSS, LGS, and LFS instructions. These instruction explicitly reference the segment registers.
- Implied load instructions such as the far pointer versions of the CALL, JMP, and RET instructions, the SYSENTER and SYSEXIT instructions, and the IRET, INT n, INTO, INT3, and INT1 instructions. These instructions change the contents of the CS register (and sometimes other segment registers) as an incidental part of their operation.
各 segment register は明示的に読み書きできる visible part と、明示的に操作できない hidden part がある (同 3.4.3)。
Every segment register has a “visible” part and a “hidden” part. (The hidden part is sometimes referred to as a “descriptor cache” or a “shadow register.”)
Hidden part は base address, limit, access information の情報を含んでいる (Fig 3-7)。
Visible part をロードすると hidden part も descriptor を参照して一緒に書き換わるらしい。
When a segment selector is loaded into the visible part of a segment register, the processor also loads the hidden part of the segment register with the base address, segment limit, and access control information from the segment descriptor pointed to by the segment selector.
明示的に書き換えるまでは hidden part の cache が使われるので、一時的に CS が無効な segment selector を含んでいても実行を継続できる。
CS 以外の segment selector は mov
で書き換えられるが、 CS は mov
では設定できない。
Vol 2. "MOV—Move" からの引用:
The MOV instruction cannot be used to load the CS register. Attempting to do so results in an invalid opcode exception (#UD). To load the CS register, use the far JMP, CALL, or RET instruction.
CS を mov
で書き換えようとすると、 #UD が発生するはず... なのだが、 QEMU 環境で試してみたところ mov cs, ax
で CS を書き換えることができてしまった。
これは厳密には QEMU の実装が x86 CPU 仕様に従っていないのでは、という気がする。

Rust の inline assembly に対する assembler
LLVM の internal assembler (GAS?) が使われるらしい。
x86 では .intel_syntax noprefix
オプションが使われる。