おしぼりの日常

低レイヤが好きです.

VMMを自作してみた

はじめに

どうも、@oshibori  です。2020年に結構力を入れてVMMを自作したので、その振り返りを書いてみたいと思います。いきなりですが、僕が作ったのはkvmmというVMMです。他の人がcloneしてすぐに動作確認できるほどリポジトリを整備していないのですが、コード量は少ないので読んでちょっといじると意外にすぐ動かせると思います。今回はkvmmの紹介と開発の苦労話や今後自作VMをするなら的なことを書いていきたいと思います。

なぜ自作VMM?

いきなりちょっと話が逸れるのですが、そもそもなぜVMMを自作したのかって話をしようと思います。すごいちゃんとした理由があるわけではないのですが、強いていうなら「他の人がやったことなさそうだったから」です。自作OS、自作コンパイラ、自作エミュレータなどはよく聞きますが、自作VMM(ハイパーバイザ)についてはかなり挑戦者は少ないと思います。(国内だと、@garasubo さんなんかが有名だと思います)自作VMMはサイボウズ・ラボユースの活動として行うため、ほとんどの人がやった事がないのならOSSとして公開するモチベーションも湧きます。(また、体感ではありますが自作界隈でVMMの話題がちょっとずつ増えていた気もします)

 

kvmmについて

先にkvmmについて話してしまいます。kvmmはユーザースペース(ring3)で動くVMMです。(kvmmという名前には 「KVM APIを利用したVMMなのでkvmm」という非常に安直な由来があります)kvmmはVMM上でxv6を動かすことを目標としていて、その目標は8割くらい達成済みです。対応している機能を列挙してみると、

  • VMの作成/停止/再開
  • ゲストOSの命令実行
  • LAPIC/IOAPICのエミュレーション
  • 割り込みエミュレーション
  • ディスクエミュレーション
  • UARTエミュレーション
  • ブレークポイントレジスタ表示などのデバッグ機能

という感じです。複数VMの起動やマルチプロセッサ対応などはまだ着手できていませんが、これくらい実装すると普通にxv6が動いてlsやechoなどコマンドの実行も可能になります。上記の項目の中で、上の2つに関してはAPI経由でKVMが大部分の処理を代わりにやってくれます。そのため、僕が実装したのはAPIを利用したVMの状態管理とエミュレーション関連の処理になります。比率としては2:8くらいでしょうか。(なので半分くらいエミュレータを実装している気持ちになりますし、実際エミュレータの実装はかなり参考にしています)

 

苦労したこと

一番苦労したのは、KVM APIに関する情報がとても少なかったことです。公式のドキュメントがあるのですが、これがまあまあ不親切です。ざっくりとした使い方などは書いてありますが、APIユースケースについてはそこそこ書いてあるものの、パラメータの意味やKVM側での動作についての記述が乏しく、初見ではそのAPIを利用してどうVMを動かしていくのかのイメージが全然湧きませんでした。また、拡張性のためだけに用意されていて使用されないflagsやpaddingなどの変数もあり、いちいちそれをチェックする作業も大変でした。また、そもそもKVM APIを利用して実装されたVMMがかなり少ないです。有名なところだとQEMUとfirecrackerがありますが、QEMUはコード量が膨大 + firecrackerはRust製ということで僕はどちらの実装もあまり参考にしていません。(機会と体力があれば読みたいとは思っていますが・・・)

 

逆に、デバイスエミュレーションなどの処理は意外とスムーズに進みました。多くの人が自作エミュを実装していたのでそれらを参考にすることもできましたし、デバイスのデータシートがあったので仕様についてもそれほど頭を悩ますことはなかったです。目標はxv6が動作することだったので、デバイスの動作を全て仕様書通りにエミュレートする必要もなかったのも大きいと思います。

 

これから自作VMMをするなら

自作VMM(ハイパーバイザ)を作ろうとしたら、まずホスト型かベアメタル型かという選択を迫られることになります。BitVisorやgarasuboさんのハイパーバイザなんかはベアメタル型でkvmmやQEMUなどはホスト型になります。ホスト型は狭義のハイパーバイザには含まれませんが、VMMには含まれるような立ち位置になります。

