借り初めのひみつきち

仮ブログです。

今週の MYOS 🎉

今週、というには間が空いてしまった最近の MYOS/TOE 界隈のトピックです。

MAYSTORM

直訳すると5月の風になります。

MYOS が最初のコミットから1年経ったので、1周年を記念して新しい名前を付けて仮プロジェクトから正式なプロジェクトに昇格しました🎉

github.com

ページング

TOEMMU のない CPU で動作するのが目標なのでページングを現状利用していないし今後も利用予定はありません。

一方 MYOS は x64 CPU で動作するのでページングを完全に OFF にすることができず、ブートローダーが設定した Identity Mapping (物理アドレスと仮想アドレスが一致する割り当て方法) 領域をそのまま使って、カーネルとしてはページングを見て見ぬ振りしていました。

今まではこのやり方でも特に大きな問題は起きていませんでしたが、 MMIOバイスの扱いで問題が発生しました。 ブートローダーは MMIOバイスの存在を知らないので、あらかじめページを割り当てておくことができません。 というわけで、カーネル起動後にデバイスドライバの指示でページ割り当てる機能を実装しました。

そもそも、実は APIC や HPET も MMIOバイスなのでページ割り当てが必要でしたが、これまでは本来カーネルが設定するべきページ設定をブートローダーの中で固定アドレスで勝手に設定していました。 今回の変更でブートローダーの固定配置を削除してカーネル側で ACPI の情報に基づいて動的に設定するように修正しました。

また、今まではカーネル以外は Identity Mapping 前提で物理メモリに配置して動いていましたが、メモリマネージャにページマネージャが仲介して仮想アドレスを返すようにしたので物理アドレスを直接扱わないようになりました。 カーネルの配置はブートローダーがカーネル専用の領域をマッピングしていたので今まで通りです。

一連の変更によって SMP 初期化以降のカーネルは Identity Mapping 領域にアクセスせずにページマネージャが割り当てた新しいページテーブルで動くようになりました。 ただし、残っている Identity Mapping 領域をそのまま切り離してしまうと haribote-os アプリが 32bit のために 4GB 以下のアドレスにしか配置できず、メモリアクセスできなくて動作しなくなってしまいます。この部分はまだ調整中です。

SMP のページング

SMP 起動後のページングはページテーブル操作中に他のプロセッサがページテーブルの書き換えを行うと競合して予期せぬ動作をする可能性があるため排他制御が必要になります。 また、ページテーブル変更があったことを他のプロセッサは知らずにそのまま実行を続けてしまうので、ページテーブル操作が終わったら TLB フラッシュを全てのプロセッサに通知するためにプロセッサ間通信する必要があります。

もしも単純にそのままページテーブルをロックしてしまうと、あるプロセッサでページテーブル変更後の通知の応答を待ってる間に他のプロセッサでページテーブル操作のロック待ちが発生した場合、お互いを待ち続けてデッドロックする可能性があります。

これに対する解決策として、ページ操作専用のスレッドを作って全てのページ操作を非同期にする改修に着手しています。 なお、現在は SMP 起動後にページング操作をするシチュエーションが存在しないのでこの機能は未完成です。

カーネルの ELF 対応

PC でよく使われるメジャーな実行ファイル形式は大きく分けると PE と ELF の2種類存在します。 UEFI は PE を採用していますが、現代的なほとんどの OS は ELF を採用しています。

MYOS は当初 UEFI アプリとして開発を始めたため PE を採用し、ブートローダーとカーネルの分離後もそのまま PE を使っていました。 しかし、 PE だと制限も多く扱いづらくなってきたので ELF への移行を検討していました。

まずは試しに単純に ELF をサポートしているターゲットに変えてビルドしなおしたところ、今まで見たことないエラーが出てきました。

その中のひとつに SSE が無効化されていてビルトインライブラリがビルドできないというエラーがありました。

現在 MYOS では SSE を無効にしています。 SSE は汎用レジスタとは別のレジスタセットを使用するので割り込みやコンテキストスイッチのコストが上昇しますが、 SSE は x86 アーキテクチャに後から追加された機能なので使わなくても大半のプログラムは記述可能です。

アプリケーションレベルでは SSE の機能を利用できた方が便利なこともあると思うので、将来アプリケーションで利用することがあれば SSE に対応する予定があります。 一方、カーネルでは利用するメリットよりも全てのスレッドがコンテキストスイッチのたびに毎回 SSE レジスタの退避処理をするコスト上昇の方がデメリットだと考えています。 そのため、カーネル内で不用意に SSE にアクセスする命令を実行しないようにコンパイラオプションで SSE を無効にしています。

Rust では OS ごとにターゲットが存在していて、 PE 形式から ELF 形式に移行するということは今まで使っていた UEFI のターゲット (x86_64-unknown-uefi) をやめて ELF をサポートするターゲットに変更することになります。 しかし、 ELF に対応するために一番無難そうだった Linux ターゲット (x86_64-unknown-linux-gnu) のビルトインライブラリの中には先述のように SSE を利用するものが含まれていてこのままではビルドできませんでした。

そこで他に使えそうなターゲットを探していたところ、 x86_64-unknown-none というターゲット設定のテンプレートを見つけたので、これをカスタマイズして無事に ELF に移行することができました🎉

PE から ELF に移行すると ABI も変更されますが、あらかじめ ABI が違うことはわかっていて色々準備していたので大きな混乱もなく無事に移行できました。

ちなみに TOE は最初から linux ターゲットで ELF 形式を採用していましたが、ロードサイズ制限緩和のためにビルドスクリプトで独自形式に変換していました。 MYOS では TOE ほどメモリ要求が厳しくないのでこのまま ELF のまま利用する予定です。