BitVisorとNMIについて書きます.対象アーキテクチャはIntelです.AMDではNMI処理が異なるはずです.
NMI (Non-Maskable Interrupt)
NMIは名前の通りマスク不可能な割り込みです.Intelの場合,cli
命令でInterrupt FlagをクリアしてもNMIはブロックされません.
NMIは主にHWエラーの通知に利用されます.サーバによっては物理的なNMIボタンがあって,それを押せばNMIを発生させることができるものもあります.また,APICのDelivery modeを設定することでもNMIを発行できます.例えば,パフォーマンスカウンタ(PMU)には一定以上の閾値に到達した際割り込みを発生させる機能がありますが,LinuxではPMUを利用したサンプリング(i.e. perf)をする際はNMIを利用しています.
NMI自体の有効・無効は切り替えることができます.こちらはチップセット依存ですが,通常のマシンであればI/Oポートの0x70番地がNMI enableレジスタです.
NMIハンドラ
NMIはIRQ番号2番が割り当てられています.そのためNMIが発生した場合はIDTのインデックス2番目のハンドラが実行されます.NMI受信を受信してNMIハンドラが呼び出されると,iret
命令が実行されるまで,後続のNMIはブロックされます.
この"再度iret
が実行されるまで"というは実は曲者です.というのもNMIハンドラ実行中にpage faultやブレークポイント命令などによる例外が発生し,その例外ハンドラから戻る際にiret
命令が発行されると,NMIのブロッキングも解除されてしまいます.従ってその後のNMIハンドラ処理中に別のNMIを受信する可能性があります.これに対応するにはNMIハンドラをリエントラントにするか,あるいはNMIハンドラ中に例外が発生しないようにする必要があります.
Linuxはnested NMIに対応していて,以下に解説があります.
- Steven Rostedt, "The x86 NMI iret problem", https://lwn.net/Articles/484932/, March 7, 2012.
仮想化時 (VMX operation中)のNMIの処理
仮想化時ではNMIの挙動が少し変化します.
NMIは, VMX root mode 及び VMX non-root mode それぞれ受信する可能性があり,それに応じた処理が必要になります.
VMX root modeでのNMI処理
こちらは通常の仮想化時でない場合と同じです.NMIを受信するとIDT2番のハンドラが実行され,iretが実行されるまで後続のNMIはブロックされます.
VMX non-root modeでのNMI処理
ここが少々ややこしいところです.
まず,NMIに関連するVMCSレジスタは以下にあります.
- Pin-based VM-Execution Cnotrol
- NMI exiting (5bit目)
- Virtual NMIs (6bit目)
- Primary Processor-based VM-Execution Cnotrol
- NMI-window exiting (22bit目)
- Guest Non-Register State
- Blocking by NMI (3bit目)
VMX non-root mode時にVMEXITが発生するかどうかは,pin-based controlのNMI exitingが管理します.また,NMI exitingが1のときは,Virtual NMIsという項目もあって,これを0にするか1にするかで動作が変わるのですが,基本的にVirtual NMIsは1にすると思うので(NMI exitingが0のときは,Virtual NMIsは0でなければならない),以下ではNMI exitingが1のときはVirtual NMIが1の場合について説明します(Virtual NMIsは最初期のVT-xには存在しなくて,後から追加された機能のようです).
NMI-exiting が0のとき
NMIはそのままゲストに通知され,ゲストのIDT2番のハンドラが実行されます.
ゲストがiretを実行するまでNMIはブロックされます.
また,ホストからNMIを挿入すると,Blocking by NMI bitが1になります.
そして,ゲストがiretを実行するまでNMIがブロックされます.
(ゲストがiretを実行すると,Blocking by NMIのbitがクリアされる(多分.未確認))
NMI-exiting が1のとき
NMIが発生した場合,NMI_OR_EXCEPTION
というexit reasonでVMEXITが発生します.
このとき,NMIはブロックされません(SDMにはNMIによってNMIハンドラが起動したとき,後続のNMIはブロックされると書いてありますが,NMIでVMEXITしたときについては何も言及がありません.ブロックしないとも言ってないが,ブロックするとも言っていない.結果的にはブロックしないようです).従ってNMIのVMEXITを処理中に別のNMIを受信する可能性があります.
ホストからNMIを挿入すると,Blocking by NMI bitが1になりますが,virtual NMIsが1のとき,NMIはブロックされません.したがってゲストがNMIハンドラ実行中に別のNMIを受信してVMEXITする可能性があります.virtual NMIsが1のとき,ゲストがiretするとBlocking by NMI bitが0になります.
virtual NMIsはNMI-Window exitingという機能のためにあります.
NMI-window exitingを1にすると,Blocking by NMI bitが0になったとき(=ゲストがNMIを受信可能になったとき)にVMEXITが発生します(NMI-windowというexit reasonになる).ホストがゲストにNMIを挿入したいとき,ゲストがNMI処理中で挿入できない場合があります.そういった場合にNMI-window exitingを1にしておけば,ゲストが受信可能なタイミングでVMEXITするので,そのタイミングでNMIを挿入すれば良いことになります.
なお,割り込みに対してもこれと同様の機能がinterrupt-windowという名称であります.
BitVisorのNMIハンドラ
BitVisorではNMI exiting及びvirtual NMIsが有効になっています.また,必要に応じてNMI-window exitingを利用します.
パススルーが基本のBitVisorでなぜNMI exitingさせているかというと,panicする際にNMIとしてIPIを送信しているからです.それ以外の場合は,VMX non-root, VMX root時のいずれでも受信したNMIはゲストに再挿入するようになっています.
処理を共通化するために,NMIハンドラおよびNMI VMEXITハンドラはNMIの受信数を示すカウンタをインクリメントするだけで,あとはvt_main_loop()
の中でそのカウンタをチェックして,必要に応じて処理をするという構成になっています.
NMIハンドラ
core/nmi.c, core/nmi_handler.sにあります.
CPUローカルな領域にあるgs_nmi_count
をインクリメントします.
NMI VMEXIT ハンドラ
VMEXITハンドラの中でgs_nmi_count
をインクリメントします.
NMI挿入処理
BitVisorのNMI処理については昨年のBitVisor Summitで発表がありました.
- 榮樂 英樹, BitVisor 2018年の主な変更点, https://www.bitvisor.org/summit7/slides/bitvisor-summit-7-2-eiraku.pdf, BitVisor Summit 7, 2018.
受信したNMIをなるべく早く挿入するために,いろいろと工夫が施されていますが,基本的にはgs_nmi_count
が1以上であれば,vt_nmi_has_come()
関数でNMIを挿入するようです.
パニック処理
panic()
すると,panic_wakeup_all()
=> apic_send_nmi
でAPICを通じてNMIを送信します.
NMIハンドラの中では上述したようにgs_nmi_count
をインクリメントするだけですが,その後vt_main_loop()
の中でpanic_test()
が実行され,panic messageが設定されているかで他コアがpanicしたかを判断しています.
おわり
NMI処理はややこしく,以前困っていたところをhdk_2さんに教えていただきました.ありがとうございます.
IntelはSDMをもうちょっと人間に優しく書いて欲しい
参考文献
- Intel SDM ("NMI exiting"等で検索)