UEFI環境でのBitVisorのマルチプロセッサ開始が遅い理由

UEFI環境では、BitVisorのマルチプロセッサ開始がBitVisor起動のタイミングではなく、ゲストオペレーティングシステムがlocal APICにアクセスしてINITやstart-up IPIを送ろうとしたタイミングになっています。その理由について簡単に紹介します。

# UEFI対応当初の資料

https://bitvisor.org/summit2/#program
「BitVisorのUEFI対応」

この資料ではExitBootServices()でフリーズする環境があったとあります。あっ、理由はこれで説明できてしまいました:sweat_smile:

# 今は?

実際にUEFI環境ではどういう状態になっているのか、観察してみます。QEMUを使うのが簡単です。

```shell-session:
$ qemu-system-x86_64 -bios OVMF.fd -smp 2 -nographic
```

OVMF.fdのパスは、DebianパッケージのQEMUとOVMFを使用している場合はこれでいけますが、環境に合わせて修正してください。こうして起動してしばらく待つとEFI Shellのプロンプトが出てきます。そうしたら、C-a c (Ctrl+a c) を押して、QEMUモニターに切り替えます。そして、info cpusというコマンドを入れると、各CPUの状態が出ます。

```
QEMU 2.8.1 monitor - type 'help' for more information
(qemu) info cpus
* CPU #0: pc=0x0000000007dc61c1 (halted) thread_id=1892
  CPU #1: pc=0x0000000007dc61c1 (halted) thread_id=1890
```

どちらもhaltedとなっていて、プログラムカウンターは同じ値になっています。つまり、UEFIファームウェアは、マルチプロセッサを開始して何らかのプログラムを走らせているようです。実際にどんな命令が実行されているかは、逆アセンブルすればわかります。haltedですから十中八九hlt命令でしょうが...

```
(qemu) x/2i 0x0000000007dc61c1-1
0x0000000007dc61c0:  hlt    
0x0000000007dc61c1:  retq   
```

やはりそうでした。前述の資料にあったような、スピンロックを繰り返すといった挙動は見られませんが、何らかの形でプロセッサを開始してはいるようですので、勝手にそれを止めてしまったらおかしなことになるのは予想できます。

# BitVisorを使って調べる

実機で確認しようと思うとなかなか面倒なのですが、オペレーティングシステムの開始までにどんなIPIが送られるか、くらいなら調べることができます。

```diff:
diff --git a/core/localapic.c b/core/localapic.c
--- a/core/localapic.c
+++ b/core/localapic.c
@@ -35,6 +35,8 @@
 #include "mmio.h"
 #include "msr_pass.h"
 #include "panic.h"
+#include "printf.h"
+#include "time.h"
 
 #define APIC_BASE	0xFEE00000
 #define APIC_LEN	0x1000
@@ -166,6 +168,12 @@ mmio_apic (void *data, phys_t gphys, boo
 
 	apic_icr_low = buf;
 	if (ap_start) {
+		apic_icr_high = mapmem_hphys (APIC_ICR_HIGH,
+					      sizeof *apic_icr_high,
+					      MAPMEM_PCD | MAPMEM_PWT);
+		printf ("[%10llu] APIC_ICR: %08X_%08X\n", get_time (),
+			*apic_icr_high, *apic_icr_low);
+		unmapmem (apic_icr_high, sizeof *apic_icr_high);
 		if (!handle_ap_start (*apic_icr_low))
 			return 0;
 	}
```

こんな感じでIPIを送るタイミングでログに残すようにして、後で確認すると以下のようになります。

```
Starting a virtual machine.
[   6014544] APIC_ICR: 01000000_00000400
[   6014950] APIC_ICR: 02000000_00000400
[   6014961] APIC_ICR: 03000000_00000400
[   6014972] APIC_ICR: 04000000_00000400
[   6014982] APIC_ICR: 05000000_00000400
[   6014994] APIC_ICR: 06000000_00000400
[   6015004] APIC_ICR: 07000000_00000400
[   6015015] APIC_ICR: 08000000_00000400
[   6015025] APIC_ICR: 09000000_00000400
[   6015036] APIC_ICR: 0A000000_00000400
[   6015047] APIC_ICR: 0B000000_00000400
[   6015058] APIC_ICR: 0C000000_00000400
[   6015071] APIC_ICR: 0D000000_00000400
[   6015083] APIC_ICR: 0E000000_00000400
[   6015095] APIC_ICR: 0F000000_00000400
[   6015325] APIC_ICR: 01000000_00000400
[   6015335] APIC_ICR: 02000000_00000400
[   6015345] APIC_ICR: 03000000_00000400
[   6015354] APIC_ICR: 04000000_00000400
[   6015363] APIC_ICR: 05000000_00000400
[   6015372] APIC_ICR: 06000000_00000400
[   6015382] APIC_ICR: 07000000_00000400
[   6015391] APIC_ICR: 08000000_00000400
[   6015400] APIC_ICR: 09000000_00000400
[   6015409] APIC_ICR: 0A000000_00000400
[   6015419] APIC_ICR: 0B000000_00000400
[   6015428] APIC_ICR: 0C000000_00000400
[   6015438] APIC_ICR: 0D000000_00000400
[   6015447] APIC_ICR: 0E000000_00000400
[   6015456] APIC_ICR: 0F000000_00000400
[   6877868] APIC_ICR: 01000000_0000C500
Processor 1 (AP)   
Processor 2 (AP)   
Processor 3 (AP)   
(以下略)
```

長い... 400というのはNMIのようです。最上位の8ビットが宛先ですから、各プロセッサに対してNMIを2回送っているようです。それからC500のところ、それがINITを送るところで、それを見つけたBitVisorは直ちにマルチプロセッサの開始処理を始めます。

結局何をやっているのかはっきりしませんが、NMIを送っているので、それにきちんと応答がないとその先正常に動かなくなってしまうだろうということは予想できます。

# もうちょっと調べるには

EDK II
https://github.com/tianocore/edk2

おそらくたいていのUEFIファームウェアはこれをベースに実装されているでしょうから、これを読むと具体的な処理がわかるのではないかと思います。少し見た感じでは、SendInitSipiSipi()関数などマルチプロセッサの開始処理のようなルーチンはすぐに見つかりましたが、NMIを送るようなルーチンを見つけることはできませんでした。