唐凌 发表于 2021-7-25 16:53:30

【虚拟化】使用WHP实现在64位Windows 10中运行DOS的Hello World!


# 前言
WHP,即Windows Hypervisor Platform,是微软在Windows 10 x64 1803之后的引入的一套API,其作用是让第三方的虚拟机软件能使用它的API。但有一说一,本人使用Windows 10 x64 LTSC 2019(也就是1809版本的Windows 10)的WHP的功能有些拉胯,可以说是几乎只有基本功能。以此可以推测VMware Workstation要求2004版本及以上的Windows 10是为什么了。很明显,VMware Workstation要求WHP在Windows 10 x64 2004上的新功能。
此外,WHP仅支持x64版本的Windows 10。因此用Visual Studio开发的时候可以直接删除Win32配置,只保留x64配置。其实也好理解,x86的保护模式虽然也可以使用Intel VT-x/AMD-V,也可以运行长模式的Guest,但是不仅无法保存64位寄存器的高32位,还无法保存r8-r15这8个寄存器。换言之就是除非Host切换到长模式,否则Host顶多只能运行一个长模式的Guest,这必然是无法接受的。

# 查询WHP功能
使用WHP写一个虚拟机软件的时候需要先用`WHvGetCapability`函数查询当前系统的WHP支持一些什么功能,首要就是要查询系统里有没有支持WHP功能的Hypervisor,否则就别玩了。确定所有需要的功能都支持后,才能开始创建虚拟机运行之。
`WHvGetCapability`的函数原型如下:
```C
HRESULT
WINAPI
WHvGetCapability(
    _In_ WHV_CAPABILITY_CODE CapabilityCode,
    _Out_writes_bytes_to_(CapabilityBufferSizeInBytes, *WrittenSizeInBytes) VOID* CapabilityBuffer,
    _In_ UINT32 CapabilityBufferSizeInBytes,
    _Out_opt_ UINT32 *WrittenSizeInBytes
    );
```
其中`WHV_CAPABILITY_CODE`的定义如下:
```C
typedef enum WHV_CAPABILITY_CODE
{
    // Capabilities of the API implementation
    WHvCapabilityCodeHypervisorPresent      = 0x00000000,
    WHvCapabilityCodeFeatures               = 0x00000001,
    WHvCapabilityCodeExtendedVmExits      = 0x00000002,

    // Capabilities of the system's processor
    WHvCapabilityCodeProcessorVendor      = 0x00001000,
    WHvCapabilityCodeProcessorFeatures      = 0x00001001,
    WHvCapabilityCodeProcessorClFlushSize   = 0x00001002,
    WHvCapabilityCodeProcessorXsaveFeatures = 0x00001003,
} WHV_CAPABILITY_CODE;
```
那么第一个参数输入`WHvCapabilityCodeHypervisorPresent`就可以查询系统里到底有没有支持WHP功能的Hypervisor。
如果返回了`FALSE`就意味着你没有安装WHP,请去控制面板里安装了WHP后再运行程序。

其他内容对于本文所设计的内容无关,故不在本文讨论。

# 初始化虚拟机
初始化虚拟机的步骤比较多。

## 创建虚拟机
使用WHP初始化虚拟机需要先用`WHvCreatePartition`创建一个虚拟机,其函数原型如下:
```C
typedef VOID* WHV_PARTITION_HANDLE;

HRESULT
WINAPI
WHvCreatePartition(
    _Out_ WHV_PARTITION_HANDLE* Partition
    );
```
返回的是一个虚拟机句柄。

## 初始化虚拟机属性
创建完成后,需要用`WHvSetPartitionProperty`函数设置虚拟机属性,其函数原型如下:
```C
HRESULT
WINAPI
WHvSetPartitionProperty(
    _In_ WHV_PARTITION_HANDLE Partition,
    _In_ WHV_PARTITION_PROPERTY_CODE PropertyCode,
    _In_reads_bytes_(PropertyBufferSizeInBytes) const VOID* PropertyBuffer,
    _In_ UINT32 PropertyBufferSizeInBytes
    );
```
其中`WHV_PARITION_PROPERTY_CODE`的定义如下:
```C
typedef enum {
    WHvPartitionPropertyCodeExtendedVmExits      = 0x00000001,
    WHvPartitionPropertyCodeExceptionExitBitmap   = 0x00000002,
    WHvPartitionPropertyCodeSeparateSecurityDomain= 0x00000003,

    WHvPartitionPropertyCodeProcessorFeatures      = 0x00001001,
    WHVPartitionPropertyCodeProcessorClFlushSize   = 0x00001002,
    WHvPartitionPropertyCodeCpuidExitList         = 0x00001003,
    WHvPartitionPropertyCodeCpuidResultList         = 0x00001004,
    WHvPartitionPropertyCodeLocalApicEmulationMode= 0x00001005,
    WHvPartitionPropertyCodeProcessorXsaveFeatures= 0x00001006,

    WHvPartitionPropertyCodeProcessorCount         = 0x00001fff
} WHV_PARTITION_PROPERTY_CODE;
```
本文创建的虚拟机是为了运行一个DOS程序,因此只需要设置`WHvPartitionPropertyCodeProcessorCount`为1即可,也就是虚拟机里有一个vCPU。

