VMM呼び出しとは、ゲストオペレーティングシステム上から仮想マシンモニター (VMM) を呼び出すことを言います。BitVisorに実装されているVMM呼び出しAPIについて紹介します。
VMM呼び出し命令
Intel VT-xおよびAMD SVMには、VMM呼び出しのための専用の命令があります。Intel VT-xではVMCALL命令、AMD SVMではVMMCALL命令です。
Intel CPUでVMMCALL命令、あるいは、AMD CPU/APUでVMCALL命令を実行したり、VMMが動作していないのにこれらの命令を実行したりすると、無効命令などの例外が発生します。
BitVisorのVMM呼び出し仕様
低レベルAPI
VMM呼び出し命令にはオペランドがなく、単にVMMに制御を移す、以上の意味はありません。どのようにしてVMMに情報を渡すか、あるいは、どのようにしてVMMから情報を得るかは、VMMによって異なります。
BitVisorの場合、機能ごとにファンクション番号を割り当てて、その番号を%eax (64ビットでは%raxですが、以下%eaxと表記します) レジスターに入れた状態でVMM呼び出し命令を実行することで、VMMの機能を呼び出す仕組みになっています。ファンクション番号は、0だけ予め決められた機能があり、それ以外の番号はBitVisorの初期化時に自動的に割り振られることになっています。
ファンクション番号0は、機能名からファンクション番号を取得する機能です。
- 入力:
- %eax=ファンクション番号 (0)
- %ebx=機能名を入れた仮想アドレス (char *型のポインター、US-ASCII, '\0'で終端)
- 出力:
- %eax=機能名に対応するファンクション番号、見つからなかった場合や、仮想アドレスの参照に失敗した場合は0
ファンクション番号1以降については、割り当てられる機能によってレジスターの使われ方などが異なります。ファンクション番号が不正な場合は%eaxに0が返されることになっています。
高レベルAPI (VMM側)
機能を登録する関数があります。
void vmmcall_register (char *name, vmmcall_func_t func);
機能名と関数を結びつけるだけです。高レベルとは言いがたいものです。
高レベルAPI (ゲストオペレーティングシステム側)
dbgshなどのツール類は、GNU/LinuxだけでなくWindowsなどのオペレーティングシステムでも同じように呼び出しができるよう、共通化されたインターフェイスを使用しています。この共通APIは、BitVisorのtools/commonディレクトリーにあります。
CALL_VMM_GET_FUNCTIONマクロ
CALL_VMM_GET_FUNCTION ("機能名", call_vmm_function_t *);
これは、機能名を渡すことで呼び出しの準備をします。VMCALL命令とVMMCALL命令のどちらが使えるかを判定しつつ、機能名からファンクション番号を取得し、構造体call_vmm_function_tにそれらの情報を格納します。
これはマクロです。インラインアセンブリにより展開されるため、"機能名" のところはchar[]型の変数ではなく、必ず文字列でなければなりません。
call_vmm_function_callable関数
int call_vmm_function_callable (call_vmm_function_t *f);
CALL_VMM_GET_FUNCTIONで得た情報を見て、VMM呼び出しが使用可能かどうかを判定する関数です。VMM呼び出し命令が使えない場合、または、機能名が正しくない場合に0を返します。使える場合は1を返します。他のVMMが動いていた場合の結果はVMMの仕様によって異なります。
call_vmm_function_no_vmm関数
int call_vmm_function_no_vmm (call_vmm_function_t *f);
CALL_VMM_GET_FUNCTIONで得た情報の、ファンクション番号を返す関数です。
call_vmm_call_function関数
void call_vmm_call_function (call_vmm_function_t *function,
call_vmm_arg_t *arg, call_vmm_ret_t *ret);
VMM呼び出しを行います。
call_vmm_arg_tおよびcall_vmm_ret_t構造体には、レジスターの値を渡します。rbx, rcx, rdx, rsi, rdiに加え、call_vmm_ret_tのみraxもあります。intptr_t型を使用しており、32ビットコンパイル時は32ビット幅、64ビットコンパイル時は64ビット幅になります。
実装
低レベルAPI実装
低レベルAPIは、VMM呼び出し命令による#VMEXIT (IntelではVM exitと表記されます) があったら、%eaxレジスターを見て処理を分岐するだけです。あまり見どころはありません。
#VMEXITの処理は、core/vt_main.cのdo_vmcall()関数、および、core/svm_main.cのdo_vmmcall()関数にあります。
%eaxレジスターを見て処理を分岐する処理は、core/vmmcall.cのvmmcall()関数にあります。
高レベルAPI実装
高レベルAPIは、GNU/LinuxだけでなくWindowsなどのゲストオペレーティングシステムでも同じように呼び出しができるよう、注意深く実装されています。
ファンクション番号取得で困るのは、機能名を入れた仮想アドレスが実際にはマップされていない可能性があるところです。確実にマップされた状態にするには、通常mlock()システムコールなどを使用しますが、権限がないと使えなかったとか、Windowsでは使えないとか、そういった問題があります。VMM呼び出し命令を実行する時には命令そのものがあるページは必ず存在しているということを利用し、VMM呼び出し命令と同じページに機能名を入れることで、システムコールを使用せずにこの問題を解決します。
この条件を満たす方法について、機能名がページをまたいでしまうとうまくいきませんが、機能名はページサイズと比較して十分に小さいものとみなし、機能名とVMM呼び出し命令とジャンプ命令を、2回分連続で並べることで、どちらかが条件を満たすようにします。ページサイズはIA-32/AMD64 (Intel 64) なので4,096バイトです。機能名は1,000バイトもないのでこれでうまくいきます。CALL_VMM_GET_FUNCTIONマクロはこれをインラインアセンブリで記述し、そのポインターを本体の処理に渡して処理してもらうようになっています。本当はアセンブラーの機能でこの条件を無駄なく満たすようアラインメントがかければよかったのですが...
VMCALL命令とVMMCALL命令の判断は、SIGILLとSIGSEGVのシグナルハンドラーを一時的に登録し、さらに、setjmp/longjmpを利用してシグナルハンドラーからの脱出を行うようにして、まずVMCALL命令を試し、シグナルが発生したらVMMCALL命令を試す、というやり方で行います。もともとはこれでWindowsでもGNU/Linuxでも同じソースコードでいけていたのですが、残念ながらglibc version 2.19以降は互換性がなくなり、sigsetjmp/siglongjmpを使わなければならなくなったため、#ifにより記述をわけてあります。
使用例
dbgsh
dbgshは簡単なレジスター渡しで1バイトずつ入出力のやり取りをすることで実装されています。デバッグ用の機能で、特に権限の制限はありません。
機能名: "dbgsh"
- 入力:
- %eax=ファンクション番号
- %ebx=(32ビット) 入力される1バイト、または、-1
- 出力:
- %eax=出力される1バイト、0x10A、0、または、-1
VMM側から0が渡されると、入力の要求を意味します。キーボード等からの入力を待ち、次のVMM呼び出しでその1バイトを入力します。入力の要求がなければ-1を入力します。
VMMから0x10Aが渡されると、終了の合図です。exitコマンドが入力された場合などにこの値が返されます。以前のバージョンのdbgshコマンドはこの値を処理しておらず、単に0x0A ('\n') を出力して動作を続けます。
特に入力の要求がなければVMM側に-1を入力しますが、VMM側も、出力する中身がなく入力待ちでもなければ-1を出力します。
dbgsh機能からはdebugコマンドでメモリーダンプなど危険な操作が行えますので、有効・無効をdefconfig/bitvisor.confで設定できるようになっています。無効に設定されている場合でも本インターフェイス自体は機能しますが、シェルは実行されず、単にdbgsh is disabledのようなメッセージを出力するようになっています。
log
VMMのログをゲストオペレーティングシステムに提供します。dbgshとは異なり特権レベル0のプログラムのみが使用できます。特権レベルが0でない場合、処理は何も行われません。
機能名: "log_set_buf"
- 入力:
- %eax=ファンクション番号
- %ebx=バッファーの物理アドレス
- %ecx=バッファーの大きさ (バイト数)
- 出力:
- なし
Linuxカーネルモジュールなど、カーネルレベルからの呼び出しを想定しており、スワップアウトされないバッファーの物理アドレスを指定するAPIになっています。バッファーの先頭32ビットはゲストオペレーティングシステムが未回収のバイト数、残りがログを記録するリングバッファーとなっており、VMM側でバイト数をインクリメント、ゲストオペレーティングシステム側でバイト数をデクリメントします。