BitVisorのブートシーケンス解説 (UEFI編)

最近BitVisorのブートシーケンス(UEFI)を真面目に追うことがあったので，それについて簡単にまとめます．

BitVisorの起動については既にいろいろと文章があります（参考文献参照）が，ここでは主に全体の起動の流れに着目して説明しています．

## UEFIブートの概要
UEFIブートの仮想化の全体像は以下のようになります．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153099/34d915e3-2529-d146-0113-e149083d1788.png)


loadvmm.efiからbitvisor.elfの先頭部分(2nd-loader)を読み出し，BitVisor側に制御を移します．
2nd-loaderはbitvisorl.elf全体を読み出したあと，初期化をして最終的にVMMを起動します．
VMMはゲストとして最初トランポリンコードを実行し，このトランポリンコードからloadvmm.efiへreturnしますが，この時点で既にCPUは仮想化状態にあります．

なお，BitVisorは64bit UEFI firmwareのみサポートしています．この場合longmodeで処理が開始します．

## UEFIブートの順序 (BSP)
entryから概ね以下の順序でVMMを起動します．

```
loader.elf:entry_func()
   => entry.s:entry
   => entry.s:uefi64_entry
   => uefi.c:uefi_init
   => entry.s:uefi_entry_start
   => entry.s:callmain64
   => main.c:vmm_main
   => call_initfunc("global")
   => main.c:start_all_processors
   => main.c:bsp_continue
   => (change stack)
   => main.c:bsp_proc
      => call_initfunc("bsp")
      => call_initfunc("para")
      => call_initfunc("pcpu")
  => main.c:create_pass_vm (from "pcpu" intfunc)
      => bps == true
      => vcpu.c:load_new_vcpu()
      => call_initfunc("pass")
      => vmmcall_boot.c:vmm_call_boot_enable()
         => create vmmcall_boot_thread
           => wait_for_boot_continue
           => continue_flag = false
           => wait until continue_flag become true
         => schedule => vmmcall_boot_thread
      => (scheduled from vmmcall_boot_thread)
   => vmctrl.start_vm (vt_main.c:vt_start_vm)
   => vt_mainloop()
```

init thread

```
vmmcall_boot.c:vmmcall_boot_thread
  => main.c:bsp_init_thread
    => main.c:initregs
       => calluefi.c:copy_uefi_bootcode()
         => if boot from uefi-loader-login
           => vmmcall_boot_continue
               => continue_flag = true
           => schedule
           => return to create_pass_vm
           => (run vm w/o driver initialization)
           => return to loader (boot/uefi-loader-login/loadvmm.c)
              => authentication is performed in the loader
              => loadvmm.c:decrypt_intl
                  => vmmcall_loadcfg64_intl => vmmcall_boot.c:loadcfg64
                  => vmmcall_boot_intel => vmmcall_boot.c:boot_guest
                      => wait_for_boot_continue
                      => continue_flag = false
                      => as a result, bsp_init_thread wakes up
                      => vmmcall handler wait until continue_flag become true
                      => call_initfunc("config0")
       => call_initfunc("drivers")
       => call_initfunc("config0")
  => continue_flag = true
  => thread exit
  => schedule
  => return to create_pass_vm
```

BitVisorのブートコードはBIOSブートの場合と，UEFIブートの場合両方の処理が記載されています．
UEFIブートの場合は基本`if(uefi_booted)`の中身を見ていくことになります．

init threadについては後述します．

### initfunc
`call_initfunc()`では`INITFUNC`マクロで登録された関数をアルファベット順に呼び出しています．

initfuncで呼ばれる関数はconfigにより変わりますが，例えば以下のような順序で呼ばれます． (CPUコア数2の場合)

