4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

BitVisorAdvent Calendar 2018

Day 21

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

Posted at

先日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のエントリポイントにジャンプした際,以下のようなページテーブルに切り替えます.

image.png

このためのページテーブルが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です.

image.png

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関数呼び出しのための切り替え処理,おもしろいですね.

参考資料

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

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

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?