この記事はアドベントカレンダー2024の記事です。
はじめに
こんばんは、ほたるいかといいます。
x86のハイパーバイザの仕組みに興味を持ってBitVisorと呼ばれるハイパーバイザを調べていたついでに第一段階のブートローダをRustで作った(レポジトリ)のでその仕組みや詰まったところ等を記事にしようと思います。時間が足りなくて全部は作れなかった
BitVisorについてここ数ヶ月調べた程度であり、間違っているところ等ありましたら教えていただけると助かります。
BitVisorについて
BitVisorとは、thin hypervisor と呼ばれるハイパーバイザの一種でありOSを一台しか動かせない変わりに軽い動作を実現したハイパーバイザです。
BitVisorはOSの更に上位でファイルの暗号化やデバイスの隠蔽をすることでセキュリティの向上を主目的としているようです。
第一段階ブートローダについて
今回はx86のuefi環境の話をします。他にもBIOS環境やaarch64アーキテクチャ等にも対応しているようです。
BitVisorは2段階ブートローダを採用しており、1段階目のブートローダでbitvisor.elfの先頭から0x10000までの領域にある2段階目のブートローダをメモリ上に読み出し、2段階目のブートローダでBitVisor全体を読み出す形式になっています。BIOS環境とコードを共通化するためにこのような仕様になっていそうです。
1段階目のブートローダは/boot/uefi-loader/にあります。
以下では、実装しているときに知った面白い仕様について説明していきます。
ファームウェアドライバ切断
BitVisorはuefiから呼び出されるとcpuの仮想化を行ってuefiに制御を返し、その後にuefiからOSを呼び出すという挙動をするため、完全な仮想化が行われているハイパーバイザと比較してOSが実デバイスを直接制御する仕組みとなっています。BitVisorがネットワークのログを取る等でデバイスを操作することが必要になった場合、para passthrough driverと呼ばれるデータの監視や変換を行うドライバを利用するわけですが、このときにuefiが提供しているドライバと競合する恐れがあります。そのため、BitVisor起動時にuefiのUninstallMultipleProtocolInterfacesと呼ばれる関数により、一旦すべてのデバイスドライバを切断しています。
Apple製品のバグへの対応
ファームウェアドライバを切断するときに利用するUninstallMultipleProtocolInterfaces関数ですが、Apple Mac mini 2018では実装が間違っているせいで切断できなくなっています。そのため、BlockIoCryptoProtocolというプロトコルがあるかどうかでこの製品かどうかを判別して、Mac mini 2018モデルであればこのバグを回避した関数を代わりに利用しています。
ファームウェアドライバインストール時の対応
これはどこにも詳しい記述がなかったので、もしかしたら間違っているかもしれません。(参考)
BitVisorはPCのディープスリープ(S2以上)からの復帰時にCPUのコンテクストを復元するためスリープ検出のフックを作成したりするためにACPIを改変しています。この修正がファームウェアドライバ(EFI_ACPI_TABLE_PROTOCOL)の再インストールにより失われないように、uefiのInstallConfigurationTable関数が呼び出されたときにフックされるbsdriverというドライバを作成し、ACPIに関連するファームウェアのインストールが行われたときに改変しています。
実際の実装で詰まったところ
実際にRustで1段階目のブートローダの実装をしてみました。基本的にMilvusVisorという、BitVisorのaarch64アーキテクチャバージョンであるハイパーバイザがRustで書かれているため、そのコードを改変する形で作成しました。
BitVisorのelfが32bit
bitvisor.elfを読み出すときにエラーが発生していたのですが、これはelfが32bitであったからでした。これは、BIOSの32bitと互換性をもたせるためなのでしょうか?
EFI_FILE_PROTOCOL
bitvisor.elfを起動するには、bitvisor.elfに対応するハンドルをOpenして、メモリ上に確保した領域にそれをRead関数で展開すればよい。EFI_FILE_PROTOCOL.Read()は読んだ分それに対応するプロトコルのハンドルのポインタが増加してしまうため、elfファイルを読むなどしたあとはEFI_FILE_PROTOCOL.seek()で戻る必要がある。また、EFI_FILE_PROTOCOL.seek()はプロトコルの初期位置からの絶対アドレスで示される点に注意です。(1敗)
終わりに
今回はBitVisorのブートローダをRustで書き直すという誰得なことをしてみたわけですが、かけられる時間があまりなかったためbsdriverは既存のコードをそのまま利用していました。Rustでuefi driverを書いている先例も見つけたので、暇なときに書いてみたいです。
明日は、zoi_dayoさん 、 nzt3さん、 cp20さんの3名です! お楽しみに!!!
参考情報
BitVisorのブートシーケンス解説 (UEFI編)
BitVisorのUEFI対応
BitVisor 2017年の主な変更点
BitVisor 2019年の主な変更点