BitVisor
BitVisorDay 15

BitVisor上でマイクロコードアップデート

More than 1 year has passed since last update.

BitVisor上でマイクロコードアップデートがどのように行われるかについて紹介します。

AMDプロセッサの場合

AMDプロセッサではPATCH_LOADER MSR(0xC0010020)への書き込みでマイクロコードアップデートが行われます。このMSRに書き込まれる値は仮想アドレスなので、単純にVMMで同じ値を書き込んでも正しく適用されません。

BitVisorではこの書き込みをパススルーに設定しているので、マイクロコードアップデートは仮想マシン上から直接適用されます。

Intelプロセッサの場合

IntelプロセッサではIA32_BIOS_UPDT_TRIG MSR(0x79)への書き込みでマイクロコードアップデートが行われます。このMSRに書き込まれる値は仮想アドレスなので、単純にVMMで同じ値を書き込んでも正しく適用されません。

Intelプロセッサの制約により、このMSRへの書き込みをパススルーにしても、マイクロコードアップデートは適用されず、スキップされます。そのため、以前のバージョンのBitVisorでは、パススルーにせず、代わりに以下のようなメッセージを出して、マイクロコードアップデートをスキップしていました:

msr_pass: microcode updates cannot be loaded.

しかし、最近のLinuxは、マイクロコードアップデートが適用されないと永遠に再試行するようになってしまったので、このような実装では起動しなくなりました。本当は、マイクロコードアップデートなど必要ないと、ゲストオペレーティングシステム(OS)に伝えることができれば良いのですが、CPUIDなどもほぼパススルーとしているBitVisorではそれもなかなか難しいようですので、現在は、ゲストOSが行うマイクロコードアップデートを、VMMが代わりに実行する処理が実装されています。

代わりに実行するというのは、つまり、ゲストOSの仮想アドレス空間をVMM側にマップして、そのVMM側の仮想アドレスをIA32_BIOS_UPDT_TRIG MSRに書き込んでやるということです。ここで問題なのは、マイクロコードアップデートのデータサイズがわからないことです。マニュアルを見ると、マイクロコードアップデートのヘッダーなどの説明があり、一見するとデータサイズもわかりそうに見えます。ところが、これはIntelから配布されるアップデートファイルに含まれるヘッダーの話で、OSがこのヘッダーを見て、アップデートデータのアドレスをMSRに書き込むということになっています。アップデートデータの構造はマニュアルに載っていませんので、サイズがわかりません。

そこで、どうやるかというと、ページフォールトを使います。マニュアルを見ても特に書かれていませんが、どうも試したところでは、このMSRに書き込んだ仮想アドレスがマップされていないなどの場合は、普通にWRMSR命令でページフォールトが発生するようです。よって、不在ページのアドレスをMSRに書き込み、ページフォールトが発生するたびに、そのページをマップしてやり直していけば、プロセッサが本当に必要としているページだけをマップして、マイクロコードアップデートを実行できます。

ソースコードはこのあたりにあります:
https://bitbucket.org/bitvisor/bitvisor/src/34fa14de2421cd3564323eec998f8e0c465e99a0/core/msr_pass.c?at=default&fileviewer=file-view-default#msr_pass.c-126

mm_process_alloc()関数とmm_process_switch()関数を使って、プロセス機能が用いる仮想アドレス0-0x3FFFFFFFをあけます。その上で、アドレス0を使うのはさすがにまずいかなということで、アドレス0x1000に、ゲストOSが指定するアドレスの下位12ビットを加えた値をMSRに書き込みます。この時、callfunc_and_getint()関数を用いて書き込みを行うことで、その際に例外が発生してもクラッシュせず、例外の割り込み番号を取得することができます。

例外が発生しなければ成功ですが、最初は例外が発生するはずです。一般保護例外に関してはそのままゲストOSに対して同じ例外を生成します。ページフォールトの場合はそのアドレスをCR2から取り出し、そこからゲストOS側の仮想アドレスguest_addrを計算して、cpu_mmu_get_pte()関数でゲストOSのページテーブルを読み出します。この関数は、large page/huge pageであっても、ページテーブルエントリーがあるならこうなっているだろう、という値を生成するので、特に気にせずページテーブルエントリーを読み出したものとして扱うことができます。もしページがなければゲストOSに対してページフォールトを生成します。そうして得られた物理アドレスを、念の為MMIOフックがないかチェックしたうえで、mm_process_map_shared_physpage()関数を用いてマップします。うまくいったら、MSRの書き込みをやり直します。

今のところ、ページテーブルエントリーにアクセスビットをセットする処理や、ページテーブルエントリー内のキャッシュに関わるビットの処理が欠けていますが💧、実用上は問題ないでしょう。