```
global, global0, core/callrealmode.c:callrealmode_init_global
global, global0, core/panic.c:panic_init_global
global, global0, core/printf.c:printf_init_global
global, global0, core/seg.c:segment_init_global
global, global0, core/sleep.c:sleep_init_global
global, global0, core/tty.c:tty_init_global
global, global1, core/int.c:int_init_global
global, global1, core/main.c:print_boot_msg
global, global2, core/mm.c:mm_init_global
global, global3, core/acpi.c:acpi_init_global
global, global3, core/main.c:copy_minios
global, global3, core/main.c:get_shiftflags
global, global3, core/panic.c:panic_init_global3
global, global3, core/process.c:process_init_global
global, global3, core/thread.c:thread_init_global
global, global3, core/time.c:time_init_global
global, global3, core/tty.c:tty_init_global2
global, global3, core/vmmcall_dbgsh.c:vmmcall_dbgsh_init_global
global, global3, core/vmmcall_status.c:vmmcall_status_init_global
global, global4, core/cpu_mmu_spt.c:cpu_mmu_spt_init_global
global, global4, core/msg.c:msg_init_global
msg, msg0, core/beep.c:beep_init_msg
msg, msg0, core/keyboard.c:keyboard_init_msg
msg, msg0, core/panic.c:panic_init_msg
msg, msg0, core/reboot.c:reboot_init_msg
msg, msg0, core/serial.c:serial_init_msg
msg, msg0, core/sleep.c:sleep_init_msg
msg, msg0, core/time.c:time_init_msg
msg, msg0, core/vramwrite.c:vramwrite_init_msg
msg, msg0, drivers/pci_monitor.c:pci_monitor_init_msg
msg, msg0, ip/echoctl.c:echoctl_init_msg
msg, msg1, core/tty.c:tty_init_msg
global, global5, core/uefi_param_ext.c:uefi_param_ext_init
bsp, bsp0, core/gmm_pass.c:install_int0x15_hook
bsp, bsp0, core/main.c:debug_on_shift_key
bsp, bsp0, core/random.c:random_init_global
bsp, bsp0, core/vramwrite.c:vramwrite_init_bsp
paral0, paral00, core/vcpu.c:vcpu_init_global
paral0, paral01, core/iccard.c:idman_init_global
paral0, paral01, core/time.c:time_init_global_status
paral0, paral01, core/vt_main.c:vt_register_status_callback
paral1, paral10, core/vmmcall.c:vmmcall_init
vmmcal, vmmcal0, core/vmmcall_boot.c:vmmcall_boot_init
vmmcal, vmmcal0, core/vmmcall_dbgsh.c:vmmcall_dbgsh_init
vmmcal, vmmcal0, core/vmmcall_status.c:vmmcall_status_init
paral2, paral20, core/timer.c:timer_init_global
paral3, paral30, core/acpi.c:acpi_init_paral
pcpu, pcpu0, core/cpu_mmu_spt.c:cpu_mmu_spt_init_pcpu
pcpu, pcpu0, core/initipi.c:initipi_init_pcpu
pcpu, pcpu0, core/nmi.c:nmi_init_pcpu
pcpu, pcpu0, core/thread.c:thread_init_pcpu
pcpu, pcpu2, core/main.c:virtualization_init_pcpu
pcpu, pcpu3, core/panic.c:panic_init_pcpu
pcpu, pcpu4, core/cache.c:cache_init_pcpu
pcpu, pcpu4, core/time.c:time_init_pcpu
pcpu, pcpu5, core/main.c:create_pass_vm
vcpu, vcpu0, core/cache.c:cache_init_vcpu
vcpu, vcpu0, core/cpuid.c:cpuid_init
vcpu, vcpu0, core/initipi.c:initipi_init
vcpu, vcpu0, core/io_io.c:io_io_init
vcpu, vcpu0, core/localapic.c:localapic_init
vcpu, vcpu0, core/mmio.c:mmio_init
vcpu, vcpu0, core/msr.c:msr_init
vcpu, vcpu0, core/nmi.c:nmi_init
vcpu, vcpu0, core/xsetbv.c:xsetbv_init
pass, pass0, core/cache.c:cache_init_pass
pass, pass0, core/cpuid_pass.c:cpuid_pass_init
pass, pass0, core/exint_pass.c:exint_pass_init
pass, pass0, core/gmm_pass.c:gmm_pass_init
pass, pass0, core/initipi_pass.c:initipi_pass_init
pass, pass0, core/io_iopass.c:io_iopass_init
pass, pass0, core/msr_pass.c:msr_pass_init
pass, pass0, core/nmi_pass.c:nmi_pass_init
pass, pass0, core/xsetbv_pass.c:xsetbv_pass_init
pass, pass1, core/io_iohook.c:setiohooks
pass, pass5, core/localapic.c:localapic_init2
config0, config00, core/random.c:random_init_config0
driver, driver0, core/mmio.c:mmio_debug
driver, driver0, core/vga.c:vga_init
driver, driver0, drivers/core.c:core_init
driver, driver0, drivers/pci_match_compat.c:pci_match_compat_init
driver, driver0, net/netapi.c:netapi_init
driver, driver1, drivers/nvme/nvme_crypt.c:nvme_crypt_ext
driver, driver1, drivers/nvme/nvme_storage_io.c:nvme_storage_io_ext
driver, driver1, idman/kernel.c:idman_kernel_init
driver, driver1, ip/net_main.c:net_main_init
driver, driver1, storage/kernel.c:storage_kernel_init
driver, driver1, vpn/kernel.c:vpn_kernel_init
driver, driver2, ip/telnet-dbgsh.c:telnet_dbgsh_init
driver, driver2, storage_io/storage_io.c:storage_io_initfunc
driver, driver3, drivers/ata/ata_init.c:ata_init
driver, driver3, drivers/ieee1394.c:ieee1394_init
driver, driver3, drivers/ieee1394log.c:ieee1394_thunderbolt_conceal
driver, driver3, drivers/ieee1394log.c:ieee1394log_init
driver, driver3, drivers/net/bnx.c:bnx_init
driver, driver3, drivers/net/pro100.c:pro100_init
driver, driver3, drivers/net/pro1000.c:vpn_pro1000_init
driver, driver3, drivers/nvme/nvme.c:nvme_init
driver, driver3, drivers/pci_conceal.c:pci_conceal_init
driver, driver3, drivers/pci_monitor.c:pci_monitor_init
driver, driver3, drivers/usb/ehci.c:ehci_init
driver, driver3, drivers/usb/uhci.c:uhci_init
driver, driver3, drivers/usb/xhci.c:xhci_init
driver, driver3, drivers/x540.c:x540_init
driver, driver4, core/iccard.c:idman_init
driver, driver5, core/vmmcall_iccard.c:vmmcall_iccard_init
driver, driver90, drivers/pci_init.c:pci_init
driver, driver95, drivers/iommu.c:iommu_setup
driver, driver99, drivers/ieee1394log.c:ieee1394_hint
config1, config10, drivers/net/bnx.c:bnx_set_hotplug

ap, ap0, core/mm.c:unmap_user_area
ap, ap0, core/process.c:process_init_ap
pcpu, pcpu0, core/cpu_mmu_spt.c:cpu_mmu_spt_init_pcpu
pcpu, pcpu0, core/initipi.c:initipi_init_pcpu
pcpu, pcpu0, core/nmi.c:nmi_init_pcpu
pcpu, pcpu0, core/thread.c:thread_init_pcpu
pcpu, pcpu2, core/main.c:virtualization_init_pcpu
pcpu, pcpu3, core/panic.c:panic_init_pcpu
pcpu, pcpu4, core/cache.c:cache_init_pcpu
dbsp, dbsp4, core/cache.c:update_mtrr_and_pat
dbsp, dbsp4, core/time.c:time_init_dbsp
pcpu, pcpu4, core/time.c:time_init_pcpu
dbsp, dbsp5, core/main.c:wait_for_create_pass_vm
pcpu, pcpu5, core/main.c:create_pass_vm
vcpu, vcpu0, core/cache.c:cache_init_vcpu
vcpu, vcpu0, core/cpuid.c:cpuid_init
vcpu, vcpu0, core/initipi.c:initipi_init
vcpu, vcpu0, core/io_io.c:io_io_init
vcpu, vcpu0, core/localapic.c:localapic_init
vcpu, vcpu0, core/mmio.c:mmio_init
vcpu, vcpu0, core/msr.c:msr_init
vcpu, vcpu0, core/nmi.c:nmi_init
vcpu, vcpu0, core/xsetbv.c:xsetbv_init
pass, pass0, core/cache.c:cache_init_pass
pass, pass0, core/cpuid_pass.c:cpuid_pass_init
pass, pass0, core/exint_pass.c:exint_pass_init
pass, pass0, core/gmm_pass.c:gmm_pass_init
pass, pass0, core/initipi_pass.c:initipi_pass_init
pass, pass0, core/io_iopass.c:io_iopass_init
pass, pass0, core/msr_pass.c:msr_pass_init
pass, pass0, core/nmi_pass.c:nmi_pass_init
pass, pass0, core/xsetbv_pass.c:xsetbv_pass_init
pass, pass1, core/io_iohook.c:setiohooks
pass, pass5, core/localapic.c:localapic_init2
```