## 注册虚拟机到Hypervisor
设置完成后,使用`WHvSetupPartition`函数把这个虚拟机注册到Hypervisor中,其函数原型如下:
```C
HRESULT
WINAPI
WHvSetupPartition(
    _In_ WHV_PARTITION_HANDLE Partition
    );
```

## 内存虚拟化
使用硬件虚拟化实现虚拟机需要让内存按页对齐,因此最好用(https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc)这样的函数来分配内存。
除此之外,还要设置内存映射。不过WHP的映射要求你输入的GPA和HVA,而不是HPA。设置映射需要使用`WHvMapGpaRange`函数,其函数原型如下:
```C
// Guest physical or virtual address
typedef UINT64 WHV_GUEST_PHYSICAL_ADDRESS;
typedef UINT64 WHV_GUEST_VIRTUAL_ADDRESS;


// Flags used by WHvMapGpaRange
typedef enum WHV_MAP_GPA_RANGE_FLAGS
{
    WHvMapGpaRangeFlagNone            = 0x00000000,
    WHvMapGpaRangeFlagRead            = 0x00000001,
    WHvMapGpaRangeFlagWrite             = 0x00000002,
    WHvMapGpaRangeFlagExecute         = 0x00000004,
    WHvMapGpaRangeFlagTrackDirtyPages   = 0x00000008,
} WHV_MAP_GPA_RANGE_FLAGS;

DEFINE_ENUM_FLAG_OPERATORS(WHV_MAP_GPA_RANGE_FLAGS);

HRESULT
WINAPI
WHvMapGpaRange(
    _In_ WHV_PARTITION_HANDLE Partition,
    _In_ VOID* SourceAddress,
    _In_ WHV_GUEST_PHYSICAL_ADDRESS GuestAddress,
    _In_ UINT64 SizeInBytes,
    _In_ WHV_MAP_GPA_RANGE_FLAGS Flags
    );
```

## 创建vCPU
这没啥好说的,没有vCPU那虚拟机拿什么运行。创建vCPU用`WHvCreateVirtualProcessor`函数,原型如下:
```C
HRESULT
WINAPI
WHvCreateVirtualProcessor(
    _In_ WHV_PARTITION_HANDLE Partition,
    _In_ UINT32 VpIndex,
    _In_ UINT32 Flags
    );
```
注意`VpIndex`从0开始。另外`Flags`参数是一个保留的参数,直接填0即可。

## 初始化vCPU
主要是要初始化vCPU的寄存器,让vCPU能运行起来。需要初始化的寄存器有:
- 通用寄存器(General-Purpose Registers),需要重点设置`rip`,`rsp`,`rflags`三个寄存器。
- 段寄存器(Segment Registers),全部需要特别设置。
- 控制寄存器(Control Registers),主要设置`cr0`寄存器。
- 扩展控制寄存器(Extended Control Registers),主要设置`xcr0`寄存器以启用x87 FPU。x86处理器要求不得禁用x87。
- 中断描述符与全局描述符(Interrupt/Global Descriptor Table),主要是设置地址和大小。
- 调试寄存器(Debug Registers),主要设置`dr6`和`dr7`寄存器。
- 浮点数协处理器控制与状态寄存器(Floating-Point Coprocessor Control and Status Register)。
设置`vCPU`的寄存器用`WHvSetVirtualProcessorRegisters`函数,原型如下:
```C
HRESULT
WINAPI
WHvSetVirtualProcessorRegisters(
    _In_ WHV_PARTITION_HANDLE Partition,
    _In_ UINT32 VpIndex,
    _In_reads_(RegisterCount) const WHV_REGISTER_NAME* RegisterNames,
    _In_ UINT32 RegisterCount,
    _In_reads_(RegisterCount) const WHV_REGISTER_VALUE* RegisterValues
    );
```
这个函数支持批量设置寄存器。不过一次性的批量设置并不是很好用,主要是因为不能优雅地把所有寄存器定义到一块去。我的做法是分类批量设置。
其中`WHV_REGISTER_VALUE`是个128位的联合体,定义如下:
```C
typedef union WHV_REGISTER_VALUE
{
    WHV_UINT128 Reg128;
    UINT64 Reg64;
    UINT32 Reg32;
    UINT16 Reg16;
    UINT8 Reg8;
    WHV_X64_FP_REGISTER Fp;
    WHV_X64_FP_CONTROL_STATUS_REGISTER FpControlStatus;
    WHV_X64_XMM_CONTROL_STATUS_REGISTER XmmControlStatus;
    WHV_X64_SEGMENT_REGISTER Segment;
    WHV_X64_TABLE_REGISTER Table;
    WHV_X64_INTERRUPT_STATE_REGISTER InterruptState;
    WHV_X64_PENDING_INTERRUPTION_REGISTER PendingInterruption;
    WHV_X64_DELIVERABILITY_NOTIFICATIONS_REGISTER DeliverabilityNotifications;
    WHV_X64_PENDING_EXCEPTION_EVENT ExceptionEvent;
    WHV_X64_PENDING_EXT_INT_EVENT ExtIntEvent;
} WHV_REGISTER_VALUE;
```

