【虚拟化】使用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共存的。
# 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。
论抢沙发我第一,论写代码你牛逼。 这直接就能调用API写虚拟机了呀!突然想到,Win10的API里有个CreateEnclave(),对比下来怎么样。
腻害..........................
0xAA55 发表于 2021-7-26 06:49
这直接就能调用API写虚拟机了呀!突然想到,Win10的API里有个CreateEnclave(),对比下来怎么样。 ...
这个好像是跟INTEL SGX相关的东西吧,就是允许你创建的了一块内存区域,但别人无法访问,只有你自己可以访问。 0xAA55 发表于 2021-7-26 06:49
这直接就能调用API写虚拟机了呀!突然想到,Win10的API里有个CreateEnclave(),对比下来怎么样。 ...
并非同一个用途的东西。SGX虽然拥有隔离计算的能力来实现可信计算环境,但是逻辑上Enclave之内只能是Ring3的权限,而且还只能是Host所运行的模式。即便是Windows用VBS实现的Enclave也只能用相同的逻辑。
页:
[1]