なお，`call_initfunc("para")`では名前的に並列に実行可能な関数を呼び出しているようです．
もともとBIOSからブートした場合，マルチコアであれば並行して初期化を進めますが，どれかのコアで一度だけ実行すれば良い処理がこのinitfuncのparaで定義されているのだと思います．
ただし，後述するようにUEFIではAPの仮想化は遅延されるので，`call_initfunc("para")`はBSPのみで逐次的に実行されます．

## init thread
`create_pass_vm`の中でスレッドを生成して一部の初期化をしています．
この部分はローダーの種類により，処理内容が異なります．

なお，BitVisorのスレッドは割り込みによるプリエンプションはありません．
スレッドが自ら`schedule()`を呼び出すと他のスレッドがスケジュールされます．

### uefi-loader
`boot/uefi-loader`では，init threadは以下のような挙動になります．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153099/d43f3713-d196-681d-6e16-bbc7428148cc.png)

結果的にはinit threadは実際にはただの関数呼び出しのようになりす．
何故こういうことをしているかというと，もともとBIOSブートの方で後述のuefi-loader-loginみたいなことをしていて，その構成を再利用したからじゃないかと思います．多分．

### uefi-loader-login
`boot/uefi-loader`では，init threadは以下のような挙動になります．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153099/ba15fa7a-1268-852b-2211-4ee33e5a0eb9.png)