### 初始化通用寄存器
分类注册的原则是为每一类寄存器定义一组Name和一组Value,以通用寄存器为例,代码如下:
```C
WHV_REGISTER_NAME SwInitGprNameGroup =
{
        WHvX64RegisterRax,
        WHvX64RegisterRcx,
        WHvX64RegisterRdx,
        WHvX64RegisterRbx,
        WHvX64RegisterRsp,
        WHvX64RegisterRbp,
        WHvX64RegisterRsi,
        WHvX64RegisterRdi,
        WHvX64RegisterR8,
        WHvX64RegisterR9,
        WHvX64RegisterR10,
        WHvX64RegisterR11,
        WHvX64RegisterR12,
        WHvX64RegisterR13,
        WHvX64RegisterR14,
        WHvX64RegisterR15,
        WHvX64RegisterRip,
        WHvX64RegisterRflags
};

WHV_REGISTER_VALUE SwInitGprValueGroup =
{
        {0},{0},{0},{0},{0xFFF0},{0},{0},{0},
        {0},{0},{0},{0},{0},{0},{0},{0},
        {0x100},{0x202}
};
```
注意`WHV_REGISTER_VALUE`是个联合体,所以每个值都要套一个大括号

### 初始化段寄存器
段寄存器也是同理:
```C
WHV_REGISTER_NAME SwInitSrNameGroup =
{
        WHvX64RegisterEs,
        WHvX64RegisterCs,
        WHvX64RegisterSs,
        WHvX64RegisterDs,
        WHvX64RegisterFs,
        WHvX64RegisterGs,
        WHvX64RegisterLdtr,
        WHvX64RegisterTr
};

WHV_X64_SEGMENT_REGISTER SwInitSrValueGroup =
{
        {0,0xFFFF,0x1000,{3,1,0,1,0,1,0,0,0}},
        {0,0xFFFF,0x1000,{11,1,0,1,0,1,0,0,0}},
        {0,0xFFFF,0x1000,{3,1,0,1,0,1,0,0,0}},
        {0,0xFFFF,0x1000,{3,1,0,1,0,1,0,0,0}},
        {0,0xFFFF,0x1000,{3,1,0,1,0,1,0,0,0}},
        {0,0xFFFF,0x1000,{3,1,0,1,0,1,0,0,0}},
        {0,0xFFFF,0,{2,0,0,1,0,1,0,0,0}},
        {0,0xFFFF,0,{3,0,0,1,0,1,0,0,0}}
};
```
其中`WHV_X64_SEGMENT_REGISTER`的定义为:
```C
typedef struct WHV_X64_SEGMENT_REGISTER
{
    UINT64 Base;
    UINT32 Limit;
    UINT16 Selector;

    union
    {
      struct
      {
            UINT16 SegmentType:4;
            UINT16 NonSystemSegment:1;
            UINT16 DescriptorPrivilegeLevel:2;
            UINT16 Present:1;
            UINT16 Reserved:4;
            UINT16 Available:1;
            UINT16 Long:1;
            UINT16 Default:1;
            UINT16 Granularity:1;
      };

      UINT16 Attributes;
    };
} WHV_X64_SEGMENT_REGISTER;
```
每一项是什么意思请参见Intel x64/AMD64手册。

### 初始化其他寄存器
其他寄存器的初始化也是同理:
```C
WHV_REGISTER_NAME SwInitDescriptorNameGroup =
{
        WHvX64RegisterIdtr,
        WHvX64RegisterGdtr
};

WHV_X64_TABLE_REGISTER SwInitDescriptorValueGroup =
{
        {{0,0,0},0xFFFF,0},
        {{0,0,0},0xFFFF,0}
};

WHV_REGISTER_NAME SwInitCrNameGroup =
{
        WHvX64RegisterCr0,
        WHvX64RegisterCr2,
        WHvX64RegisterCr3,
        WHvX64RegisterCr4
};

WHV_REGISTER_VALUE SwInitCrValueGroup =
{
        {0x60000010},
        {0},{0},{0}
};

WHV_REGISTER_NAME SwInitDrNameGroup =
{
        WHvX64RegisterDr0,
        WHvX64RegisterDr1,
        WHvX64RegisterDr2,
        WHvX64RegisterDr3,
        WHvX64RegisterDr6,
        WHvX64RegisterDr7,
};

WHV_REGISTER_VALUE SwInitDrValueGroup =
{
        {0},{0},{0},{0},
        {0xFFFF0FF0},{0x400}
};

WHV_REGISTER_NAME SwInitXcrNameGroup =
{
        WHvX64RegisterXCr0
};

WHV_REGISTER_VALUE SwInitXcrValueGroup =
{
        {1}
};

WHV_REGISTER_NAME SwInitFpcsName = WHvX64RegisterFpControlStatus;
WHV_X64_FP_CONTROL_STATUS_REGISTER SwInitFpcsValue =
{
        0x40,0x0,0x5555,0x0,0x0,{0}
};
```

