先日VirtualBox 6.0がリリースされました.リリースノートには書いてありませんが,ついにNested Virtualizationがサポートされたという噂を耳にしたので,今回はVirtualBox上でBitVisorが動作するのか検証しよう..と思ってましたが,よくよく調べてみると現時点ではAMDのCPU(の一部)をサポートということらしく,あいにく手持ちのAMD CPUが無いので断念しました.AMDなCPUお持ちの方,試してみてはいかがでしょうか1.
ということで別の話ということで,BitVisorのUEFIブート時(BIOSブート時もですが)には何回かページテーブルを切り替える処理が存在します.今回はその話です.
1. ブート用ページテーブルへの切り替え
UEFIのファームウェアは64bit環境であればlong modeで開始されます.このときページテーブルは仮想アドレス=物理アドレスとなるIdentity mappingが設定されています.
loadvmm.efi
からentry.s
のエントリポイントにジャンプした際,以下のようなページテーブルに切り替えます.
このためのページテーブルがentry_pml4
,entry_pdp
,entry_pd0
,entry_pd
です.
BitVisorのコードは0x40100000からのアドレスに配置されるので(リンカスクリプト),そのためのページテーブルを作成しています.このとき2MBページングを使って,一つのPDPTEエントリで0x40000000-0x7FFFFFFFの1GBの領域をカバーするようにしています.
PTPTE[0]のIdentity mappingはマッピング切り替え用に存在します.
x86でのページテーブルの切り替えはmov cr3
命令を使うだけですが,問題となるのはmov cr3
を実行した直後から仮想アドレスと物理アドレスのマッピングが変わるため,次の命令を実行した際にページフォールトにならないように適切にマッピングを作成しておく必要があるという点です.
UEFIファームウェアはIdentity Mappingで動作しており,さらにloadvmm.efiはBitVisorを物理アドレス1GB以下の領域に配置するので2,このPDPTE[0]のエントリを作成しておけば,このページテーブルをロードしてもページテーブルが発生することはありません.
実際には以下のように絶対アドレスジャンプを利用して,Identity MappingからBitVisorのアドレス空間へ切り替えます.
mov %rax, %cr3
mov $start_stack, %rsp
mov $1f, %rax
jmp *%rax // Change RIP
1: // Use 0x40000000- address
ページテーブル作成処理はこのあたりです.
ちなみに,UEFIファームウェアのページテーブルから独自のページテーブルへ切り替えた際は,割り込み無効化状態で動作させます.これはUEFIの仕様に基づくものです.
2. UEFI関数呼び出しのための切り替え (その1)
無事ページテーブルが切り替えられたら,その後はuefi.cのエントリポイントへ飛んでいきますが,ここで何回かUEFIファームウェアの関数を呼び出しています.
このとき,ページテーブルを元のUEFIファームウェアが使っていたidentity mappingなページテーブルに戻す必要があります.
このための関数がuefi_entry_call
です.
asm
uefi_entry_call:
mov uefi_entry_physoff(%rip),%rax
call uefi_entry_rip_plus_rax
mov %rsp,%rsi
mov uefi_entry_rsp(%rip),%rsp
and $~0xF,%rsp # Force 16-byte alignment; see calluefi
mov uefi_entry_cr3(%rip),%rax
mov %rax,%cr3
cld
sti
xchg %rcx,%rdx
push %r9
push %r8
push %rdx
push %rcx
call *%rdi
cli
mov %rsi,%rsp
lea entry_pml4(%rip),%rsi
mov %rsi,%cr3
ret
...
uefi_entry_rip_plus_rax:
add %rax,(%rsp)
ret
uefi_entry_physoff
には名前から推測するに物理アドレスと仮想アドレスの(負の)オフセットが記録されていると思われるので,call uefi_entry_rip_plus_rax
を利用してその値を戻り値に加算してreturnすることで,PDPTE[1]のアドレス空間から,PDPTE[0]の空間へと切り替えることができます.
その後は引数をスタックに積みUEFIの関数を呼び出します.関数呼び出し後は再びページテーブルをBitVisorのものに戻します.
UEFIアドレス空間からのデータコピーをおこなうuefi_entry_pcpy
も同様のページテーブル切り替え処理をおこなっています.
ちなみに, uefi_entry_call
は以下のように呼ばれています.
uefi_entry_call (uefi_free_pages, 0, alloc_addr64, freesize);
3. メインのエントリ用のページテーブルの切り替え
loadvmm.efiはbitvisor.elfの先頭64KBのみをロードしており,uefi.cのuefi_init()
関数でbitvisor.elf全体がロードされます.bitvisor.elfのロードが終わると,その読み込んだ物理アドレスを引数としてuefi_entry_start
に飛びます.このあとvmm_main()
を呼ぶのですが,その前に,ロードしたbitvisor.elfを適切にマップする必要があります.uefi_entry_start
の先頭でこのためのページテーブルの修正をしています.
uefi_entry_start:
.if longmode
mov uefi_entry_physoff(%rip),%rax
call uefi_entry_rip_plus_rax
mov uefi_entry_cr3(%rip),%rax
mov %rax,%cr3
mov uefi_entry_physoff(%rip),%rax
add $head-0x100000,%rax
neg %rax
add %rdi,%rax
add %rax,entry_pml4-DIFFPHYS(%rdi)
add %rax,entry_pdp+0-DIFFPHYS(%rdi)
add %rax,entry_pdp+8-DIFFPHYS(%rdi)
mov %rdi,%rax
mov $0x83,%al
xor %ebx,%ebx
1:
mov %rax,entry_pd-DIFFPHYS(%rdi,%rbx,8)
add $0x200000,%rax
add $1,%ebx
cmp $512,%ebx
jb 1b
lea entry_pml4-DIFFPHYS(%rdi),%rax
mov %rax,%cr3
ここで,%rdi
にはロードされたbitvisor.elfの先頭の物理アドレスが格納されています.またDIFFPHYSは0x40000000です.
4. BitVisorのメインのページテーブルへの切り替え
BitVisorが実際にメインに利用するページテーブルは,初期化の中でも初期段階に実行されるmm_init_global()
から呼ばれるcreate_vmm_pd()
で作成されます.
64bitで関連するところを抜粋すると以下のようになります.
static void
create_vmm_pd (void)
{
int i;
ulong cr3;
/* map memory areas copied to at 0xC0000000 */
for (i = 0; i < VMMSIZE_ALL >> PAGESIZE2M_SHIFT; i++)
vmm_pd[i] =
(vmm_start_phys + (i << PAGESIZE2M_SHIFT)) |
PDE_P_BIT | PDE_RW_BIT | PDE_PS_BIT | PDE_A_BIT |
PDE_D_BIT | PDE_G_BIT;
entry_pdp[3] = (((u64)(virt_t)vmm_pd) - 0x40000000) | PDPE_ATTR;
asm_rdcr3 (&cr3);
asm_wrcr3 (cr3);
/* make a new page directory */
vmm_base_cr3 = sym_to_phys (vmm_pdp);
memcpy (vmm_pd1, entry_pd0, PAGESIZE);
memset (vmm_pd2, 0, PAGESIZE);
vmm_pdp[0] = sym_to_phys (entry_pd0) | PDPE_ATTR;
vmm_pdp[1] = sym_to_phys (vmm_pd) | PDPE_ATTR;
vmm_pdp[2] = sym_to_phys (vmm_pd1) | PDPE_ATTR;
vmm_pdp[3] = sym_to_phys (vmm_pd2) | PDPE_ATTR;
vmm_base_cr3 = sym_to_phys (vmm_pml4);
vmm_pml4[0] = sym_to_phys (vmm_pdp) | PDE_P_BIT | PDE_RW_BIT |
PDE_US_BIT;
}
static void
mm_init_global (void)
{
...
if (uefi_booted) {
create_vmm_pd ();
asm_wrcr3 (vmm_base_cr3);
}
...
map_hphys();
...
}
コードを見ると,以下のようなページテーブルが作成されることがわかります.
ちなみに,VMMSIZE_ALL
は(12810241024), PAGESIZE2M_SHIFT
は21なので,VMMSIZE_ALL >> PAGESIZE2M_SHIFT = 64
です.
PDPTE[3]は物理アドレス領域の動的マップに使用するための領域だと思います.
また,map_hphys()
で予め物理メモリ領域へアクセスするためのマッピングを作成しています.
メモリの静的・動的割り当てについては参考資料の「BitVisorの仮想メモリーマップ」が参考になります.
5. UEFI関数呼び出しのための切り替え(その2)
正式なBitVisorのアドレス空間に以降した後も,一部の処理でUEFIの関数を呼び出す場面があります(ゲストのトランポリンコード用のリアルアドレス領域のメモリ確保など).
このために利用されるのがcalluefi
です.
"UEFI関数呼び出しのための切り替え(その1)"のときは,Identity mapping上のページに対応するコードが存在していたため,オフセットの加算でよかったですが,bitvisor.elf全体はidentity mapping上には存在しないため,今回の場合は単純なオフセット計算ではページテーブルを切り替えることができません.
そこで,calluefi
では,fill_pagetable()
を利用して,UEFIファームウェア呼び出し前に,全てのページテーブルが,同じ物理アドレス領域を指す特殊なページテーブルを利用します.これを使って,RIPをcalluefi
を実行している物理アドレスに変更することでidentity mappingで動作可能な状況を作成し,UEFI関数を呼び出します.
BitVisorからのUEFIファームウェア呼び出しについては,参考資料の"BitVisorのUEFI対応"に図が書いてあります.
まとめ
UEFI関数呼び出しのための切り替え処理,おもしろいですね.
参考資料
- 榮樂 英樹, BitVisorのUEFI対応, BitVisor Summit 2, 2013.
- @hdk_2, BitVisor本体のブート仕様, BitVisor Advent Calendar 2015.
- @hdk_2, BitVisorの仮想メモリーマップ, BitVisor Advent Calendar 2015.
-
フォーラムをみると前途多難そう (https://forums.virtualbox.org/viewtopic.php?f=1&t=90831) ↩
-
これは,UEFIファームウェアの関数
AllocatePages()
でAllocateMaxAddressを指定することでおこないます. https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/boot/uefi-loader/loadvmm.c#lines-150 ↩