この記事は「自作OS Advent Calendar 2024」の7日目の記事です(JSTとは言ってない)
自作OS Advent Calendar 2024 - Adventar
WasabiOS とは
私が日本国内で開発している、Rustで書かれたOSなので、Wa (和) + Sabi (Rust) = Wasabi という名前になっています。
ちなみに、この上で動くウェブブラウザを、いつも配信を一緒にやっている d0iasm さんが実装してくれて、 それについてはつい先日、書籍が出版されたので、興味のある方はそちらもぜひご一読ください!
作って学ぶ ブラウザのしくみ
— hikalium (@hikalium) 2024年11月10日
届いた!!!( @d0iasm さんの本です!!!)
ブラウザをRustでぜんぶ自作するぞ!!!という力強い内容となっているので、ブラウザのしくみに興味がある高レイヤの人々も、キラーアプリに困っている低レイヤの人々も、楽しめるはずなのでぜひご一読ください!#low_layer_girls pic.twitter.com/V9nbL6sjQA
そういうわけで、WasabiOSは、雑にWebブラウザが動く程度の機能があるOSだよ、というのが大雑把な説明なのですが、もう少し細かく、今年の開発の進捗を振り返りながらお話しましょう。
1月あたり
年初の書き初めです(これ、今年だったのか…去年くらいかと思ってました…)
会社で書き初めイベントがあったので書いた pic.twitter.com/ffUbrZlOsL
— hikalium (@hikalium) 2024年2月8日
PEバイナリと和解した
PEバイナリと和解した(これで自作OSがクラッシュしたらmake crashって叩くだけで例外吐いている箇所のdisasが見れるようになった) pic.twitter.com/6LsiCEEWNs
— hikalium (@hikalium) 2023年1月28日
もう少し解説すると、WasabiOSはUEFIのみに対応するOSなので、OS本体も含めすべてがひとつのPEバイナリとなっています。 そのため、QEMUで自作OSを実行してデバッグする際に一般的に用いられる、gdbからremote target としてQEMUをattachして、QEMUにELFファイルのありかも教えてあげることで、ディスアセンブルされたコードを読んだりする手法は、そのままではうまく動きません。 PEはWindowsワールドのPDBというデバッグ情報形式を使っているのに対して、ELFはLinuxワールドのDWARFというデバッグ情報形式を利用しているのが原因です。
とはいえ、自作OSで例外ハンドラが正しく動作しないようなタイプのバグをデバッグする際には、現在の命令ポインタが、プログラム中のどの機械語、もしくはソースコードの行に対応するのかをどうしても知りたくなります。 これを実現するには、UEFIがロードするPEファイルが任意のアドレスに配置されうるため、PEファイルにかかれているセグメントのアドレス範囲と、実際にロードされたプログラムのアドレス範囲のずれを特定する必要があります。 そのために、OSが実際にロードされたメモリ上の位置をなんとかして知る必要があるわけですが、これは実は簡単にできます。 結論から言えば、雑にどこかの関数のポインタのアドレスを表示してあげればよいのです。
/// This function prints the addr of itself for debug purpose. #[no_mangle] pub fn print_kernel_debug_metadata() { // Note: This log message is used by the e2etest and dbgutil // so please do not edit if you are unsure! info!( "DEBUG_METADATA: print_kernel_debug_metadata = {:#018p}", print_kernel_debug_metadata as *const () ); }
wasabi/os/src/debug.rs at 8e23542da41be26f37d52f2be1b728c06c53fffa · hikalium/wasabi · GitHub
この関数はOS起動時の比較的初期に呼ばれ、シリアルポートに関数の実際のアドレスが出力されます。
make run
を実行した際には、QEMUのシリアルポート出力をファイルに保存するよう、QEMUの引数で指定しているので、あとはそこからいい感じにこの数値を切り出してあげれば、
PEファイル中に書かれているアドレスと、実際のメモリ上のアドレスの差が判明します。
あとは、これとシンボルテーブルを突き合わせればいいのですが、gdbは標準でPEバイナリのデバッグ情報を解釈してくれません。 これの解決策は、随分前にretrageさんが書いてくれたブログがあるので、それを参考にするとよいです(ありがとうございます!)
で、この記事を読んでみると、retrageさん謹製のツールを使って、gdbに食べさせられる形式を生成するという手法になっています。
とはいえ、今回はせっかく全部Rustで書いているので、なんとかならないかなーと思って、結果自分でdbgutilというスクリプトを書きました。
PEをパースする部分はpdbという外部crateですが、結構便利なのでおすすめです。
これで、make symbols
ってやると、直近に実行したWasabiOSのバイナリに含まれるシンボルが出力されたり、
make crash
って実行すれば、QEMUがトリプルフォルトで落ちたログをパースして、そのアドレスに相当するシンボルの情報を出力してくれるようにしておきました。
とはいえ、ここらへんの機能は、私が必要なときに必要なものを実装して、テストは特に書いていなかったので、毎回動かなくなっていて手直ししています。今度はちゃんとテスト書こう…。
そこから8月くらいまで
さて、ここから8月くらいまでは、怒涛の実装とデバッグ期間になっていました。
主な実装内容はというと、
- アプリのロードと実行
- システムコールのサポート
- アプリとOSのコンテキストスイッチまわり
- ネットワークスタックの実装
といった感じでしょうか。
さらに今回は、d0iasmさんのブラウザという、そこそこ大きな、別の開発者による、独立したアプリが存在していたので、noliと命名したライブラリの整備や、描画・UI関連のAPIの実装などもありました。 自作OSで最初から独立した開発者のアプリが存在するケースは珍しいと思いますが、自分の変更によってアプリ開発者からバグ報告が飛んでくると、動作を破壊して申し訳ない気持ちと同時に、うおーOSみたいじゃん!(OSです)という気持ちになれて、非常に面白かったです。
ちなみにライブラリの名前noliは、Wasabiとかの「寿司」っぽいテイストつながりの「海苔」と、OSとアプリをつなぐ存在である、という、くっつけるほうの「のり」をかけ合わせたものになります。
さらにいうと、ブラウザの名前sabaは"SAmple Browser App"ですが、もちろんこれも魚のサバを意識しています。
システムコールについては、実装に時間がかかることが明らかだったので、最初はnoliのレイヤーにモックを仕込んでおいて、それを利用してブラウザ側を開発してもらうようにしていました。 たとえば、TCPのreadをすると、問答無用でexample.comのHTMLソースが返ってくるコードを最初は入れておき、あとでそれを実際の実装に置き換えるなどしていました。
DNSの問い合わせに関しても、NXDOMAINを返すドメイン名や、127.0.0.1を常に返すドメイン名などを独自に設定して、それに対応する処理を最初はnoliに、次はOS側に、というふうに、実装が進むにつれてモックを削除したり、OS側に移動させるなどして、アプリの開発のしやすさと、OSの実装の並行作業を実現しました。
ちなみに .invalidというTLDは、NXDOMAINを常に返すドメインとして扱いなさい、とRFC6761には書かれています。
The domain "invalid." and any names falling within ".invalid." are special in the ways listed below. Users MAY assume that queries for "invalid" names will always return NXDOMAIN responses. Name resolution APIs and libraries SHOULD recognize "invalid" names as special and SHOULD always return immediate negative responses.
本当は全部の.invalid.をハンドルするべき(SHOULD)ではありますが、いまの実装ではwasabitest.example.invalid
だけがsyscallの実装でNXDOMAINを即座に返すようになっています。
(というのも、リモートのDNSサーバーがNXDOMAINを返すようなケースもテストしたいので、すべてをハンドルしてしまうと外向けにNXDOMAINを返してくれることが保証されているクエリを投げられなくなってしまうからです(というのは今考えた言い訳で、まだテストは実装していません…あとでやります…))
あと、ブラウザアプリ実装時にローカルにWebサーバーを立てて実験したいときのために、QEMUのユーザーネットワークでホストマシンに相当する10.0.2.2を必ず返すhost.testというドメインもハードコードされていたりします。
もしかすると、世の中のハードコードされている各種ドメイン名を集めてみると、面白いかもしれませんね!
3時間のバトルの末、赤色のペイントソフト()ができた図です(マウスカーソルの重ね合わせ処理が実装された!)(久々の突発的配信を見に来てくれた皆様、ありがとうございました!)#low_layer_girls pic.twitter.com/mlH375m3Ov
— hikalium (@hikalium) 2024年8月3日
9月くらい
TCP/IPまわりの実装が動くようになり、d0iasmさんが実装したブラウザも動くようになりました!!!
ああああああ〜やっと自作OSからHTTPリクエストを送れるようになった…これで私もインターネットにいけるよ!!! pic.twitter.com/PWzz5umQbx
— hikalium (@hikalium) 2024年9月2日
うお〜!!!自作ブラウザが!動く!!!自作OSの上で!!!(やった〜!!!)(これは最高に嬉しい) https://t.co/uTHnsR3rYt
— hikalium (@hikalium) 2024年9月2日
TCP/IP, 実ははじめて実装したんですが、これ結構楽しいですね!パケットキャプチャとにらめっこしつつ、ちょっとずつ応答できるケースを増やしていくと、いつの間にか動く、というとても楽しい経験なので、みなさんもぜひやってみてください! というか、TCPの再送という仕組みがあるおかげで、うまくいっていないときには向こうからパケットがどんどん再送されてくるので、あー動いていないなあ、と気づけて、デバッグもやりやすかったです。
ちなみに、私はチェックサムの計算で無限回詰まりました。
ここでデバッグに大いに役に立ったのが、tsharkちゃんです。
tshark -V -o ip.check_checksum:TRUE -o tcp.check_checksum:TRUE -r log/dump_net1.pcap
みたいな感じにしてあげると、チェックサムの検証結果をTCPのパケットごとに出してくれるので、QEMUのパケットダンプオプションと併用して、ぜひデバッグにお役立てください。
ちなみに私はそういったデバッグに役立つコマンド片をMakefileにしれっと紛れ込ませていたりするので、ぜひそちらも参考にしてみるとよいです。 tsharkのJSON形式出力とかは、かなり便利だったので、ぜひ使うと良いと思います。
.PHONY : tshark tshark: @tshark -V -o ip.check_checksum:TRUE -o tcp.check_checksum:TRUE -r log/dump_net1.pcap .PHONY : tshark_json tshark_json: @tshark -V -o ip.check_checksum:TRUE -x -T json -r log/dump_net1.pcap | jq . .PHONY : tshark_tcp tshark_tcp: @tshark -V -o ip.check_checksum:TRUE -T json -Y tcp -r log/dump_net1.pcap | jq -c '.[]._source.layers | {src_ip: .ip."ip.src", dst_ip: .ip."ip.dst", src: .tcp."tcp.srcport", dst: .tcp."tcp.dstport", flags: .tcp."tcp.flags_tree"."tcp.flags.str", seq: .tcp."tcp.seq_raw", ack_raw: .tcp."tcp.ack_raw"}' .PHONY : tcp_hello tcp_hello: echo "hello" | nc localhost ${TCP_FORWARD_PORT} .PHONY : tcp_echo_server tcp_echo_server: socat -v tcp-l:15000,reuseaddr,fork exec:'/bin/cat'
https://github.com/hikalium/wasabi/blob/8e23542da41be26f37d52f2be1b728c06c53fffa/Makefile#L284-L306
End-to-end test
WasabiではEnd-to-end test (e2etest) にも力を入れていて、e2etestディレクトリ配下にコードが格納されています。
make run_e2e_test
と実行すれば、QEMUを画面無しで起動して、ネットワーク周りの機能やキー入力、アプリのロードと実行が正しく動作しているかを、ユーザーと同様の外部入出力レベルでテストすることができます。実装コストや実行時間はかかりますが、これが通れば一通りの機能は動く、という安心感をもって作業を進められるのは、とても心強かったです!
また、async/awaitを使えるRustの強みが、ここでも生かされているなあ、と個人的には感じました。もし興味があれば、e2etest/src/main.rsにテストの実装が、同じディレクトリにQEMUの起動などをラップするコードが配置されていますので、一度読んでみてください。
で、今はなにやってるの?
休養しつつ、日本語を書く締め切りに追われてます。お楽しみに!!!(無理せず頑張ります!)