## 初始化虚拟机的代码实现
按照之前所描述的顺序调用API即可,代码如下:
```C
HRESULT SwInitializeVirtualMachine()
{
        BOOL PartitionCreated = FALSE;
        BOOL VcpuCreated = FALSE;
        BOOL MemoryAllocated = FALSE;
        // Create a virtual machine.
        HRESULT hr = WHvCreatePartition(&hPart);
        if (hr == S_OK)
                PartitionCreated = TRUE;
        else
                goto Cleanup;
        // Setup Partition Properties.
        hr = WHvSetPartitionProperty(hPart, WHvPartitionPropertyCodeProcessorCount, &SwProcessorCount, sizeof(SwProcessorCount));
        if (hr != S_OK)
        {
                printf("Failed to setup Processor Count! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        // Setup Partition
        hr = WHvSetupPartition(hPart);
        if (hr != S_OK)
        {
                printf("Failed to setup Virtual Machine! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        // Create Virtual Memory.
        VirtualMemory = VirtualAlloc(NULL, GuestMemorySize, MEM_COMMIT, PAGE_READWRITE);
        if (VirtualMemory)
                MemoryAllocated = TRUE;
        else
                goto Cleanup;
        RtlZeroMemory(VirtualMemory, GuestMemorySize);
        hr = WHvMapGpaRange(hPart, VirtualMemory, 0, GuestMemorySize, WHvMapGpaRangeFlagRead | WHvMapGpaRangeFlagWrite | WHvMapGpaRangeFlagExecute);
        if (hr != S_OK)goto Cleanup;
        // Create Virtual Processors.
        hr = WHvCreateVirtualProcessor(hPart, 0, 0);
        if (hr == S_OK)
                VcpuCreated = TRUE;
        else
                goto Cleanup;
        // Initialize Virtual Processor State
        hr = WHvSetVirtualProcessorRegisters(hPart, 0, SwInitGprNameGroup, 0x12, SwInitGprValueGroup);
        if (hr != S_OK)
        {
                printf("Failed to initialize General Purpose Registers! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        hr = WHvSetVirtualProcessorRegisters(hPart, 0, SwInitSrNameGroup, 8, (WHV_REGISTER_VALUE*)SwInitSrValueGroup);
        if (hr != S_OK)
        {
                printf("Failed to initialize Segment Registers! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        hr = WHvSetVirtualProcessorRegisters(hPart, 0, SwInitDescriptorNameGroup, 2, (WHV_REGISTER_VALUE*)SwInitDescriptorValueGroup);
        if (hr != S_OK)
        {
                printf("Failed to initialize Descriptor Tables! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        hr = WHvSetVirtualProcessorRegisters(hPart, 0, SwInitCrNameGroup, 4, SwInitCrValueGroup);
        if (hr != S_OK)
        {
                printf("Failed to initialize Control Registers! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        hr = WHvSetVirtualProcessorRegisters(hPart, 0, SwInitDrNameGroup, 6, SwInitDrValueGroup);
        if (hr != S_OK)
        {
                printf("Failed to initialize Debug Registers! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        hr = WHvSetVirtualProcessorRegisters(hPart, 0, SwInitXcrNameGroup, 1, SwInitXcrValueGroup);
        if (hr != S_OK)
        {
                printf("Failed to initialize Extended Control Registers! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        hr = WHvSetVirtualProcessorRegisters(hPart, 0, &SwInitFpcsName, 1, (WHV_REGISTER_VALUE*)&SwInitFpcsValue);
        if (hr != S_OK)
        {
                printf("Failed to initialize x87 Floating Point Control Status! HRESULT=0x%X\n", hr);
                goto Cleanup;
        }
        return S_OK;
Cleanup:
        if (MemoryAllocated)VirtualFree(VirtualMemory, 0, MEM_RELEASE);
        if (VcpuCreated)WHvDeleteVirtualProcessor(hPart, 0);
        if (PartitionCreated)WHvDeletePartition(hPart);
        return S_FALSE;
}
```

