最近BitVisorのブートシーケンス(UEFI)を真面目に追うことがあったので,それについて簡単にまとめます.
BitVisorの起動については既にいろいろと文章があります(参考文献参照)が,ここでは主に全体の起動の流れに着目して説明しています.
UEFIブートの概要
UEFIブートの仮想化の全体像は以下のようになります.
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は以下のような挙動になります.
結果的にはinit threadは実際にはただの関数呼び出しのようになりす.
何故こういうことをしているかというと,もともとBIOSブートの方で後述のuefi-loader-loginみたいなことをしていて,その構成を再利用したからじゃないかと思います.多分.
uefi-loader-login
boot/uefi-loader
では,init threadは以下のような挙動になります.
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対応, BitVisor Summit 2, 2013.
- @hdk_2, BitVisor本体のブート仕様, BitVisor Advent Calendar 2015.
- @deep_tkkn, BitVisor ブート時に通るソースコードを辿ってみる, BitVisor Advent Calendar 2015.
- @retrage01, BitVisorのEFI向けVMM Loader(1st stage)のコードを読んでみる, BitVisor Advent Calendar 2017.
- @kotatsu_mi, BitVisor の UEFI 向けのブートローダーを読む(2nd stage), BitVisor Advent Calendar 2017.
- @hdk_2, BitVisor boot/uefi-loader-loginの使い方, BitVisor Advent Calendar 2017.