BitVisorとACPI


BitVisorではACPIのデータを参照している部分があります。それについて簡単に紹介します。

何に使うのか

主に以下のような目的で使っています。

  • Suspend-to-RAM (S3, スリープ、スタンバイ、サスペンド) の検出と復帰への対応
  • Suspend-to-RAM隠蔽
  • SMI用I/Oポートの取得
  • システムリセット
  • PCI MMCONFIG
  • シリアルポート電源対策
  • Power Management Timer

Suspend-to-RAMの検出と復帰への対応

Suspend-to-RAMを行うとCPUの電源が切れます。そのため、CPUのwrite-backキャッシュに残っている未書き込みのデータは失われます。また、復帰する時にはファームウェアから実行が再開され、ファームウェアが特定のメモリーに書かれている番地にジャンプすることで、オペレーティングシステムの復帰処理に実行が移る、みたいな感じになります。

BitVisorは、Suspend-to-RAMの際に復帰に必要なデータを保存しキャッシュをフラッシュするのと、復帰時にBitVisorの復帰処理を最初に実行させるようにするため、Suspend-to-RAMの検出を行い、復帰時にBitVisorの復帰処理が実行されるように設定します。これらの処理はcore/acpi.cのacpi_io_monitor()関数やacpi_pm1_sleep()関数で行われます。

Suspend-to-RAM隠蔽

CONFIG_DISABLE_SLEEP=1とすることでSuspend-to-RAM隠蔽が行われます。core/acpi_dsdt.cのparser()関数で、_S3などの名前の部分を書き換えることによって、S2およびS3に未対応という風に見せることで実現されています。

SMI用I/Oポートの取得

ThinkPad X61のTCG BIOSや、ThinkPad T410iのdisk BIOSがハングアップするということで、原因を調べるとSMIでとまっていたというのがありました。それを回避するのに、どうやらあらかじめ下位1MiBの仮想アドレスをマップしておけばいいということを発見し、そのマップを行うために、SMI用I/Oポートの情報をACPIから取得しています。

SMIはSMMと呼ばれる裏モードみたいなものにCPUを切り替えて秘密のコードみたいなものを実行させるものです。これを、そのへんのBIOSはレジスターに何か情報を入れておいてSMIを発生させると裏モードで処理が行われて結果がレジスターに返ってくる、みたいな使い方をしているところがあります。なので、通常のI/OフックのようにBitVisorで代行してしまうとうまくいきません。そこで、メモリーマップをしたら一時的にI/Oフックを外して実行させ、またマップが変わったらI/Oフックを入れる、みたいなちょっとトリッキーなことをやっています。

処理はcore/acpi.cのacpi_smi_monitor()関数あたりで行われています。

システムリセット

PC/AT互換機のシステムリセットの方法はいろいろあって、伝統的には8088時代のようにFFFF:0000にジャンプする、とか、トリプルフォールトを起こしてCPUをシャットダウン状態と呼ばれる状態にする、とか、あるのですが、OSが動いていると何かそういうのでうまくシステムリセットできないケースが結構ありました。そこで、割と確実性の高いのがACPIにあるシステムリセット方法なのです。

何か、噂によればLinuxなどもシステムリセットには実は結構苦労しているみたいで、いろんなworkaroundの塊みたいな感じのようです。なので、BitVisorの現実装でもうまくいかない環境もあるかも知れません。

処理はcore/acpi.cのacpi_reset()関数で、これを呼び出しているのはcore/reboot.cのdo_reboot()関数です。なお、うまくいかなかった場合はシャットダウン状態にする (IDTRをリミット0にしてint3を実行する) か、FFFF:0000へのジャンプかを行っています。

PCI MMCONFIG

PCIのconfiguration spaceにアクセスする方法としてI/Oポートを使う方法の他にMMIOの方法があります。これがMMCONFIGというやつで、ACPIのMCFGというテーブルにアドレスなどの情報が書き込まれています。core/acpi.cのsave_mcfg()関数で起動時にMCFGテーブルの情報をコピーしておき、その後はacpi_read_mcfg()関数が必要な情報をdrivers/pci_init.c側に提供します。

シリアルポート電源対策