## 加载程序
创建虚拟机是为了运行代码的,没有代码还创建什么虚拟机呢。这里以读文件的方式加载程序,代码如下:
```C
BOOL LoadVirtualMachineProgram(IN PSTR FileName, IN ULONG Offset)
{
        BOOL Result = FALSE;
        HANDLE hFile = CreateFileA(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        if (hFile != INVALID_HANDLE_VALUE)
        {
                DWORD FileSize = GetFileSize(hFile, NULL);
                if (FileSize != INVALID_FILE_SIZE)
                {
                        DWORD dwSize = 0;
                        PVOID ProgramAddress = (PVOID)((ULONG_PTR)VirtualMemory + Offset);
                        Result = ReadFile(hFile, ProgramAddress, FileSize, &dwSize, NULL);
                }
                CloseHandle(hFile);
        }
        return Result;
}
```

# 运行虚拟机
与其说是运行虚拟机,不如说是运行虚拟机的一个vCPU。运行vCPU用`WHvRunVirtualProcessor`函数,其原型如下:
```C
HRESULT
WINAPI
WHvRunVirtualProcessor(
    _In_ WHV_PARTITION_HANDLE Partition,
    _In_ UINT32 VpIndex,
    _Out_writes_bytes_(ExitContextSizeInBytes) VOID* ExitContext,
    _In_ UINT32 ExitContextSizeInBytes
    );
```

## 处理VM-Exit
当处理器遇到特定事件的时候会产生VM-Exit,交给Host以判断如何进行下一步。在WHP中,遇到VM-Exit时会把vCPU的一些状态保存到`ExitContext`参数中。其中`ExitContext`参数是一个`WHV_RUN_VP_EXIT_CONTEXT`结构体,其定义如下:
```C
typedef struct WHV_RUN_VP_EXIT_CONTEXT
{
    WHV_RUN_VP_EXIT_REASON ExitReason;
    UINT32 Reserved;
    WHV_VP_EXIT_CONTEXT VpContext;

    union
    {
      WHV_MEMORY_ACCESS_CONTEXT MemoryAccess;
      WHV_X64_IO_PORT_ACCESS_CONTEXT IoPortAccess;
      WHV_X64_MSR_ACCESS_CONTEXT MsrAccess;
      WHV_X64_CPUID_ACCESS_CONTEXT CpuidAccess;
      WHV_VP_EXCEPTION_CONTEXT VpException;
      WHV_X64_INTERRUPTION_DELIVERABLE_CONTEXT InterruptWindow;
      WHV_X64_UNSUPPORTED_FEATURE_CONTEXT UnsupportedFeature;
      WHV_RUN_VP_CANCELED_CONTEXT CancelReason;
      WHV_X64_APIC_EOI_CONTEXT ApicEoi;
      WHV_X64_RDTSC_CONTEXT ReadTsc;
    };
} WHV_RUN_VP_EXIT_CONTEXT;
```
第一个成员`ExitReason`表明VM-Exit的原因,其定义如下:
```C
typedef enum WHV_RUN_VP_EXIT_REASON
{
    WHvRunVpExitReasonNone                   = 0x00000000,

    // Standard exits caused by operations of the virtual processor
    WHvRunVpExitReasonMemoryAccess         = 0x00000001,
    WHvRunVpExitReasonX64IoPortAccess      = 0x00000002,
    WHvRunVpExitReasonUnrecoverableException = 0x00000004,
    WHvRunVpExitReasonInvalidVpRegisterValue = 0x00000005,
    WHvRunVpExitReasonUnsupportedFeature   = 0x00000006,
    WHvRunVpExitReasonX64InterruptWindow   = 0x00000007,
    WHvRunVpExitReasonX64Halt                = 0x00000008,
    WHvRunVpExitReasonX64ApicEoi             = 0x00000009,

    // Additional exits that can be configured through partition properties
    WHvRunVpExitReasonX64MsrAccess         = 0x00001000,
    WHvRunVpExitReasonX64Cpuid               = 0x00001001,
    WHvRunVpExitReasonException            = 0x00001002,
    WHvRunVpExitReasonX64Rdtsc               = 0x00001003,

    // Exits caused by the host
    WHvRunVpExitReasonCanceled               = 0x00002001
} WHV_RUN_VP_EXIT_REASON;
```
而`VpContext`成员表示vCPU的上下文,其定义如下:
```C
typedef union WHV_X64_VP_EXECUTION_STATE
{
    struct
    {
      UINT16 Cpl : 2;
      UINT16 Cr0Pe : 1;
      UINT16 Cr0Am : 1;
      UINT16 EferLma : 1;
      UINT16 DebugActive : 1;
      UINT16 InterruptionPending : 1;
      UINT16 Reserved0 : 5;
      UINT16 InterruptShadow : 1;
      UINT16 Reserved1 : 3;
    };

    UINT16 AsUINT16;
} WHV_X64_VP_EXECUTION_STATE;

typedef struct WHV_VP_EXIT_CONTEXT
{
    WHV_X64_VP_EXECUTION_STATE ExecutionState;
    UINT8 InstructionLength : 4;
    UINT8 Cr8 : 4;
    UINT8 Reserved;
    UINT32 Reserved2;
    WHV_X64_SEGMENT_REGISTER Cs;
    UINT64 Rip;
    UINT64 Rflags;
} WHV_VP_EXIT_CONTEXT;
```
了解这些之后,就可以开始构造虚拟机软件了

