Bareflankを使ってみる

前から気になっていたBareflankを少し触って見ました.

あまりドキュメントがないようなので半分メモがてらBareflankについて書いてみます.


BareflankはいわゆるThin-hypervisorの一種です. 複数VMの動作を目的とする通常のハイパーバイザとは異なり,そのようなハイパーバイザは基本的に一つのゲストOSを実行し,ゲストOSのフックや解析をおこないます. セキュリティや研究目的で用いられることが多いです. そのようなハイパーバイザは(特に最近)結構あって,

などがあります.これらと比較してBareflankは"hypervisor Software Development Toolkit"を謳っており,特徴として

  • Modern C++ (C++17)で実装 (C++ STLのサポート)
  • モジュール構成による高い拡張性
  • Windows, Linuxのサポート.さらにUEFIブート可能

が挙げられます.

とりあえず動かす

公式のCompilation Instructionsを参考にすれば動くと思います. ただ最近も良く開発されており,masterブランチはunstableだったりすることがあるので場合によっては適当なタグをチェックアウトした方がいいかもしれません.

以下,Linuxで動かしたときの実行結果です.

% git clone https://github.com/bareflank/hypervisor.git
% cd hypervisor
% mkdir build; cd build
% cmake ..
% make -j8
% make driver_quick
% make quick
% make status
vmm running
Built target status
% make dump
Scanning dependencies of target dump
[0] DEBUG: host os is now in a vm
[1] DEBUG: host os is now in a vm
[2] DEBUG: host os is now in a vm
[3] DEBUG: host os is now in a vm

Built target dump
% dmesg
...
[345609.788134] [BAREFLANK DEBUG]: dev_init succeeded
[345613.817106] [BAREFLANK DEBUG]: dev_open succeeded
[345613.822454] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822471] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.822478] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822480] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.822491] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822495] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.822579] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822612] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.822632] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822637] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823104] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823214] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823324] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823359] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823526] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823573] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823583] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823590] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823598] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823600] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823669] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823713] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823731] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823745] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823755] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823762] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.872731] [BAREFLANK DEBUG]: IOCTL_LOAD_VMM: succeeded
[345613.872741] [BAREFLANK DEBUG]: dev_release succeeded
[345613.876854] [BAREFLANK DEBUG]: dev_open succeeded
[345613.911072] [BAREFLANK DEBUG]: IOCTL_START_VMM: succeeded
[345613.911077] [BAREFLANK DEBUG]: dev_release succeeded
[345616.927978] [BAREFLANK DEBUG]: dev_open succeeded
[345616.932016] [BAREFLANK DEBUG]: dev_release succeeded

構成

Linuxで使用する場合について簡単に見てみます.

前述の通りBareflankはC++で開発されています. BareflankはOSの機能が使えないベアメタル上で動作するため,そのためのC++ランタイムが実装されています.

Bareflankのロード

Bareflankはカーネルモジュールをロードすることで起動します(カーネルモジュール自体は当然ながらCで書かれています). このときBareflank本体はelfバイナリとしてコンパイルされており,カーネルモジュールはそのelfバイナリを動的にロードして呼び出すということをおこなっています. このときの_start_func()(Bareflankのエントリポイント)はbfvmm/src/entry/entry.cpp:bfmain()になります.

仮想化の流れ

カーネルモジュールからBF_REQUEST_VMM_INITを受け取ったBareflankは自身を仮想化します.

処理の流れは高度に抽象化されていて分かりにくいですが,private_init_vmm()g_vcm->create()によってbfvmm:vcpuインスタンスが作成され,g_vcm->run()によりintel_x64::vcpu::run_delegate::run_delegate()が実行されます. ここで::intel_x64::vmcs::launch() => ::intel_x64::vm::launch_demote() => _vmlaunch_demote()VMLAUNCHが以下のように実行されます.

global _vmlaunch_demote
_vmlaunch_demote:
    call _vmlaunch_trampoline
    ret

_vmlaunch_trampoline:

    pop rsi

    mov rdi, 0x0000681E     ; VMCS_GUEST_RIP
    vmwrite rdi, rsi

    mov rdi, 0x0000681C     ; VMCS_GUEST_RSP
    vmwrite rdi, rsp

    mov rax, 0x1
    vmlaunch
    mov rax, 0x0

    jmp rsi

_vmlaunch_trampolineではスタック上の関数の戻り値をVMCS_GUEST_RIPに設定したのちVMLAUNCHします. これによりVMLAUNCHが成功した場合はvmlaunch_demoteの呼び出し元から仮想化した状態で処理が再開されます.

なおVMCSの初期化はvcpuコンストラクタ内から呼ばれるintel_x64::vmxのコンスタラクタでおこなっています.

Exit handler

VMCSのhost ripにはexit_handler_entry設定されていますexit_handler_entryは第一引数(rdi)に[gs:0x00A0]を設定してbfvmm::intel_x64::exit_handler::handle(bfvmm::intel_x64::exit_handler*)を呼び出しますが,この[gs:0x00A0]の値はexit_handler_ptr経由で設定されています.

exit handlerはexit reason毎にリストで複数持てるようになっており,add_handler()を利用してハンドラを登録します.

拡張

Bareflank/hypervisorだけでは仮想化するだけでほとんど何もしません. EPTも使っていません. 何か具体的なことをするにはハイパーバイザを拡張する必要があります.

普通であればここでハイパーバイザのコードを直接修正するわけですが,ここからが"Extensible hypervisor"の本領発揮といったところで,CMakeを駆使したビルドスクリプトによりBareflankは主要な関数(クラス)を別ファイルに記述してオーバライドできるようになっています.

拡張方法の概要はextension_instructions.mdに書いてあります. Bareflank/hypervisor_example_cpuidcountにexit handlerを追加する例があるので,とりあえずこれを参考にするのが良いと思います.

hypervisor_example_cpuidcountではvcpu_factory::make()をオーバライドしbfvmm::intel_x64::vcpuを継承して作成したvcpuを返すようにしています. vcpuのコンストラクタの中でexit_handler()->add_handlerを呼び出すことでexit handlerを追加しています.

この例ではcpuid命令によりvmexitが発生する度にm_countをインクリメントし,bareflankが終了時にm_countの値を出力しています. ちなみにデフォルトではシルアルポートのCOM0に出力されます.

EPT Hook

Bareflank/extended_apisにEPTハンドラ等のベースがあります. EPTを利用したい場合はこれを拡張するのが簡単だと思います.

Bareflank/extended_apis_example_hookにextended_apisを利用した拡張例があります.

所感

BareflankはModern C++で書かれたthin hypervisorとして(まだまだ活発に開発中ですが)一番の完成度ではないでしょうか. C++のランタイムがきちんと移植されていること,そしてあの(正直どうなってるか分かってない)ビルドシステムがすごいです. UEFIからType1 hypervisorとしても起動できるようなので後で試してみようと思います. まだ応用例は多くはないようですが,これからに期待です.

最後に,何かあればgitterで質問すれば多分答えてもらえると思います.