找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3516|回复: 6

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

[复制链接]
发表于 2021-7-25 16:53:30 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

前言

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.PNG
其他内容对于本文所设计的内容无关,故不在本文讨论。

初始化虚拟机

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

创建虚拟机

使用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),主要设置dr6dr7寄存器。
  • 浮点数协处理器控制与状态寄存器(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的运行结果对比:
HelloDOS.png

结语

本文的代码已在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共存的。

回复

使用道具 举报

发表于 2024-4-4 16:52:33 | 显示全部楼层

2024-04-04 更新

最初写这篇帖子的时候没有意识到微软提供的Hypervisor Instruction Emulator API的作用。我还以为这是单纯模拟所有虚拟机指令的API。
后来在阅读QEMU源码的时候才明白,原来这套API的意义是为了更简单的模拟I/O,于是我们就不必费劲的解析I/O上下文了。本文附带源码已更新,使用模拟器API实现I/O模拟。

模拟器框架

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

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函数从而实现虚拟控制台输出。

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的数据在内存上而不是寄存器里。因此我们需要在这个回调里复制内存。

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。

回复 赞! 靠!

使用道具 举报

发表于 2021-7-25 20:02:12 | 显示全部楼层
论抢沙发我第一,论写代码你牛逼。
回复 赞! 靠!

使用道具 举报

发表于 2021-7-26 06:49:26 | 显示全部楼层
这直接就能调用API写虚拟机了呀!突然想到,Win10的API里有个CreateEnclave(),对比下来怎么样。
回复 赞! 靠!

使用道具 举报

发表于 2021-7-26 11:16:41 | 显示全部楼层

腻害..........................
回复

使用道具 举报

发表于 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也只能用相同的逻辑。
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-12-26 22:09 , Processed in 0.041434 second(s), 26 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表