🎉

自作Hypervisorに挑戦 VMXONまで

に公開

参考資料

Intel SDM
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
もっとも重要な1次情報源

Writing Hypervisor in Zig
https://hv.smallkirby.com/intro
ZigによるHypervisor作成の解説サイト。
今回やろうとしていることの大体上位互換になっている

mikanOS
https://github.com/uchan-nos/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であることの確認
https://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/Processor_Identification_071405_i.pdf

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などを経由しなくても問題ない

terminal.cpp
// ...
  } else if (strcmp(command, "hyper") == 0) {
    HypervisorMain();
  } else if (command[0] != 0) {
// ...

HypervisorMain()でやること
基本的に上述した必要条件や設定項目を実施しているだけ。

hypervisor.cpp
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であることの確認

hypervisor.cpp
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

hypervisor.cpp
bool IsVmxSupported() {
    uint32_t eax,ebx,ecx,edx;

    cpuid(1, 0, &eax, &ebx, &ecx, &edx);

    return (ecx & VMX_BIT) == VMX_BIT;
}

VMXを有効にする設定の確認

hypervisor.cpp
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の設定

hypervisor.cpp
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の作成

hypervisor.cpp
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