当然ですが、ベアメタル型の方が実装は大変です。そもそもOSがないので、まともに動かすまでにOS同等の機能を全て一から実装する必要があります。

ベアメタル型の場合は

  • UEFIブートローダ
  • セグメントやページングなどのOSの基本実装
  • 画面出力処理の実装や自作mallocの実装などなど

などなどUEFIで起動する64bitOS自作した事がある人なら全員が通ってきた道を再び通る必要があります。その上で、Intel VT-xを利用してVMMの土台を実装していくような流れになります。ベアメタル型の場合はホスト型に比べてIntel VT-x関連の実装が1番の鬼門になると思います。Intel SDMにちゃんと仕様は書いてありますが、かなり特殊な罠が多いらしいです(仕様書に書いてある前提条件を全部クリアしてるのになぜかVTX命令が失敗する、VTX命令実行後の状態が仕様書と一致しない 、など)。

もちろんこれは実装側の問題なので、正しく実装すればちゃんと動きますが、僕の体感では自作OSよりもデバッグ・修正の難易度が高いバグに見舞われる機会が多かった気がします。

 

また、QEMUのVTX関連の実装はかなりザルなので、ほとんど実機で試すしかないです。 訂正2021/02/14:これは言い過ぎでした。VirtualBoxHyper-VのNested Virtualizationを使用するなどの選択肢もあります。

 

加えて言えば、これは自作OSでも同じですがデバッグが地味にきついです。僕から言えるのは「シリアル通信を早めに実装してください」ということだけです。最終的に僕はなぜか仕様書通りに実装してもVM Runが失敗するというバグに遭遇して撤退を余儀なくされました。(誰かに僕の仇をとって欲しいです)

 

ホスト型の場合は

  • 命令のエミュレートなど含めて全てソフトウェアで実装(QEMU without KVM
  • VMKVM内で動かしてVM管理とデバイスエミュレーションだけ実装(QEMU with KVM, kvmm)
  • コンテナっぽく実装(詳しいやり方はちょっと分からないですが)

の3つがメジャーな選択肢になると思います。

どれを選択してもIntel VT-xを意識することはあまりないはずなので、そもそもIntelの仮想化支援機構についてもっと知りたいという人は、ベアメタル型じゃないとミスマッチかなと思います。

 

1番目がダントツで大変だと思いますが、いわゆる自作エミュにVM管理機構を付け加えるようなイメージなので実現自体は普通に可能だと思います。命令エミュレーションを自前でやるかどうかが2番目との違いなので、そこを自分で頑張りたい人はこれが一番あっていると思います。

 

2番目は上述の通りKVM APIの仕様と内部実装を探索する根気があればいけると思います。どちらかと言うと、KVM自体 or デバイスエミュレーションの実装に興味がある人がやると良いかもしれないです。また、1番目に比べればはるかに簡単にVMを起動可能 + 命令の実行の正確性をKVMが保証してくれるため、ただゲストOSを動かすだけでなくVMM側の機能実装を頑張りたい人はこっちの方が合っていると思います。

 

3番目は正直どんな実装になるのか分かりませんが、コンテナランタイムを読めるようになりたい or コンテナ技術そのものに興味があるという人は合っているかもしれません。自作VMMというより自作コンテナランタイムみたいな感じになるので、1,2番目よりは楽しさや目的も結構違ってくるのかなと思います。(僕は普通にどっちも興味があるので、今後自作コンテナランタイムやってみたいと思っています)

 

最後に

色々と書きましたが、自作VMMはめちゃめちゃ楽しかったです。xv6のブートローダ内で最初のin命令が実行されてVM Exitした時はテンション上がりまくりでしたし、UARTをエミュレートして 「xv6...」の文字が出力された時は飛び上がって喜びました。lsコマンドが動いたときの感動も忘れられません。ぜひこれを読んだ誰かが自作VMMに挑戦してくれたら僕もとても嬉しいです。