uefi-loader-loginではディスクの暗号化に関して認証化をおこないます．
暗号鍵が分からないとデバイスドライバの初期化ができないため，一旦ゲストをデバイスドライバの初期化なしで起動し，その後loadvmm.efiからVMCALLでパスワードを受け取り，認証が成功したらデバイスドライバの初期化をおこなう，という挙動になっています．

これを実現するためにinit threadで`boot_continue`フラグをうまく利用してVMMとゲストとの同期をとっています．

## ゲストのトランポリンコード
calluefi.cの`copy_uefi_bootcode()`で，一番はじめにVMENTRYした際に実行されるコードを作成し，レジスタに設定しています．

このときのトランポリンコードのジャンプ先のアドレス`uefi_entry_ret_addr`は最初の`uefi64_entry`の中でストアしています．

## APの仮想化
loadvmm.efiを実行終了した段階では，BSP (Bootstrap Processor; 最初に起動するコア)のみ仮想化されています．
AP (Application Processor; BSP以外のコア)はこの時点では仮想化されていません．

このことは，loadvmm.efi呼び出し直後にdbgshからログを確認すると分かります．

APの仮想化は，ゲストがAPの初期化をするためにStartup-IPIをAPに対して送信しようとした段階でおこなわれます．
このために，IPIを送信するためのLocal APICのメモリアクセスを最初補足するようにしています．

AP仮想化のおおまかなコードの流れは以下のようになります．


BSP側

```
localapic.c:mmio_apic
  => handle_ap_start
    => ap.c:ap_start
    - copy cpuinit_start code to apinit addr
        => ap_start_addr
        => apic_send_startup_ipi
        => start AP
  => call_initfunc("dbsp")
  => main.c:wait_for_create_pass_vm
```


AP側

```
receive SIPI
  => entry.s:cpuinit_start
  => entry.s:call_main64
  => ap.c:apinitproc0
  => (change stack)
  => ap.c:apinitproc1
  => int_init_ap() (initialize IDT)
  => initproc_ap (= ap_initproc = ap_proc)
    => call_initfunc("ap")
    => call_initfunc("para")
    => call_initfunc("pcpu")
  => create_pass_vm
    => load_new_vcpu
    => vmctl.vminit
    => call_initfunc("pass")
    => initregs
    => vmctrl.init_signal
  => start_vm
```

BSP側から`ap_start_addr()`でINIT, SIPI, SIPIと3つIPIを飛ばしてAPを起動させます．
この処理はmultiprocessor specificationに基づくものです．

SIPIを受け取ったコアは，IPIのベクタ番号(下位8bit) × 4096 の物理アドレスからリアルモードで実行を開始します．
BitVisorでは物理アドレスの0番地に一時的にトランポリンコードを挟み，初期化関数に飛ぶようにしています (ap.c:ap_start()のmapmemの処理)．
(そういえば，もし仮に物理アドレス0番地が使用不可能領域だと困りますね．まぁ普通そんなことはないでしょうか．）

BSP側の`call_initfunc("dbsp")`(delayed BSP?)はAP側の初期化コード内にいくつかある同期をとるコード(`sync_all_processors()`)のために存在します． 
なお，いくつか同期を取る主な理由は実装都合（デバッグがしやすい等）みたいです．

## 参考文献
- 榮樂 英樹, [BitVisorのUEFI対応](https://www.bitvisor.org/summit2/slides/bitvisor-summit-2-03-eiraku.pdf), BitVisor Summit 2, 2013.
- @hdk_2, [BitVisor本体のブート仕様](https://qiita.com/hdk_2/items/b73161f08fefce0d99c3), BitVisor Advent Calendar 2015.
- @deep_tkkn, [BitVisor ブート時に通るソースコードを辿ってみる](https://qiita.com/deep_tkkn/items/a05f87749ee352547e40), BitVisor Advent Calendar 2015.
- @retrage01, [BitVisorのEFI向けVMM Loader(1st stage)のコードを読んでみる](https://retrage01.hateblo.jp/entry/2017/12/07/020528), BitVisor Advent Calendar 2017.
- @kotatsu_mi, [BitVisor の UEFI 向けのブートローダーを読む（2nd stage）](https://orumin.blogspot.com/2017/12/bitvisor-uefi-2nd-stage.html), BitVisor Advent Calendar 2017.
- @hdk_2, [BitVisor boot/uefi-loader-loginの使い方](https://qiita.com/hdk_2/items/5f2556e6fede840607bc), BitVisor Advent Calendar 2017.