## 构造虚拟机软件
由于初代的WHP没有专门的Hypercall机制,于是我只好使用`I/O`的方法来实现Hypercall。
本文的虚拟机软件只需要实现一个Hello World即可,也就是说要能成功运行一个能在DOS中输出Hello World的字符串即可,那么比较合适的做法是用`rep outsb`指令实现Hypercall来输出字符串到控制台。
总的来说,框架如下:
DOS程序调用`int 21h`中断->中断处理代码调用`rep outsb`指令进行Hypercall->虚拟机软件收到Hypercall实现输出到控制台。
而程序是需要终止的。我的方案是用`cli`+`hlt`指令来实现程序终止。

### 编写DOS下的程序
要实现一个DOS的Hello World倒是不难,我们只管调用中断就行了。这里使用NASM编写,代码如下:
```Asm
bits 16
org 0x100

segment .text
start:
    mov dx,hello_str
    mov ah,9
    int 0x21
    xor ah,ah
    int 0x21

segment .data
hello_str:
db "Hello World in DOS!",10,'$',0
```
注意DOS的`int 21h/ah=9`这个中断需要用`$`符号终止一个字符串。

### 编写BIOS固件
固件是什么样的组织结构完全由虚拟机软件的编写者说了算,我们还是用NASM来编写,开头是1KB的IVT(Interrupt Vector Table,中断向量表),后面就是中断处理代码。
但由于代码太长,我就不在帖子里贴IVT了,只贴一些关键的代码。
```Asm
virt_int21_handler:
        cmp ah,9
        je int21_print_string_stdout
        cmp ah,0
        je int21_termination
        iret

int21_print_string_stdout:
        mov si,dx
        mov dx,str_prt_port
        rep outsb
        iret

int21_termination:
        call print_halted
        cli
        hlt
```

## 运行vCPU和处理VM-Exit
由于VM-Exit的存在,我们必须要用循环语句不停的调用`WHvRunVirtualProcessor`函数,直到结束运行的条件发生后才能跳出循环。
遇到因指令而退出的VM-Exit时,需要增进`rip`寄存器的值让`rip`指向下一条指令,否则就会在当前的这条指令上死循环而不断的VM-Exit。
对于`outs`造成VM-Exit而言,输出的地址位于`ds:rsi`,而输入的地址位于`es:rdi`,长度由`rcx`寄存器指定。
对于`hlt`指令造成的VM-Exit而言,由于我们假设`cli`+`hlt`为程序退出,因此要判断`rflags.if`是否置位再决定是否退出。
对于其他的VM-Exit,我们一概认为Guest存在异常,停止执行。
代码如下:
```C
HRESULT SwExecuteProgram()
{
        WHV_RUN_VP_EXIT_CONTEXT ExitContext = { 0 };
        BOOL ContinueExecution = TRUE;
        HRESULT hr = S_FALSE;
        while (ContinueExecution)
        {
                hr = WHvRunVirtualProcessor(hPart, 0, &ExitContext, sizeof(ExitContext));
                if (hr == S_OK)
                {
                        WHV_REGISTER_NAME RipName = WHvX64RegisterRip;
                        WHV_REGISTER_VALUE Rip = { ExitContext.VpContext.Rip };
                        switch (ExitContext.ExitReason)
                        {
                        case WHvRunVpExitReasonMemoryAccess:
                        {
                                PSTR AccessType = { "Read","Write","Execute","Unknown"};
                                puts("Memory Access Violation occured!");
                                printf("Access Context: GVA=0x%llX GPA=0x%0llX\n", ExitContext.MemoryAccess.Gva, ExitContext.MemoryAccess.Gpa);
                                printf("Behavior: %s\t", AccessType);
                                printf("GVA is %s \t", ExitContext.MemoryAccess.AccessInfo.GvaValid ? "Valid" : "Invalid");
                                printf("GPA is %s \n", ExitContext.MemoryAccess.AccessInfo.GpaUnmapped ? "Mapped" : "Unmapped");
                                printf("Number of Instruction Bytes: %d\n Instruction Bytes: ", ExitContext.MemoryAccess.InstructionByteCount);
                                for (UINT8 i = 0; i < ExitContext.MemoryAccess.InstructionByteCount; i++)
                                        printf("%02X ", ExitContext.MemoryAccess.InstructionBytes);
                                ContinueExecution = FALSE;
                                break;
                        }
                        case WHvRunVpExitReasonX64IoPortAccess:
                        {
                                WHV_REGISTER_NAME RevGprName = { WHvX64RegisterRax,WHvX64RegisterRcx,WHvX64RegisterRsi,WHvX64RegisterRdi };
                                WHV_REGISTER_VALUE RevGprValue;
                                RevGprValue.Reg64 = ExitContext.IoPortAccess.Rax;
                                RevGprValue.Reg64 = ExitContext.IoPortAccess.Rcx;
                                RevGprValue.Reg64 = ExitContext.IoPortAccess.Rsi;
                                RevGprValue.Reg64 = ExitContext.IoPortAccess.Rdi;
                                if (ExitContext.IoPortAccess.PortNumber == IO_PORT_STRING_PRINT)
                                {
                                        if (ExitContext.IoPortAccess.AccessInfo.IsWrite)
                                        {
                                                INT32 Direction = _bittest64(&ExitContext.VpContext.Rflags, 10) ? -1 : 1;
                                                INT32 Increment = ExitContext.IoPortAccess.AccessInfo.AccessSize * Direction;
                                                if (ExitContext.IoPortAccess.AccessInfo.StringOp)
                                                {
                                                        UINT64 Gpa = ((UINT64)ExitContext.IoPortAccess.Ds.Selector << 4) + ExitContext.IoPortAccess.Rsi;
                                                        PSTR StringAddress = (PSTR)((ULONG_PTR)VirtualMemory + Gpa);
                                                        if (ExitContext.IoPortAccess.AccessInfo.RepPrefix)
                                                        {
                                                                UINT32 StrLen = SwDosStringLength(StringAddress, 1000);
                                                                printf("%.*s", StrLen, StringAddress);
                                                                RevGprValue.Reg64 = 0;
                                                        }
                                                        else
                                                        {
                                                                putc(*StringAddress, stdout);
                                                        }
                                                }
                                                else
                                                {
                                                        putc((UINT8)ExitContext.IoPortAccess.Rax, stdout);
                                                }
                                        }
                                }
                                WHvSetVirtualProcessorRegisters(hPart, 0, RevGprName, 4, RevGprValue);
                                break;
                        }
                        case WHvRunVpExitReasonUnrecoverableException:
                                puts("The processor went into shutdown state due to unrecoverable exception!");
                                ContinueExecution = FALSE;
                                break;
                        case WHvRunVpExitReasonInvalidVpRegisterValue:
                                puts("The specified processor state is invalid!");
                                ContinueExecution = FALSE;
                                break;
                        case WHvRunVpExitReasonX64Halt:
                                ContinueExecution = _bittest64(&ExitContext.VpContext.Rflags, 9);
                                break;
                        default:
                                printf("Unknown VM-Exit Code=0x%X!\n", ExitContext.ExitReason);
                                ContinueExecution = FALSE;
                                break;
                        }
                        Rip.Reg64 += ExitContext.VpContext.InstructionLength;
                        hr = WHvSetVirtualProcessorRegisters(hPart, 0, &RipName, 1, &Rip);
                }
                else
                {
                        printf("Failed to run virtual processor! HRESULT=0x%X\n", hr);
                        ContinueExecution = FALSE;
                }
        }
        return hr;
}
```

