BitVisorのUEFIブート時におけるページテーブル切り替え処理

先日[VirtualBox 6.0](https://www.virtualbox.org/)がリリースされました．[リリースノート](https://www.virtualbox.org/wiki/Changelog-6.0)には書いてありませんが，ついにNested Virtualizationがサポートされたという噂を耳にしたので，今回はVirtualBox上でBitVisorが動作するのか検証しよう..と思ってましたが，よくよく調べてみると現時点では[AMDのCPU（の一部）をサポート](https://www.virtualbox.org/manual/ch10.html#nested-virt)ということらしく，あいにく手持ちのAMD CPUが無いので断念しました．AMDなCPUお持ちの方，試してみてはいかがでしょうか[^1]．

[^1]: フォーラムをみると前途多難そう (https://forums.virtualbox.org/viewtopic.php?f=1&t=90831)

ということで別の話ということで，BitVisorのUEFIブート時（BIOSブート時もですが）には何回かページテーブルを切り替える処理が存在します．今回はその話です．

## 1. ブート用ページテーブルへの切り替え
UEFIのファームウェアは64bit環境であればlong modeで開始されます．このときページテーブルは仮想アドレス=物理アドレスとなるIdentity mappingが設定されています．


`loadvmm.efi`から`entry.s`のエントリポイントにジャンプした際，以下のようなページテーブルに切り替えます．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153099/5157ec74-9965-6204-8e85-06b084498955.png)

このためのページテーブルが[`entry_pml4`,`entry_pdp`,`entry_pd0`,`entry_pd`](https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/core/entry.s#lines-832)です．

BitVisorのコードは0x40100000からのアドレスに配置されるので（[リンカスクリプト](https://bitbucket.org/bitvisor/bitvisor/src/default/bitvisor.lds))，そのためのページテーブルを作成しています．このとき2MBページングを使って，一つのPDPTEエントリで0x40000000-0x7FFFFFFFの1GBの領域をカバーするようにしています．

PTPTE[0]のIdentity mappingはマッピング切り替え用に存在します．
x86でのページテーブルの切り替えは`mov cr3`命令を使うだけですが，問題となるのは`mov cr3`を実行した直後から仮想アドレスと物理アドレスのマッピングが変わるため，次の命令を実行した際にページフォールトにならないように適切にマッピングを作成しておく必要があるという点です．
UEFIファームウェアはIdentity Mappingで動作しており，さらにloadvmm.efiはBitVisorを物理アドレス1GB以下の領域に配置するので[^2]，このPDPTE[0]のエントリを作成しておけば，このページテーブルをロードしてもページテーブルが発生することはありません．

[^2]: これは，UEFIファームウェアの関数`AllocatePages()`でAllocateMaxAddressを指定することでおこないます． https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/boot/uefi-loader/loadvmm.c#lines-150

実際には以下のように絶対アドレスジャンプを利用して，Identity MappingからBitVisorのアドレス空間へ切り替えます．

```nasm

    mov %rax, %cr3
    mov $start_stack, %rsp
    mov $1f, %rax
    jmp *%rax        // Change RIP 
1:                   // Use 0x40000000- address
```

ページテーブル作成処理は[このあたり](https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/core/entry.s#lines-310)です．

ちなみに，UEFIファームウェアのページテーブルから独自のページテーブルへ切り替えた際は，割り込み無効化状態で動作させます．これはUEFIの仕様に基づくものです．

## 2. UEFI関数呼び出しのための切り替え (その1)
無事ページテーブルが切り替えられたら，その後はuefi.cの[エントリポイント](https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/core/uefi.c#lines-174c)へ飛んでいきますが，ここで何回かUEFIファームウェアの関数を呼び出しています．
このとき，ページテーブルを元のUEFIファームウェアが使っていたidentity mappingなページテーブルに戻す必要があります．

このための関数が[`uefi_entry_call`](https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/core/entry.s#lines-395)です．

```nasm
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`](https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/core/entry.s#lines-418)も同様のページテーブル切り替え処理をおこなっています．

[^2]: ちなみに，0x100000は`uefi_entry_physoff`格納直前でおこなっているページテーブルの修正のために必要です．


ちなみに， `uefi_entry_call`は以下のように呼ばれています．

```c
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`](https://bitbucket.org/bitvisor/bitvisor/src/f8d6f8f0751cd759f3c2f1b6f4c75b9180444c50/core/entry.s#lines-447)に飛びます．このあと`vmm_main()`を呼ぶのですが，その前に，ロードしたbitvisor.elfを適切にマップする必要があります．`uefi_entry_start`の先頭でこのためのページテーブルの修正をしています．

```nasm

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で関連するところを抜粋すると以下のようになります．

```c
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`は(128*1024*1024), `PAGESIZE2M_SHIFT`は21なので，`VMMSIZE_ALL >> PAGESIZE2M_SHIFT = 64`です．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153099/cbe1f6a3-baed-d317-036c-37de2039fb5b.png)

PDPTE[3]は物理アドレス領域の動的マップに使用するための領域だと思います．
また，`map_hphys()`で予め物理メモリ領域へアクセスするためのマッピングを作成しています．

メモリの静的・動的割り当てについては参考資料の「BitVisorの仮想メモリーマップ」が参考になります．

## 5. UEFI関数呼び出しのための切り替え（その2）
正式なBitVisorのアドレス空間に以降した後も，一部の処理でUEFIの関数を呼び出す場面があります（ゲストのトランポリンコード用のリアルアドレス領域のメモリ確保など）．

このために利用されるのが`calluefi`です．
"UEFI関数呼び出しのための切り替え（その1）"のときは，Identity mapping上のページに対応するコードが存在していたため，オフセットの加算でよかったですが，bitvisor.elf全体はidentity mapping上には存在しないため，今回の場合は単純なオフセット計算ではページテーブルを切り替えることができません．
そこで，`calluefi`では，[`fill_pagetable()`](https://bitbucket.org/bitvisor/bitvisor/src/124a31eea07d4e4ea340e974e5183775a6546c6b/core/calluefi.c#lines-261)を利用して，UEFIファームウェア呼び出し前に，全てのページテーブルが，同じ物理アドレス領域を指す特殊なページテーブルを利用します．これを使って，RIPを`calluefi`を実行している物理アドレスに変更することでidentity mappingで動作可能な状況を作成し，UEFI関数を呼び出します．

BitVisorからのUEFIファームウェア呼び出しについては，参考資料の"BitVisorのUEFI対応"に図が書いてあります．

## まとめ
UEFI関数呼び出しのための切り替え処理，おもしろいですね．

## 参考資料
- 榮樂 英樹, [BitVisorのUEFI対応](https://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.
- @hdk_2, [BitVisorの仮想メモリーマップ](https://qiita.com/hdk_2/items/6c7aaa72f5dcfcfda342), BitVisor Advent Calendar 2015.