前言
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
的函数原型如下:
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
的定义如下:
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
创建一个虚拟机,其函数原型如下:
typedef VOID* WHV_PARTITION_HANDLE;
HRESULT
WINAPI
WHvCreatePartition(
_Out_ WHV_PARTITION_HANDLE* Partition
);
返回的是一个虚拟机句柄。
初始化虚拟机属性
创建完成后,需要用WHvSetPartitionProperty
函数设置虚拟机属性,其函数原型如下:
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
的定义如下:
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中,其函数原型如下:
HRESULT
WINAPI
WHvSetupPartition(
_In_ WHV_PARTITION_HANDLE Partition
);
内存虚拟化
使用硬件虚拟化实现虚拟机需要让内存按页对齐,因此最好用VirtualAlloc这样的函数来分配内存。
除此之外,还要设置内存映射。不过WHP的映射要求你输入的GPA和HVA,而不是HPA。设置映射需要使用WHvMapGpaRange
函数,其函数原型如下:
// 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
函数,原型如下:
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
函数,原型如下:
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位的联合体,定义如下:
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,以通用寄存器为例,代码如下:
WHV_REGISTER_NAME SwInitGprNameGroup[0x12] =
{
WHvX64RegisterRax,
WHvX64RegisterRcx,
WHvX64RegisterRdx,
WHvX64RegisterRbx,
WHvX64RegisterRsp,
WHvX64RegisterRbp,
WHvX64RegisterRsi,
WHvX64RegisterRdi,
WHvX64RegisterR8,
WHvX64RegisterR9,
WHvX64RegisterR10,
WHvX64RegisterR11,
WHvX64RegisterR12,
WHvX64RegisterR13,
WHvX64RegisterR14,
WHvX64RegisterR15,
WHvX64RegisterRip,
WHvX64RegisterRflags
};
WHV_REGISTER_VALUE SwInitGprValueGroup[0x12] =
{
{0},{0},{0},{0},{0xFFF0},{0},{0},{0},
{0},{0},{0},{0},{0},{0},{0},{0},
{0x100},{0x202}
};
注意WHV_REGISTER_VALUE
是个联合体,所以每个值都要套一个大括号
初始化段寄存器
段寄存器也是同理:
WHV_REGISTER_NAME SwInitSrNameGroup[8] =
{
WHvX64RegisterEs,
WHvX64RegisterCs,
WHvX64RegisterSs,
WHvX64RegisterDs,
WHvX64RegisterFs,
WHvX64RegisterGs,
WHvX64RegisterLdtr,
WHvX64RegisterTr
};
WHV_X64_SEGMENT_REGISTER SwInitSrValueGroup[8] =
{
{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
的定义为:
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手册。
初始化其他寄存器
其他寄存器的初始化也是同理:
WHV_REGISTER_NAME SwInitDescriptorNameGroup[2] =
{
WHvX64RegisterIdtr,
WHvX64RegisterGdtr
};
WHV_X64_TABLE_REGISTER SwInitDescriptorValueGroup[2] =
{
{{0,0,0},0xFFFF,0},
{{0,0,0},0xFFFF,0}
};
WHV_REGISTER_NAME SwInitCrNameGroup[4] =
{
WHvX64RegisterCr0,
WHvX64RegisterCr2,
WHvX64RegisterCr3,
WHvX64RegisterCr4
};
WHV_REGISTER_VALUE SwInitCrValueGroup[4] =
{
{0x60000010},
{0},{0},{0}
};
WHV_REGISTER_NAME SwInitDrNameGroup[6] =
{
WHvX64RegisterDr0,
WHvX64RegisterDr1,
WHvX64RegisterDr2,
WHvX64RegisterDr3,
WHvX64RegisterDr6,
WHvX64RegisterDr7,
};
WHV_REGISTER_VALUE SwInitDrValueGroup[6] =
{
{0},{0},{0},{0},
{0xFFFF0FF0},{0x400}
};
WHV_REGISTER_NAME SwInitXcrNameGroup[1] =
{
WHvX64RegisterXCr0
};
WHV_REGISTER_VALUE SwInitXcrValueGroup[1] =
{
{1}
};
WHV_REGISTER_NAME SwInitFpcsName = WHvX64RegisterFpControlStatus;
WHV_X64_FP_CONTROL_STATUS_REGISTER SwInitFpcsValue =
{
0x40,0x0,0x5555,0x0,0x0,{0}
};
初始化虚拟机的代码实现
按照之前所描述的顺序调用API即可,代码如下:
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;
}
加载程序
创建虚拟机是为了运行代码的,没有代码还创建什么虚拟机呢。这里以读文件的方式加载程序,代码如下:
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
函数,其原型如下:
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
结构体,其定义如下:
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的原因,其定义如下:
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的上下文,其定义如下:
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编写,代码如下:
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了,只贴一些关键的代码。
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存在异常,停止执行。
代码如下:
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[4] = { "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[ExitContext.MemoryAccess.AccessInfo.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[i]);
ContinueExecution = FALSE;
break;
}
case WHvRunVpExitReasonX64IoPortAccess:
{
WHV_REGISTER_NAME RevGprName[4] = { WHvX64RegisterRax,WHvX64RegisterRcx,WHvX64RegisterRsi,WHvX64RegisterRdi };
WHV_REGISTER_VALUE RevGprValue[4];
RevGprValue[0].Reg64 = ExitContext.IoPortAccess.Rax;
RevGprValue[1].Reg64 = ExitContext.IoPortAccess.Rcx;
RevGprValue[2].Reg64 = ExitContext.IoPortAccess.Rsi;
RevGprValue[3].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[1].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文档写的实在是糟糕,很多内容都太潦草了。
此外截止到发帖日(2021-07-25),WHP的文档有好久没有更新了,很多函数,结构体,联合体,常量等都没有相应的文档。我敢断定VMware肯定是拿到或推测出了微软内部的WHP文档才在2004版本的Windows 10中实现了VMware Workstation与Hyper-V共存的。