コンパイル時にCONFIG_TTY_SERIAL=1としていた場合、シリアルポートにBitVisorのログが出力されます。同時に、ゲストOSがシリアルポートを使おうとすると衝突してしまうため、ゲストOSがアクセスできないよう隠蔽しています。

ところが、ACPIの電源管理の関係らしきところでシリアルポートの電源を落としてしまうような感じの処理があるらしく、Windowsを立ち上げるとログが止まってしまうという問題が発生しました。その対策として、ACPIの電源管理の関係らしきところを何もしない命令で潰してしまうことで、問題が発生しないようにしています。

これは、DSDTおよびSSDTと呼ばれるテーブルに含まれるAMLという機械語みたいなやつの一部をnoopにするというような感じの処理で、core/acpi_dsdt.cにあります。find_pnp0501()関数が見つける処理で、break_method()関数がnoopにしてしまう処理です。

Power Management Timer

3.579545MHzくらいでカウントされる24bitのカウンターです。BitVisor起動時からの動作時間を読むのに使用しています。もともとは、CPUから読めるTSC (Time Stamp Counter) を使用していましたが、SpeedStepなどの動作周波数の動的変更によりズレが生じたため、確実に一定のスピードで変化するカウンターとして使用することになりました。ただし、I/Oが少し遅いので、Invariant TSCが報告されるCPUの場合はTSCを使用します。Invariant TSCが報告されるのになぜか電源管理の影響を受けてズレるPCがあった気がします。

24bitですので16777216回のカウントで1周します。16777216/3579545秒、つまり、およそ4.7秒で1周しますので、その時間以上#VMEXITが発生しなかった場合、BitVisor内部の動作時間のカウントはズレます。

DSDT/SSDT

上述のシリアルポート云々とSuspend-to-RAM関連はDSDT/SSDTというテーブルが重要になります。これがACPI Machine Language (AML) という機械語になっています。仕様はACPIのspecificationに載っています、が、事実上はIntelの実装が一般的には使われているらしく、実際には仕様に沿っていないものが散見されます。BitVisorではIntelの実装は大げさすぎるだろうという判断によりspecificationに沿ったことから、仕様に沿っていない環境で問題が発生し、いくつかの回避策が入っています。

解釈ルーチンはcore/acpi_dsdt.cに入っています。大量のcase文と謎のaddbuf()関数呼び出しは、基本的にはspecificationの定義に沿ったものになっています。ただし、大部分の内容には興味が無いことから、解釈に時間がかかるのを避けるため、一部単純化されています。

仕様に沿っていない環境には、以下のようなものがあります。

  • iMac (Retina 5K, 27-inch, Late 2015)
  • Intel NUC D34010WYKH
  • ONKYO TW2B-A31B7PH, HP ProBook 4320s

iMacはもっとも致命的なパターンで、PkgLengthに入っている長さが間違っているというものです。Intelの実装では解釈できるのかも知れませんが、BitVisorのルーチンでは長さを絶対的なものとして扱ってしまうため解釈することができず、エラーになります。どうせ使わない部分なので、回避策としてCONFIG_ACPI_IGNORE_ERRORが実装されています。

Intel NUCはDeviceのところのObjectListにZeroOpが入っていたというパターンでした。仕様の通りであればObjectListは0個以上のObjectで、ObjectはNameSpaceModifierObjまたはNamedObjですから、ZeroOpが入ることはありませんが、実際入っていたので、仕方なく入っていても良いことにしました。Intel純正の安心感!

ONKYO TW2B-A31B7PH, HP ProBook 4320sは、詳しくはBitVisor Summitの「準パススルー型VMM開発の難しいところ」というスライドにありますが、やはりDeviceのObjectListに、DefIfElseがあったというものです。これもどう見てもおかしいのですが、入っていても良いことにしました。

BitVisor Summit
http://bitvisor.org/summit/

実際のところ、ACPIのこの仕組みは大変高度で、BitVisorの現実装でもだいたいは動いているものの、現実装が対応できないような書き方をすることは可能だと思います。いつだったか_S3の定義そのものがIfで囲まれている環境を見た時にはあれっと思いました。(BitVisorはIfを無視して_S3の定義を抽出しており、正常に動作してはいましたが、例えばCで言うところのif(0)みたいなので何種類もダミーの情報が置かれれば、ifを無視するとまともに解釈はできないわけです。)