ご存知の通りBitVisorのNMI処理には以下のパターンがあります:
- 仮想マシン上のプログラムを実行中のNMI - #VMEXITで処理
- ハイパーバイザーを実行中のVMI - NMIハンドラーで処理
いずれにしても、VMM側のpanicの場合を除き、仮想マシン上のオペレーティングシステムのNMIハンドラーが呼び出される形になります。
それで、#VMEXITであればすぐにNMIをinjectすればいいし、NMIハンドラーが実行された場合は内部でカウンターをインクリメントしておき、適切なタイミングでinjectすればいいのですが、特にvmlaunch/vmresume/vmrun命令の周辺では、際どいタイミングというものが存在するわけです。それをどう対策しているのか簡単に紹介します。
Intelプロセッサ用
Intelプロセッサの場合NMIをマスクすることができません。そのためvmlaunch/vmresume命令の直前までNMIが発生する可能性があります。それを逃さず処理する仕掛けがあります。
その仕掛けを設定している部分は、core/asm.s
の中を%gs:gs_nmi_critical
というキーワードで検索していただくと見つかります。vmlaunch/vmresume命令を使うより前にここにアドレスを設定し、後で0を設定しています。このアドレスに書き込まれている3つのアドレスは、順に、際どい部分の開始アドレス、際どい部分の終了アドレス(範囲の次のバイト)と、際どい部分を実行中にNMIが発生した場合の戻り先のアドレスを表します。この範囲内を戻り先アドレスとするNMIが発生したら、戻り先アドレスを書き換えて、残りの部分は強制的にすっ飛ばしてしまう仕掛けです。
そして、この範囲内で%gs:gs_nmi_count
をチェックします。これは通常のNMIの際にインクリメントされます。範囲外でNMIが発生して、そのままここに来た場合はこのチェックに引っかかり、NMI用の脱出口にジャンプするのでvmlaunch/vmresume命令は実行されません。範囲内でNMIが発生した場合はNMIハンドラーから戻り先のアドレスが変更され、強制的に同じ脱出口にジャンプします。
なぜこのような範囲を設定しているかというと、vmlaunch/vmresume命令を実行する直前までは、すっ飛ばして欲しいけれど、実行した後は、そのVM Exitを処理する必要があって、すっ飛ばしてほしくないためです。NMIがマスクできない以上、際どい部分はそのようにNMIハンドラー側で何とかする必要がありました。
NMIハンドラーはcore/nmi_handler.s
にあります。戻り先のアドレスを見て差し替える処理があります。
AMDプロセッサ用
AMDの場合はSVMの機能によりNMIをマスクできますのでもう少し簡単です。core/asm.s
の中を見ていただくと、vmrun命令より手前にclgiという命令があります。これが、NMIを含めて割り込み禁止にする命令です。(sti命令でセットされる割り込み許可フラグよりも優先されるので、clgiを実行した状態では外部割り込みもすべて発生しません。) この状態で%gs:gs_nmi_count
をチェックすれば、ここからvmrun命令までにNMIが発生して値が変化することはありません。
vmrun命令はclgiのまま実行します。仮想マシン上に実行が移った時には自動的にstgiを実行したのと同じ状態になっており、NMIも受け付けられます。clgi命令実行後にNMI信号が入っていた場合、そのNMIはvmrun命令の直後に受け付けられ、即座に#VMEXITとなります。
また、#VMEXIT後はclgiの状態になるので、必要なレジスターの切り替え処理後にstgi命令を実行してNMIを受け付けられるようにしています。