Windows Hypervisor Platform (WHP) の使い方

（これは [東京大学 品川研究室 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/shina-lab) の3日目の記事です）

# 1. はじめに

Windows 上で Intel VT や AMD-V などのハードウェア仮想化支援機能を利用するための OS 標準の API として [Windows Hypervisor Platform (WHP)](https://docs.microsoft.com/virtualization/api/) があります。

この OS 標準 API を使うとわりと簡単に仮想マシン（ Virtual Machine (VM) ）や仮想マシンモニタ（ Virtual Machine Monitor (VMM) ）を作ることが出来ます。

しかし、[WHP の API のドキュメント](https://docs.microsoft.com/ja-jp/virtualization/api/hypervisor-platform/hypervisor-platform)だけでは、最初にどうやって動かせば良いかが分かりにくいと思います。

そこで、とりあえず VM を1つ作って、VM entry してから、すぐに VM exit してホストに戻ってくるだけの簡単なサンプルコードを作ってみました。

# 2. WHP の準備

まずは WHP を有効化します。管理者権限で Windows PowerShell を起動して、以下のコマンドを入力します。

```powershell
Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform
```

「Windows の機能の有効化または無効化」で「Windows ハイパーバイザー プラットフォーム」にチェックを入れても良いです。

また、最新の Windows SDK を以下の URL からダウンロードしてインストールして下さい。

https://developer.microsoft.com/windows/downloads/windows-10-sdk/


# 3. 全体の流れ

WHP における VM 作成の主な流れは以下のようになります。

1. WHP がサポートされているかどうか調べる（ WHvGetCapability() ）
2. VM を作成する（ WHvCreatePartition(), WHvSetPartitionProperty(), WHvSetupPartition() ）
3. VM の物理アドレス空間を設定する（ WHvMapGpaRange() ）
4. VM 内に仮想CPU（vCPU）を作成する（ WHvCreateVirtualProcessor() ）
5. vCPU のレジスタを設定する（ WHvSetVirtualProcessorRegisters() ）
6. VM 内の vCPU を実行する（ WHvRunVirtualProcessor() ）
7. Exit Reason を調べて処理を続行する

# 4. 実際のコード

それでは、実際のコードを見ていきます。

## 4.0. 準備

Windows の関数がエラーを返したら即時終了するための簡単なマクロ `OR` と `panic()` を定義します。ちょっと変わった書き方ですが、後で出てくるコードが見やすくなります。

```c
#define OR >=0?(void)0:
#define panic(x) (void)(fputs("panic: " x "\n",stderr),exit(1))
```

また VM を管理するための `WHV_PARTITION_HANDLE` 型のハンドル `handle` と、vCPU の番号を格納するための変数 `vcpu` を定義します。

```c
	WHV_PARTITION_HANDLE handle;
	UINT16 vcpu = 0;
```


## 4.1. WHP がサポートされているかどうか調べる

`WHvGetCapability()` という関数で `WHvCapabilityCodeHypervisorPresent` というケーパビリティコードを指定して問い合わせると、`WHV_CAPABILITY` 型の構造体に値が返ってきます。この構造体の `.HypervisorPresent` というフラグが立っていれば WHP が使用可能です。

```c
	// Is Windows Hypervisor Platform (WHP) enabled?
	UINT32 size;
	WHV_CAPABILITY capability;
	WHvGetCapability(
		WHvCapabilityCodeHypervisorPresent,
		&capability, sizeof(capability), &size);
	if (!capability.HypervisorPresent)
		panic("Windows Hypervisor Platform is not enabled");
```

## 4.2. VM を作成する

まず、`WHvCreatePartition()` で VM を作成します。WHP では VM のことを Partition と呼んでいます。`handle` のポインタを渡して VM のハンドルを受け取ります。

```c
	// create a VM
	WHvCreatePartition(&handle)
		OR panic("create partition");
```

次に、`WHvSetPartitionProperty()` で VM の属性を設定します。少なくとも `WHvPartitionPropertyCodeProcessorCount` という属性コードを使って、VM で使う vCPU の数を設定する必要があります。

```c
	UINT32 cpu_count = 1;
	WHvSetPartitionProperty(
		handle,
		WHvPartitionPropertyCodeProcessorCount,
		&cpu_count, sizeof(cpu_count))
		OR panic("set partition property (cpu count)");

```

また、後で使うので、`WHvPartitionPropertyCodeExtendedVmExits` という属性コードで、`WHV_EXTENDED_VM_EXITS` 型の構造体の `.HypercallExit` を 1 に設定して、ハイパーコール命令（vmcall, vmmcall命令）が実行されたときに専用のエラーコードが返るようにします。

```c
	WHV_EXTENDED_VM_EXITS vmexits = { 0 };
	vmexits.HypercallExit = 1;
	WHvSetPartitionProperty(
		handle,
		WHvPartitionPropertyCodeExtendedVmExits,
		&vmexits, sizeof(vmexits))
		OR panic("set partition property (vmexits)");
```

最後に、`WHvSetupPartition()` で実際に VM を作成して使える状態にします。

```c
	WHvSetupPartition(handle)
		OR panic("setup partition");
```


## 4.3. VM の物理アドレス空間を設定する

VM の中の物理アドレス空間を設定します。カーネル用のデータ構造を置く領域と、ユーザ空間のコードを置くための領域を確保して設定します。

カーネル用データ構造は、実際にはページテーブルだけです。ここではラージページを使ってゲスト仮想アドレス空間の最初の 1 GiB だけをストレートマッピング（仮想アドレス＝物理アドレス）するので、PML4 と PDPT だけを用意します。ページテーブルは、とりあえずゲスト物理アドレス空間の 1 GiB から始まる領域に置くことにします。

```c
const UINT64 user_start = 4 KiB;
const UINT64 kernel_start = 1 GiB;
struct kernel {
	UINT64 pml4[512];
	UINT64 pdpt[512];
} *kernel;

```

まずは、ホストの Windows 側でページテーブル用のメモリを確保して初期化します。4 KiB アラインされたメモリ領域を `aligned_alloc()` で確保して、`memset()` でゼロフィルします。次に、pml4 の最初のエントリに pdpt のゲスト物理アドレス（1 GiB ＋ struct kernel での pdpt のオフセット）とフラグ（Present, Writable, User）を設定します。また、pdpt の最初のエントリに物理アドレス 0 番地とフラグ（Present, Writable, User, 1 GiB page）を設定します。 最後に、`WHvMapGpaRange()` でカーネルのメモリ領域をゲスト物理アドレス空間に読み書き可能（`WHvMapGpaRangeFlagRead | WHvMapGpaRangeFlagWrite`）でマッピングします。

ページテーブルの構造については、Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3A: System Programming Guide, Part 1 を参照して下さい。
https://software.intel.com/content/www/us/en/develop/download/intel-64-and-ia-32-architectures-sdm-volume-3a-system-programming-guide-part-1.html

```c
	// prepare and map kernel data structures
	assert((sizeof(*kernel) & (4 KiB - 1)) == 0); // 4 KiB align
	kernel = (struct kernel *)aligned_alloc(4 KiB, sizeof(*kernel));
	if (!kernel)
		panic("aligned_alloc");
	memset(kernel, 0, sizeof(*kernel));
	kernel->pml4[0] = (kernel_start + offsetof(struct kernel, pdpt))
		| (PTE_P | PTE_RW | PTE_US);
	kernel->pdpt[0] = 0x0
		| (PTE_P | PTE_RW | PTE_US | PTE_PS);
	WHvMapGpaRange(handle, kernel, kernel_start, sizeof(*kernel),
		       WHvMapGpaRangeFlagRead | WHvMapGpaRangeFlagWrite)
		OR panic("map the kernel region");
```

この領域は、ゲスト物理アドレス空間の 1GiB から始まる領域に存在しますが、ゲストのページテーブルでマッピングしていないので、ゲスト側のコードからはアクセスできません。ページテーブルの書き込みはホスト側で可能ですし、ページテーブルの物理アドレスを CPU が認識できれば良いので、このようになっています。

ユーザ空間のコードは、ゲスト物理アドレスの 4 KiB から始まる 1 ページ分だけを用意することにします。

まず、ユーザ空間のコードを置くためのページを `aligned_alloc()` で1ページ分確保して、あらかじめ定義した vmcall のオペコード（`user_code[]`）をコピーしておきます。Intel CPU では `vmcall (0x0f, 0x01, 0xc1)` ですが、AMD CPU では `vmmcall (0x0f, 0x01, 0xd9)` に変更して下さい。最後に、`WHvMapGpaRange()` でゲスト物理アドレス空間に読み込み及び実行可能（`WHvMapGpaRangeFlagRead | WHvMapGpaRangeFlagExecute`）でマッピングします。

```c
	// map user space
	void *user_page = aligned_alloc(4 KiB, 4 KiB);
	if (!user_page)
		panic("aligned_alloc");
	const UINT8 user_code[] = {
		0x0f, 0x01, 0xc1, // vmcall
	};
	memcpy(user_page, user_code, sizeof(user_code));
	WHvMapGpaRange(handle, user_page, user_start, 4 KiB,
		       WHvMapGpaRangeFlagRead | WHvMapGpaRangeFlagExecute)
		OR panic("map the user region");

```


## 4.4. VM 内に仮想CPU（vCPU）を作成する

vCPU の作成は、`WHvCreateVirtualProcessor()` を呼ぶだけです。2番目の引数で vCPU の番号を指定します。

```c
	// create a vCPU
	WHvCreateVirtualProcessor(handle, vcpu, 0)
		OR panic("create virtual processor");
```


## 4.5. vCPU のレジスタを設定する

vCPU のレジスタの初期値を設定します。

ここで設定するのは、CR0, CR3, CR4, EFER の各コントロールレジスタと、CS, SS, DS, ES の各セグメントレジスタ、RIP のプログラムカウンタレジスタです。

設定するレジスタは、`WHV_REGISTER_NAME` 型の配列を使って複数同時に指定することが出来ます。各レジスタに設定する値は `WHV_REGISTER_VALUE` 型の同じ長さの配列を使って指定します。対応関係がずれないように、enum を使って名前とインデックスを一致させています。

CR0 には Protection Enable (PE) ビットと Paging Enable (PG) ビットを設定します。
CR3 にはページテーブル（pml4）の物理アドレスを設定します。
CR4 には Page Size Extensions (PSE) と Physical Address Extension (PAE) を指定します。
EFER には IA-32e Mode Enable (LME) と IA-32e Mode Active (LMA) を指定して、いきなり 64 ビットのロングモードで実行を開始させます。
CS, SS, DS, ES の各セグメントレジスタには、適切な値を設定しておきます。

最後に、`WHvSetVirtualProcessorRegisters()` でレジスタの値を設定します。

```c
	// setup vCPU registers
	enum {
		Cr0, Cr3, Cr4, Efer,
		Cs, Ss, Ds, Es, Rip,
		RegNum
	};
	WHV_REGISTER_NAME regname[RegNum];
	regname[Cr0] =  WHvX64RegisterCr0;
	regname[Cr3] =  WHvX64RegisterCr3;
	regname[Cr4] =  WHvX64RegisterCr4;
	regname[Efer] = WHvX64RegisterEfer;
	regname[Cs] =   WHvX64RegisterCs;
	regname[Ss] =   WHvX64RegisterSs;
	regname[Ds] =   WHvX64RegisterDs;
	regname[Es] =   WHvX64RegisterEs;
	regname[Rip] =   WHvX64RegisterRip;
	WHV_REGISTER_VALUE regvalue[RegNum];
	regvalue[Cr0].Reg64 = (CR0_PE | CR0_PG);
	regvalue[Cr3].Reg64 = kernel_start + offsetof(struct kernel, pml4);
	regvalue[Cr4].Reg64 = (CR4_PSE | CR4_PAE);
	regvalue[Efer].Reg64 = (EFER_LME | EFER_LMA);
	WHV_X64_SEGMENT_REGISTER CodeSegment;
	CodeSegment.Base = 0;
	CodeSegment.Limit = 0xffff;
	CodeSegment.Selector = 0x08;
	CodeSegment.Attributes = 0xa0fb;
	regvalue[Cs].Segment = CodeSegment;
	WHV_X64_SEGMENT_REGISTER DataSegment;
	DataSegment.Base = 0;
	DataSegment.Limit = 0xffff;
	DataSegment.Selector = 0x10;
	DataSegment.Attributes = 0xc0f3;
	regvalue[Ss].Segment = DataSegment;
	regvalue[Ds].Segment = DataSegment;
	regvalue[Es].Segment = DataSegment;
	regvalue[Rip].Reg64 = user_start;
	WHvSetVirtualProcessorRegisters(
		handle, vcpu, regname, RegNum, regvalue)
		OR panic("set virtual processor registers");
```

なお、この例では割り込み禁止で VM entry した後すぐに戻ってくるので、IDTR, TR などのレジスタは設定していません。また、セグメントレジスタも設定済みなので、GDTR も設定していません。


## 4.6. VM 内の vCPU を実行する

`WHvRunVirtualProcessor()` で VM 内の vCPU の実行を開始させます。

```c
	// run the VM
	WHV_RUN_VP_EXIT_CONTEXT context;
	WHvRunVirtualProcessor(handle, vcpu, &context, sizeof(context))
		OR panic("run virtual processor");
```

## 4.7. Exit Reason を調べて処理を続行する

`WHvRunVirtualProcessor()` に渡した `WHV_RUN_VP_EXIT_CONTEXT` 型の変数に exit reason が返ってくるので、その値に基づいて適切な処理を実行します。

ここでは、ユーザ空間で `vmcall` 命令が実行されたことを確認するために、`ExitReason` が `WHvRunVpExitReasonHypercall` かどうか確認しています。

```c
	printf("Exit reason: %x\n", context.ExitReason);
	if (context.ExitReason == WHvRunVpExitReasonHypercall)
		puts("The vmcall instruction is executed");
```

以上で、とりあえず VM entry と VM exit をすることができます。

# 5. 実行結果

実際の実行結果は以下のようになります。
![WHP-simple.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104560/f626f325-cb97-e036-f0b7-77bc45c84696.png)
ハイパーコールによる VM exit の際は、WHvRunVpExitReasonHypercall (0x1005) が返ることになっているので、実際に vmcall 命令が実行されたことを確認できます。

ちなみに、vmcall の代わりに無限ループ（0xeb 0xfe）を実行すると、強制終了するまで実行し続けます。

# 6. まとめ

Windows Hypervisor Platform (WHP) の簡単なサンプルコードを作って解説しました。

実際には、VM や vCPU をもっと細かく設定してから、ループ内で`WHvRunVirtualProcessor()` を呼び出して、exit reason に応じた処理を行うという形になると思います。

# 7. コード

GitHub にコードを置いてあります。

https://github.com/utshina/WHP-simple