# 测试结果
效果拔群!下图是本程序与DOSBox的运行结果对比:


# 结语
本文的代码已在GitHub上开源:https://github.com/Zero-Tang/SimpleWhpDemo
编译好的二进制文件也发布在GitHub上了:https://github.com/Zero-Tang/SimpleWhpDemo/releases
微软[关于WHP的API文档](https://docs.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform)写的实在是糟糕,很多内容都太潦草了。
此外截止到发帖日(2021-07-25),WHP的文档有好久没有更新了,很多函数,结构体,联合体,常量等都没有相应的文档。我敢断定VMware肯定是拿到或推测出了微软内部的WHP文档才在2004版本的Windows 10中实现了VMware Workstation与Hyper-V共存的。

tangptr@126.com 发表于 2024-4-4 16:52:33


# 2024-04-04 更新
最初写这篇帖子的时候没有意识到微软提供的(https://learn.microsoft.com/en-us/virtualization/api/hypervisor-instruction-emulator/hypervisor-instruction-emulator)的作用。我还以为这是单纯模拟所有虚拟机指令的API。
后来在阅读QEMU源码的时候才明白,原来这套API的意义是为了更简单的模拟I/O,于是我们就不必费劲的解析I/O上下文了。本文附带源码已更新,使用模拟器API实现I/O模拟。

## 模拟器框架
这套API需要虚拟机软件提供五个回调函数:端口I/O回调、内存访问回调、寄存器读取回调、寄存器写入回调、虚拟地址翻译回调。
后三者的实现非常简单,仅需要对WHP的API进行一个包装即可。注意如果虚拟机是多核的,调用模拟器需要使用`Context`参数把vCPU的核心号告知给回调函数。我们这里的虚拟机是单核的,所以无需`Context`。

```C
HRESULT SwEmulatorGetVirtualRegistersCallback(IN PVOID Context, IN CONST WHV_REGISTER_NAME* RegisterNames, IN UINT32 RegisterCount, OUT WHV_REGISTER_VALUE* RegisterValues)
{
        return WHvGetVirtualProcessorRegisters(hPart, 0, RegisterNames, RegisterCount, RegisterValues);
}

HRESULT SwEmulatorSetVirtualRegistersCallback(IN PVOID Context, IN CONST WHV_REGISTER_NAME* RegisterNames, IN UINT32 RegisterCount, IN CONST WHV_REGISTER_VALUE* RegisterValues)
{
        return WHvSetVirtualProcessorRegisters(hPart, 0, RegisterNames, RegisterCount, RegisterValues);
}

HRESULT SwEmulatorTranslateGvaPageCallback(IN PVOID Context, IN WHV_GUEST_VIRTUAL_ADDRESS GvaPage, IN WHV_TRANSLATE_GVA_FLAGS TranslateFlags, OUT WHV_TRANSLATE_GVA_RESULT_CODE* TranslationResult, OUT WHV_GUEST_PHYSICAL_ADDRESS* GpaPage)
{
        WHV_TRANSLATE_GVA_RESULT Result;
        HRESULT hr = WHvTranslateGva(hPart, 0, GvaPage, TranslateFlags, &Result, GpaPage);
        *TranslationResult = Result.ResultCode;
        return hr;
}
```

## 端口I/O回调
模拟器API会通过`WHV_EMULATOR_IO_PORT_CALLBACK`结构体传输数据,我们可以直接把结构体里提供的数据传给`putc`函数从而实现虚拟控制台输出。

```C
HRESULT SwEmulatorIoCallback(IN PVOID Context, IN OUT WHV_EMULATOR_IO_ACCESS_INFO* IoAccess)
{
        if (IoAccess->AccessSize != 1)
        {
                printf("Only size of 1 operand is allowed! Access Size is %u bytes.\n", IoAccess->AccessSize);
                return E_NOTIMPL;
        }
        if (IoAccess->Direction == 0)
        {
                puts("Input is not implemented!");
                return E_NOTIMPL;
        }
        if (IoAccess->Port == IO_PORT_STRING_PRINT)
        {
                putc(IoAccess->Data, stdout);
                return S_OK;
        }
        else
        {
                printf("Unknown I/O Port: 0x%04X is accessed!\n", IoAccess->Port);
                return E_NOTIMPL;
        }
}
```

## 内存访问回调
当调用到这个回调时,并不一定代表此时发生的一定就是MMIO。以本文为例,我们使用了`outsb`指令,因此端口I/O的数据在内存上而不是寄存器里。因此我们需要在这个回调里复制内存。

```C
HRESULT SwEmulatorMmioCallback(IN PVOID Context, IN OUT WHV_EMULATOR_MEMORY_ACCESS_INFO* MemoryAccess)
{
        PVOID HvaAddress = (PVOID)((ULONG_PTR)VirtualMemory + MemoryAccess->GpaAddress);
        if(MemoryAccess->GpaAddress+MemoryAccess->AccessSize>=GuestMemorySize)
        {
                printf("Memory-Access Overflow is detected! GPA=0x%016llX, Access-Size=%u bytes\n", MemoryAccess->GpaAddress, MemoryAccess->AccessSize);
                return E_FAIL;
        }
        if (MemoryAccess->Direction)
                RtlCopyMemory(HvaAddress, MemoryAccess->Data, MemoryAccess->AccessSize);
        else
                RtlCopyMemory(MemoryAccess->Data, HvaAddress, MemoryAccess->AccessSize);
        return S_OK;
}
```

## 总结
WHP提供的这套模拟器API并非是模拟所有虚拟机指令的软件模拟器,而是用来简化I/O模拟的非常实用的API。

Golden Blonde 发表于 2021-7-25 20:02:12

论抢沙发我第一,论写代码你牛逼。

0xAA55 发表于 2021-7-26 06:49:26

这直接就能调用API写虚拟机了呀!突然想到,Win10的API里有个CreateEnclave(),对比下来怎么样。

是北柠呀 发表于 2021-7-26 11:16:41


腻害..........................

Golden Blonde 发表于 2021-7-26 16:19:05

0xAA55 发表于 2021-7-26 06:49
这直接就能调用API写虚拟机了呀!突然想到,Win10的API里有个CreateEnclave(),对比下来怎么样。 ...

这个好像是跟INTEL SGX相关的东西吧,就是允许你创建的了一块内存区域,但别人无法访问,只有你自己可以访问。

唐凌 发表于 2021-7-26 19:27:51

0xAA55 发表于 2021-7-26 06:49
这直接就能调用API写虚拟机了呀!突然想到,Win10的API里有个CreateEnclave(),对比下来怎么样。 ...

并非同一个用途的东西。SGX虽然拥有隔离计算的能力来实现可信计算环境,但是逻辑上Enclave之内只能是Ring3的权限,而且还只能是Host所运行的模式。即便是Windows用VBS实现的Enclave也只能用相同的逻辑。
页: [1]
查看完整版本: 【虚拟化】使用WHP实现在64位Windows 10中运行DOS的Hello World!