自作Hypervisorに挑戦 VMXONまで
参考資料
Intel SDM もっとも重要な1次情報源
Writing Hypervisor in Zig
ZigによるHypervisor作成の解説サイト。
今回やろうとしていることの大体上位互換になっている
mikanOS
mikanOSをもとにHypervisorへと改造していく
メモリ管理やGUIなどを利用させてもらう
開発環境
VM Workstation Pro上に構築した仮想マシン上でqemuを使って動作確認を行う
物理マシンのCPU : Intel
物理マシンのOS : Ubuntu24.0.4(WindowsだとNusted VMXを有効化できずエラー)
仮想マシンのOS : Ubuntu22.0.4
mikanOS
Hypervisor概要
そもそもhypervisorって何やっている
これまでのsoftware(OS含む) = guset softwareとは異なる権限で動き、guest softwareの動作を監視する
Intel仕様書ではVMMと記載されている
一部の命令をtrapして、VMM側で実行する
guest sfotwareに追加の仮想メモリ層を提供する
この機能を実現するためにはCPU側の支援機能が必要
Intel CPUではIntel VMXという名前でその機能が提供されている
今回はこのIntel VMXを有効にする(VMX Operation mode)に入れる命令VMXONを実行するところまで作成する
VMXON
の前に必要な確認事項
そもそもVMXをサポートしているか
CPUID.00H:EBX = "Genu"
CPUID.00H:EDX = "ineI"
CPUID.00H:ECX = "ntel"
結合して"GenuineIntel"になる
intel cpuであることの確認
CPUID.01H:ECX.VMX[5] = 1
VMX Operationをサポートしていること
VMXON自体を有効にする
ここの条件を見たいしていないとVMXONで #UDが発生
setting CR4.VMXE[bit 13] = 1.
以降は本bitはclearできなくなる
IA32_FEATURE_CONTROL MSR[bit 0] = 1
該当MSRのロックビット、後述の設定が変更できないように設定しておく必要がある
BIOS側の責任なので確認のみ
IA32_FEATURE_CONTROL MSR[bit 1]
SMX operationでもVMXONを有効にできる
SMX = TXT環境で専用の命令が使える環境
BIOS側の責任なので確認のみ
IA32_FEATURE_CONTROL MSR[bit 2]
SMX operation外でVMXONを有効にできる
BIOS側の責任なので確認のみ
VMXON成功の条件
- CR0, CR4の特定のbitが条件を満たすこと
VMXON後に該当bitをいじろうとすると #GP
具体的には以下のMSRで指定されている
MSRs IA32_VMX_CR0_FIXED0 and IA32_VMX_CR0_FIXED1がCR0の状態を指定する?
それぞれ、CR0の1にするべきbit, 0にするべきbitを表している
CR4についても同様である
-
CR0.PE, CR0.NE, CR0.PG = 1
初回限定
paged protect modeで動作することを意味する
ただし、上記のMSRで設定される可能性が高い -
VMXON Regionの設定
VMXONの際にはVMX用の領域(VMXON Region)を確保して、そのアドレスを引数として渡す必要がある
| offset | contents |
|---|---|
| 0 | revisions |
| 4 | VMX abort Indicator |
| 8 | data |
細かい各領域はともかくrevisionsなどはこの段階で設定する必要がある
また、VMXONのサイズ、セットするべきrevisionsなどはIA32_VMX_BASIC(Appendix A1)に記載されており
bit 0-30 : このprocessorのvmcs revisions
bit 32-44 : vmcs regionのsize.
13 bitで0x1000 alignなので0x1000, 0x2000, 0x3000しかありえない?
基本的には0x1000になる
となっている
VMXONの成功確認
VMX命令はその実行結果をRFLAGS.CF, RFLAGS.ZFに格納します。
| 状態 | CF | ZF | 意味 |
|---|---|---|---|
| 成功 | 0 | 0 | VMX 命令成功 |
| VMfailInvalid | 1 | 0 | 無効な状態(前提違反) |
| VMfailValid | 0 | 1 | 有効な失敗(エラー番号あり) |
VMfailInvalid(CF=1)は必要な前提を満たせていないため発生し、そもそもの前提を満たせていないことからエラー情報を出せません
VMfailValid(ZF=1)は前提条件は満たしていることから、エラー番号をVMCSに格納してくれます。
実装
terminalにHypervisor用の処理を実行するための"hyper"コマンドを追加する
mikanOSのterminalはCPL=0で動いているのでsyscallなどを経由しなくても問題ない
// ...
} else if (strcmp(command, "hyper") == 0) {
HypervisorMain();
} else if (command[0] != 0) {
// ...
HypervisorMain()でやること
基本的に上述した必要条件や設定項目を実施しているだけ。
void HypervisorMain() {
VMX_REGIONS* VmxonRegion;
SetLogLevel(kInfo);
if (!IsIntelCpu()) {
Log(kError, "Not Intel CPU\n");
return;
}
if (!IsVmxSupported()) {
Log(kError, "Not VMX Supported\n");
return;
}
if (!IsVmxEnabled()) {
Log(kError, "Not VMX Enabled\n");
return;
}
ConfigureCr0();
ConfigureCr4();
VmxonRegion = ConfigureVmxonRegion();
if (VmxonRegion == nullptr) {
Log(kError, "Not Vmxon Region\n");
return;
}
VmxonResult result = vmxon((uint64_t)VmxonRegion);
if ((result.zf != 0) || (result.cf != 0)) {
Log(kError, "VMXON Fail zf = %02X, cf = %02X\n", result.zf, result.cf);
}
return;
}
今回はIntel CPUのものを対象にしているのでIntel CPUであることの確認
bool IsIntelCpu(){
uint32_t eax,ebx,ecx,edx;
// https://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/Processor_Identification_071405_i.pdf
cpuid(0, 0, &eax, &ebx, &ecx, &edx);
if (ebx != 0x756e6547) { // Genu
return false;
}
if (edx != 0x49656e69) { // ineI
return false;
}
if (ecx != 0x6c65746e) { // ntel
return false;
}
return true;
}
CPUID.01H:ECX.VMX[5] = 1
bool IsVmxSupported() {
uint32_t eax,ebx,ecx,edx;
cpuid(1, 0, &eax, &ebx, &ecx, &edx);
return (ecx & VMX_BIT) == VMX_BIT;
}
VMXを有効にする設定の確認
bool IsVmxEnabled() {
uint64_t msr = rdmsr(MSR_IA32_FEAT_CTL);
Log(kInfo, "MSR_IA32_FEAT_CTL %08X%08X\n", msr>>32, msr);
if ((msr & FEAT_CTL_LOCKED) != FEAT_CTL_LOCKED) {
return false;
}
if ((msr & FEAT_CTL_VMX_ENABLED_OUTSIDE_SMX) != FEAT_CTL_VMX_ENABLED_OUTSIDE_SMX) {
return false;
}
return true;
}
CR0/CR4の設定
void ConfigureCr0() {
uint64_t cr0 = read_cr0();
uint64_t MsrCr0Fixed0 = rdmsr(MSR_IA32_VMX_CR0_FIXED0);
uint64_t MsrCr0Fixed1 = rdmsr(MSR_IA32_VMX_CR0_FIXED1);
cr0 = (cr0 | MsrCr0Fixed0) & MsrCr0Fixed1;
Log(kInfo, "cr0 %08X%08X\n", cr0>>32, cr0);
write_cr0(cr0);
}
void ConfigureCr4() {
uint64_t cr4 = read_cr4();
uint64_t MsrCr4Fixed0 = rdmsr(MSR_IA32_VMX_CR4_FIXED0);
uint64_t MsrCr4Fixed1 = rdmsr(MSR_IA32_VMX_CR4_FIXED1);
cr4 = (cr4 | MsrCr4Fixed0) & MsrCr4Fixed1;
cr4 |= CR4_VMXE;
Log(kInfo, "cr4 %08X%08X\n", cr4>>32, cr4);
write_cr4(cr4);
}
VMXON Regionの作成
VMX_REGIONS* ConfigureVmxonRegion() {
uint64_t MsrVmxBasic = rdmsr(MSR_IA32_VMX_BASIC);
Log(kInfo, "MsrVmxBasic %08X%08X\n", MsrVmxBasic>>32, MsrVmxBasic);
uint32_t revisions = MsrVmxBasic & 0xFFFFFFFF;
Log(kInfo, "revisions %08X\n", revisions);
uint32_t VmcsSize = (MsrVmxBasic & BITS(44, 32)) >> 32;
Log(kInfo, "VmcsSize %08X\n", VmcsSize);
size_t num_frames = (VmcsSize+0xfff)/0x1000;
VMX_REGIONS* VmxonRegion;
// num_frames = total size / page size
auto frame = memory_manager->Allocate(num_frames);
if (frame.error) {
Log(kError, "malloc error\n");
return nullptr;
}
VmxonRegion = reinterpret_cast<VMX_REGIONS*>(frame.value.Frame());
memset(VmxonRegion, 0, num_frames * 0x1000);
VmxonRegion->revisions = revisions;
return VmxonRegion;
}
動作確認
mikanOS上でhyperコマンドを実行した様子
エラーログは表示されず、各設定値も正常そうである

VmxonRegionを設定しなかった場合
// VmxonRegion->revisions = revisions;

Discussion