hvpp

hvpp

PROJECT_SOURCE_ANALYSIS

本文是 hvpp 的中文源码阅读主文档。它不是对 README 的翻译,而是把入口、对象生命周期、CPU 状态切换、VM-exit 分发、EPT、内存管理、C 接口和示例程序连接成一条可实际跟读的路线。

本轮遵循 source-reading-map 工作流:只读取项目本身的源码、项目文件和本地文档;只向核心源码增加中文阅读注释;不修改程序行为,不编译、不加载驱动、不执行虚拟化代码。

1. 项目定位

hvpp 是一个面向 Intel x64 VT-x + EPT 的轻量级 Type-2 风格研究型 hypervisor。它不负责从零启动一个新操作系统,而是在 Windows 已经运行后,由内核驱动让每个逻辑处理器进入 VMX operation,再把原本正在运行的 Windows 作为 guest 继续执行。

这里的 “guest” 很容易误解:

  • 它不是另一个新建的 Windows 虚拟机。
  • 它就是装载驱动之前正在运行的这个 Windows。
  • VMX 启动后,普通 Windows 代码运行在 VMX non-root operation
  • 发生受监控的事件或指令时,处理器切到 VMX root operation,从 vcpu.asm 进入 C++ VM-exit handler。
  • “root/non-root” 是 VMX 的执行模式;把 root 简称为 “ring -1” 便于交流,但它不是分段保护模型中真的新增了一个 CPL=-1。

项目的主要学习价值不是“完成了多少虚拟机功能”,而是提供了一条相对紧凑的链路:

Windows 加载驱动-> 初始化日志、物理内存/MTRR/页表信息与专用分配器-> 为每个逻辑处理器构造一个 vcpu_t-> IPI 广播:每个处理器分别执行 VMXON、配置 VMCS、VMLAUNCH-> Windows 在 VMX non-root 中继续运行-> 特定事件触发 VM-exit-> 汇编保存寄存器,C++ 按 exit reason 分发并模拟原指令-> VMRESUME 返回原来的 Windows 执行流-> 卸载时用 VMCALL 主动触发 VM-exit,在 root 模式执行 VMXOFF

2. 证据来源

本分析优先采用以下仓库内证据:

证据 能确认什么
README.md 项目目标、运行约束、示例现象和作者给出的高层 workflow
hvpp.sln*.vcxproj 工程之间的依赖、静态库/驱动/应用类型、实际参与编译的文件
src/hvpp/hvpp/lib/win32/driver.cpp 真正的 Windows DriverEntry、IRP 分发和 unload 入口
src/hvppdrv/main.cpp C++ 示例驱动的回调初始化、handler 组合方式
src/hvpp/hvpp/hypervisor.cpp 全处理器 VCPU 数组、启动/停止和 IPI 广播
src/hvpp/hvpp/vcpu.cppvcpu.asm 单个逻辑处理器的 VMXON/VMCS/VMLAUNCH/VMRESUME 主链
src/hvpp/hvpp/vmexit.* exit reason 到虚函数的一级分发和 handler 组合
src/hvpp/hvpp/vmexit/vmexit_passthrough.cpp 对被截获指令/事件的默认模拟
src/hvpp/hvpp/ept.* EPT 页表创建、映射、拆分与合并
src/hvppdrv/vmexit_custom.cpp CPUID、VMCALL 和 EPT 隐藏 hook 的项目级示例
src/hvppctrl/main.cpp 用户态如何触发并观察上述示例
src/hvppdrv_c/*src/hvpp/hvpp/hvpp.* C 接口如何桥接到 C++ 核心

以下判断不是仅凭目录名:

  • hvpp 是静态库:由 src/hvpp/hvpp.vcxprojConfigurationType=StaticLibrary 确认。
  • hvppdrvhvppdrv_c 是内核驱动:由各自工程的 ConfigurationType=Driver 确认。
  • hvppctrl 是用户态应用:由其 ConfigurationType=Application 确认。
  • hvpp-entry 只编译 lib/win32/driver.cpp:这解释了为什么示例项目里看不到 DriverEntry 定义,却仍然能形成驱动入口。
  • detours/udis86/ 带各自许可证,且只服务用户态示例;它们是第三方边界,不纳入重注释。

3. 先建立的基础概念

3.1 一个物理 CPU、逻辑处理器和 vcpu_t

项目中的一个 vcpu_t 对应一个 Windows 可调度的逻辑处理器,而不是一台完整虚拟机。

假设系统有 8 个逻辑处理器:

  • mp::cpu_count() 返回 8。
  • hypervisor::start() 分配并 placement-new 8 个 vcpu_t
  • mp::ipi_call() 让 8 个逻辑处理器各自在自己的执行上下文中调用 vcpu_t::start()
  • 每个 vcpu_t 有自己的 VMXON region、VMCS、host/guest 上下文、栈、pending interrupt 队列和 EPT 指针。
  • 示例的 vmexit_custom_handler 在每个 VCPU 的 setup() 中又创建一份 EPT,所以默认是每个逻辑处理器一套 EPT 对象。

3.2 VMX root / non-root 与 host / guest

四个词不要混成两组同义词:

  • VMX root:发生 VM-exit 后 hypervisor handler 执行的模式。
  • VMX non-root:被虚拟化的 Windows 正常运行的模式。
  • host state:VM-exit 时 CPU 从 VMCS 装载的状态,例如 host CR3/RSP/RIP。
  • guest state:VM-entry/VMRESUME 时 CPU 恢复的 Windows 状态。

本项目让 host 和 guest 共享大部分 Windows 内核地址空间,host CR3 使用 System 进程的长期有效页表。这使 handler 能访问内核地址,但不代表所有 Windows 内核 API 都能在 VM-exit 中安全调用:VM-exit 环境中断关闭,实际约束接近极高 IRQL。

3.3 VMCS 是什么

VMCS(Virtual-Machine Control Structure)不是普通 C++ 对象字段集合,而是处理器管理的一块 4KB、带 revision ID 的控制结构。代码通过 VMWRITE/VMREAD 访问其字段。

字段大致分为:

  • 控制字段:什么事件要 VM-exit、是否启用 EPT/VPID、entry/exit 模式等。
  • host-state:VM-exit 后加载的 CR3、RSP、RIP、段寄存器等。
  • guest-state:VM-entry 时恢复的 Windows 状态。
  • exit information:最近一次 VM-exit 的 reason、qualification、指令长度和相关地址。

vcpu.inl 中大量短 getter/setter 本质上是类型安全的 VMREAD/VMWRITE 封装。阅读时不必逐个背诵,先按“控制、entry、exit、guest、host”五组理解。

3.4 EPT 为什么是第二套页表

Windows 自己的普通页表完成:

guest virtual address -> guest physical address

EPT 再完成:

guest physical address -> host physical address

所以完整翻译是:

GVA --Windows CR3/page tables--> GPA --EPT--> HPA

初始 identity map 让 GPA == HPA。隐藏 hook 示例利用 EPT 把“同一个 GPA 的读取”和“同一个 GPA 的执行”切换到不同 HPA:

  • 读取时映射到保存了原始字节的页。
  • 执行时映射到包含 Detours 跳转的页。
  • 用户态反汇编看到原始函数,但 CPU 实际取指执行 hook 页。

4. 顶层目录地图

hvpp/
├─ README.md                         项目目标、构建/运行说明、示例预期
├─ hvpp.sln                          解决方案和工程依赖
├─ img/                              README 截图,不参与运行
└─ src/├─ hvpp/│  ├─ hvpp.vcxproj                核心 C++ 静态库│  ├─ hvpp-entry.vcxproj          仅提供真正的 Windows DriverEntry│  └─ hvpp/│     ├─ hypervisor.*             全局多处理器生命周期│     ├─ vcpu.* / vcpu.asm        单 VCPU 与 VMX 状态切换│     ├─ vmexit.*                 handler 抽象与一级分发│     ├─ vmexit/                  passthrough、统计、断点、C 桥│     ├─ ept.*                    EPT 高层对象│     ├─ hvpp.*                   公共 C API 及其 C++ 转发实现│     ├─ ia32/                    CPU/VMX/VMCS/分页位域与汇编封装│     └─ lib/                     内存、驱动、设备、日志、多核等运行支撑├─ hvppdrv/                       推荐先读的 C++ 示例驱动├─ hvppdrv_c/                     同一思想的 C 接口示例└─ hvppctrl/├─ main.cpp                    用户态演示入口├─ ia32/、lib/                 少量用户态辅助代码├─ detours/                    第三方 Microsoft Detours└─ udis86/                     第三方反汇编库

5. 阅读路线

阶段 1:先看“如何使用”,不要先钻位域

要解决的问题:

  • 驱动由谁初始化?
  • 自定义 handler 怎么接入?
  • 用户态示例怎样制造可观察的 VM-exit?

阅读顺序:

  1. README.md 的 Code workflow 与 stealth hooking 部分。
  2. src/hvppdrv/main.cpp
  3. src/hvppdrv/vmexit_custom.h/.cpp
  4. src/hvppdrv/device_custom.h/.cpp
  5. src/hvppctrl/main.cpp

这一阶段暂时跳过:

  • ia32/ 下大多数位域。
  • vmexit_passthrough.cpp 的每一种指令模拟。
  • detours/udis86/ 内部实现。

连接到下一阶段的关键符号:

driver::initialize-> vmexit_compositor_handler<stats, dbgbreak, custom>-> hypervisor::start

阶段 2:追真正的驱动装载与全局生命周期

要解决的问题:

  • 为什么 hvppdrv/main.cpp 没有 DriverEntry
  • 日志、内存描述符和 allocator 谁先初始化?
  • 出错与卸载如何逆序清理?

阅读顺序:

  1. src/hvpp/hvpp/lib/win32/driver.cpp
  2. src/hvpp/hvpp/lib/driver.h/.cpp
  3. src/hvpp/hvpp/lib/mm.h/.cpp
  4. src/hvpp/hvpp/lib/win32/mp.cpp

真实链路:

Windows I/O Manager-> DriverEntry                         lib/win32/driver.cpp-> driver::common::initialize-> logger::initialize-> mm::initialize-> system_allocator_default_initialize-> driver::initialize             hvppdrv/main.cpp-> device_custom::create-> new combined handler-> hypervisor::start-> 必要时创建默认 hypervisor allocator

注意:hypervisor::start() 自己也会兜底创建 hypervisor allocator。因此理解“通常路径”和“API 可独立使用的兜底路径”时要分开。

阶段 3:追每个逻辑处理器如何进入 VMX

要解决的问题:

  • 全处理器启动如何协调?
  • VMXON region、VMCS、host state、guest state 按什么顺序准备?
  • VMLAUNCH 成功后为什么看起来还能“返回”到 vcpu_t::start()

阅读顺序:

  1. src/hvpp/hvpp/hypervisor.h/.cpp
  2. src/hvpp/hvpp/vcpu.h
  3. src/hvpp/hvpp/vcpu.cpp
    • 构造函数
    • start()/stop()
    • vmx_enter()/vmx_leave()
    • load_vmxon()/load_vmcs()
    • setup_host()/setup_guest()
    • entry_host()/entry_guest()
  4. src/hvpp/hvpp/vcpu.asm
  5. src/hvpp/hvpp/ia32/context.asm

启动时序:

hypervisor::start(handler)-> 分配 vcpu_t[cpu_count]-> placement-new,每个 VCPU 引用同一个 handler 对象-> 检查 VT-x/EPT/INVEPT/2MB page 等能力-> KeIpiGenericCall-> 当前 CPU 对应 vcpu_t::start()-> launch_context_.capture()-> vmx_enter()-> load_vmxon()-> load_vmcs()-> setup_host()-> setup_guest()-> handler.setup(vp)-> VMLAUNCH-> guest RIP = entry_guest_-> entry_guest()-> launch_context_.restore()-> 回到 start() 的 state::launching 分支

这里的 capture()/restore() 具有类似 setjmp/longjmp 的“双次返回”效果:第一次 capture 返回 0,restore 后控制流回到 capture 位置,但这次返回保存到 RAX 的非零状态值。

阶段 4:追一次 VM-exit 的完整往返

要解决的问题:

  • CPU 保存了什么,汇编又补存了什么?
  • exit reason 如何找到具体虚函数?
  • handler 返回后,RIP 为什么通常自动前进?

阅读顺序:

  1. vcpu.asm::entry_host_
  2. vcpu.cpp::entry_host
  3. vmexit.cpp::vmexit_handler::handle
  4. vmexit.h::vmexit_compositor_handler
  5. vmexit_passthrough.cpp 中与当前目标有关的具体 handler
  6. vcpu.cpp::entry_host 的回程部分

一次典型 CPUID VM-exit:

guest 执行 CPUID-> CPU 按 VMCS 保存部分 guest state,装载 host CR3/RSP/RIP-> host RIP = vcpu_t::entry_host_-> context_t::capture 保存 GP 寄存器-> vcpu_t::entry_host-> 保存 FPU/SSE-> handler_.handle(vp)-> compositor 顺序调用 stats、dbgbreak、custom-> 各 handler 再按 exit_reason=CPUID 分发-> 默认 guest RIP += exit_instruction_length-> 把修改后的 RSP/RIP/RFLAGS 写回 VMCS-> context_.rip = vmx::vmresume-> context_t::restore-> 跳到 VMRESUME-> guest 从 CPUID 下一条指令继续

如果 handler 只是修复映射、注入 fault,要求原指令重试,则调用 suppress_rip_adjust();否则统一尾部会跳过触发 VM-exit 的原指令。

阶段 5:读 passthrough 模拟层

vmexit_passthrough_handler 的目标不是“忽略 VM-exit”,而是尽可能重现处理器在没有拦截时的可见效果。

建议按专题读:

  1. 生命周期:setup/terminate
  2. 异常/NMI:handle_interrupt*
  3. 简单指令:CPUID、RDTSC、RDTSCP、WBINVD。
  4. 控制/调试寄存器:MOV CR、MOV DR。
  5. I/O:IN/OUT、REP string I/O。
  6. MSR:RDMSR/WRMSR。
  7. 描述符指令:SGDT/SIDT/LGDT/LIDT、SLDT/STR/LLDT/LTR。
  8. INVPCID 和 VPID invalidation。
  9. VMX 指令统一注入 #UD,防止 guest 嵌套执行 VMX。
  10. 特殊模拟:SYSCALL/SYSRET/RDTSC/RDTSCP。

阅读这份文件时始终问三个问题:

  • 原指令本来对寄存器/内存/隐藏 CPU 状态有什么副作用?
  • VM-exit 已经替 CPU 做了哪些检查,handler 还要补哪些检查?
  • 当前路径要跳过原指令,还是注入异常并重试?

阶段 6:读 EPT 与隐藏 hook 示例

阅读顺序:

  1. ia32/ept.h:硬件位布局和统一 epte_t
  2. ept.h/.cpp:分配页表、映射、拆分、合并、销毁。
  3. hvppdrv/vmexit_custom.cpp::setup:创建每 VCPU EPT。
  4. handle_execute_vmcall:接收读页/执行页地址并拆分 2MB 页。
  5. handle_ept_violation:按访问类型在两个 HPA 间切换。
  6. hvppctrl/main.cpp:用户态如何准备两份页面内容。

关键状态:

per_vcpu_data
├─ ept       每个 VCPU 自己持有的 EPT 对象
├─ page_read 保存原始 ZwClose 字节所在 HPA
└─ page_exec 含 Detours 跳转、真正用于取指的 HPA

启动时先用 2MB identity mapping,是为了降低页表内存和 page walk 成本。要对单个 4KB 页做权限切换,必须先 split_2mb_to_4kb()。解除隐藏时再 join_4kb_to_2mb() 恢复粗粒度映射。

阶段 7:读 VM-exit 安全内存管理

阅读顺序:

  1. lib/mm.h/.cpp
  2. memory_allocator.h
  3. system_memory_allocator.*
  4. hypervisor_memory_allocator.*
  5. memory_mapper.*
  6. memory_translator.*
  7. paging_descriptor.*
  8. physical_memory_descriptor.*
  9. mtrr_descriptor.h

两种 allocator 的边界:

allocator 后端 适用环境
system allocator Windows pool API 普通内核路径、允许调用 OS 分配 API 时
hypervisor allocator 预留连续虚拟区上的页粒度 bitmap/map IPI/VM-exit 等不能安全调用普通分配 API 的路径

mm::allocator_guard 不是互斥锁。它是按当前逻辑处理器切换全局 new/delete 所使用 allocator 的 RAII guard。

generic_free() 不能只看当前 allocator,因为对象可能在 system allocator 下分配、在 hypervisor allocator 环境释放,反之亦然。它用 hypervisor_allocator()->contains(address) 判断归属。

阶段 8:最后读公共 C API 和大量硬件声明

建议先有 C++ 主链,再读:

  1. hvpp.h/.cpp
  2. vmexit_c_wrapper.h/.cpp
  3. hvppdrv_c/main.c
  4. hvppdrv_c/vmexit_custom.c
  5. ia32/vmx.hia32/vmx/vmcs.h
  6. ia32/vmx/exit_reason.h
  7. ia32/vmx/exit_qualification.h
  8. 需要时再查 arch/msr/paging.h

这样能避免在还不知道字段用途时陷入上千行位域定义。

6. 核心模块详解

6.1 driver::common:所有其他生命周期的外壳

lib/win32/driver.cpp::DriverEntry 保存 Windows 提供的 DRIVER_OBJECT,建立 IRP dispatch 表,记录驱动/内核地址范围,读取注册表中的 allocator 容量覆盖值,然后调用:

driver::common::initialize(&driver::initialize, &driver::destroy)

因此 src/hvppdrv/main.cpp 中的两个同名函数是被静态库入口回调的“项目自定义部分”,并非 Windows ABI 入口。

销毁是近似逆序:

DriverUnload-> driver::common::destroy-> driver::destroy-> hypervisor::stop-> dump/delete handlers-> delete device-> mm::destroy-> logger::destroy-> destroy hypervisor allocator-> destroy system allocator

6.2 hypervisor:全局协调器

hypervisor.cpp 只维护:

  • vcpu_list
  • running

它不处理任何具体 VM-exit。职责是:

  • 拒绝重复启动。
  • 按逻辑处理器数量构造 vcpu_t
  • 做一次硬件能力检查。
  • 用 IPI 让每个逻辑处理器启动/停止自己的 VMX operation。
  • 聚合第一个启动错误并触发全局回滚。

6.3 vcpu_t:单 CPU 的核心状态机

主要状态:

off -> initializing -> launching -> running -> terminating -> terminated

重要成员按职责分组:

  • context_:最近一次 VM-exit 保存下来的通用寄存器上下文。
  • launch_context_:跨 VMLAUNCH 完成启动握手。
  • resume_context_:让 handler 强制恢复 guest 时做非局部跳转。
  • vmxon_ / vmcs_:页对齐的硬件控制区。
  • stack_:每 VCPU 的 host/guest 切换栈,底部布局与 vcpu.asm 常量绑定。
  • handler_:外部提供的 VM-exit handler 引用。
  • ept_:当前 EPT 对象指针。
  • mapper_ / translator_:访问 guest 地址空间。
  • interrupt_queue_:guest 暂时不可注入中断时的待处理队列。
  • user_data_:handler 可挂接的每 VCPU 扩展状态。

6.4 vmexit_handler:一级分发表

基类构造函数建立一个 65 项成员函数指针表,数组下标直接对应 Intel exit reason 编号。

handle() 的核心只有:

const auto handler_index = static_cast<int>(vp.exit_reason());
(this->*handlers_[handler_index])(vp);

具体类通过 override 某个 handle_* 实现行为。未 override 的普通事件进入 handle_fallback;VMX 指令进入 handle_vm_fallback,便于 passthrough 层统一注入 #UD

6.5 compositor:不是责任链的“消费即停止”

示例组合:

vmexit_compositor_handler<vmexit_stats_handler,vmexit_dbgbreak_handler,vmexit_custom_handler
>

一次 VM-exit 会依次调用三个 handler 的 handle(),不会因为前一个“处理了”就停止。后面的 custom handler 继承 passthrough,最终负责维持 guest 语义。

这意味着:

  • stats 只观察和计数。
  • dbgbreak 只在配置的事件上触发一次调试断点。
  • custom/passthrough 修改 guest 上下文或 VMCS,决定真正行为。

6.6 vmexit_custom_handler:项目示例策略

它继承 vmexit_passthrough_handler,只覆盖三类 exit:

  • CPUID:当 EAX 为 'hvpp' 魔数时返回 "hello from hvpp";其他 CPUID 交回 passthrough。
  • VMCALL:RCX=0xC1 配置隐藏页,RCX=0xC2 解除隐藏;其他 VMCALL 交回 passthrough。
  • EPT violation:在 read/write HPA 与 execute HPA 之间切换映射,并抑制 RIP 自动前进,让原访存/取指重试。

6.7 C wrapper:显式保留“继续默认处理”的能力

C 回调不能调用 C++ protected 成员函数,所以 wrapper 创建一个临时 passthrough_context,里面同时保存:

  • wrapper 对象
  • 当前 vcpu_t
  • 当前 C++ handler 成员函数指针
  • 用户 context
  • 一个 C 可调用的 passthrough routine

C 侧收到的 Passthrough 不是普通用户数据,而是“调用默认 C++ 行为所需的闭包”。HvppPassthroughHandler(Passthrough) 最终回到对应 vmexit_passthrough_handler::handle_*

7. Public API 与实现映射

7.1 C++ API

调用者看到的 API 实现 作用
hypervisor::start/stop/is_running hypervisor.cpp 全逻辑处理器生命周期
vcpu_t::start/stop vcpu.cpp 当前逻辑处理器生命周期
vcpu_t VMCS getter/setter vcpu.inl 类型化 VMREAD/VMWRITE
vmexit_handler vmexit.h/.cpp VM-exit 分发抽象
vmexit_passthrough_handler vmexit/vmexit_passthrough.* 默认指令/事件模拟
ept_t ept.h/.cpp EPT 页表管理
mm::* lib/mm* descriptor 和 allocator 管理

7.2 C API

C API 组 hvpp.cpp 转发到
HvppInitialize/Destroy driver::common 与默认 allocator
HvppStart/Stop/IsRunning vmexit_c_wrapper_handler + hypervisor
HvppEpt* ept_t
HvppVcpu* vcpu_t
HvppVmRead/Write/Call* ia32::vmx 指令封装
HvppAllocate/Free 项目重载后的 new/delete
HvppAttach/DetachAddressSpace CR3 临时切换
HvppPassthrough* C wrapper 保存的 C++ 默认 handler

8. 初始化与生命周期

8.1 正常启动

DriverEntry-> 记录 DriverObject、内核范围、用户/系统地址边界-> common::initialize-> logger-> mm descriptors-> system allocator-> driver::initialize-> device-> handler compositor-> hypervisor::start-> VCPU objects-> hardware feature check-> per-CPU IPI start

8.2 部分 CPU 启动失败

每个 IPI callback 把自己的 vcpu_t::start() 错误尝试写入 atomic<error_code_t>,只保留第一个非零错误。完成所有 CPU callback 后先设置 global.running=true,再调用 stop() 回滚;这是因为 stop()running=false 会提前返回。

8.3 正常停止

vcpu_t::stop() 在 non-root 中不能直接执行 VMXOFF,因此调用 handler 的 terminate()。passthrough handler 执行带专用 ID 的 VMCALL

non-root stop()-> VMCALL-> root handle_execute_vmcall()-> vcpu_t::vmx_leave()-> 修正 guest RIP-> 恢复 GDTR/IDTR/guest CR3-> INVVPID + INVEPT-> VMXOFF-> 清 CR4.VMXE-> handler.teardown()

9. 关键调用链

9.1 IOCTL 设置一次性 I/O 端口断点

hvppctrl CreateFile("\\\\.\\hvpp")-> DeviceIoControl-> DriverDispatch(IRP_MJ_DEVICE_CONTROL)-> device_custom::on_ioctl-> ioctl_enable_io_debugbreak-> dbgbreak_handler.storage().io_in[port] = true-> dbgbreak_handler.storage().io_out[port] = true

下一次对应端口 I/O 引起 VM-exit 时,vmexit_dbgbreak_handler 用 atomic exchange 清掉标志并只断一次。

9.2 隐藏 Detours hook

用户态:构造 original page + hooked executable page-> VMCALL(0xC1, read_va, exec_va)内核:custom::handle_execute_vmcall-> 临时切到 guest CR3 获取两个 VA 的物理地址-> split 2MB -> 4KB-> GPA(page_exec) 映射 HPA(page_exec),execute-only-> INVEPT single-context读取触发 EPT violation:GPA(page_exec) -> HPA(page_read),RW-> suppress RIP adjust -> 原读取重试执行触发 EPT violation:GPA(page_exec) -> HPA(page_exec),X-only-> suppress RIP adjust -> 原取指重试

9.3 强制 guest resume

handler 可调用 vp.guest_resume(),它修改 resume_context_.rax 并 restore。控制流回到 entry_host()resume_context_.capture() 的另一分支:

  • 清理由 stacked_lock_guard 记录、但因非局部跳转没有正常析构的锁。
  • 调用 handle_guest_resume(vp, true)
  • 继续统一 VMRESUME 回程。

10. 关键数据结构

10.1 context_t

保存通用寄存器、RIP/RSP/RFLAGS,并提供 capture/restore。它既用于 VM-exit 保存 guest GP 寄存器,也用于启动/强制恢复的非局部控制流。

10.2 vmexit_storage_t<T>

它不是只按 65 个 exit reason 存储,还为高价值子类别展开:

  • 256 个 exception vector
  • 基本/扩展 CPUID leaf
  • 每个 CR/DR 编号及读写方向
  • 65536 个 I/O port 的 IN/OUT
  • 两段常用 MSR 地址空间的 RDMSR/WRMSR

T=uint32_t 时用于统计;T=atomic_bool 时用于一次性断点。

10.3 epte_t

8 字节 union,把 PML4E/PDPTE/PDE/PTE 的共同位叠在一起。前三位 R/W/X 任一置位即被项目视为 present。large_page 决定当前 entry 指向下级表还是直接映射 1GB/2MB 区域。

10.4 hypervisor_memory_allocator

预留区内部布局:

base
├─ page bitmap            每页是否占用
├─ page allocation map    一次分配连续占用了多少页
└─ usable pool            实际返回给 new/delete 的页

所有分配至少浪费一个 4KB 页,这是为 VM-exit 安全与实现简单做的取舍,不是通用小对象 allocator。

11. 底层机制专题

11.1 为什么 host CR3 不能直接用当前 CR3

启动 VCPU 的 IPI 可能打断任意进程。当前 CR3 属于该进程,它可能退出;将它长期写入 VMCS host CR3 会留下悬空地址空间。项目从 paging_descriptor 取得 System 进程 CR3,确保 host 侧内核映射长期存在。

11.2 为什么退出 VMX 前恢复 GDTR/IDTR

VM-exit 装载 host GDTR/IDTR 时,VMCS 只提供 base,不提供 limit;硬件使用固定 limit。若 VMXOFF 后直接返回 Windows,PatchGuard 可能把异常 limit 当成篡改。项目在 vmx_leave() 用保存的 guest GDTR/IDTR 完整恢复。

11.3 为什么 EPT 修改常常伴随 INVEPT

CPU 会缓存 EPT 翻译。只改内存里的 EPT entry 不保证下一次访问立刻重新 page walk。建立/解除隐藏时显式 INVEPT single-context。在处理“正是当前地址引起的 EPT violation”时,Intel 定义该 violation 会失效相关缓存,因此示例在 read/execute 切换分支中不再额外 INVEPT。

11.4 为什么默认 VMX 指令注入 #UD

项目不支持 nested virtualization。如果 guest 内的代码执行 VMXON/VMREAD 等,不能让它操作真实 hypervisor 的 VMCS,也不能假装成功。passthrough 的统一 VMX fallback 注入 invalid opcode,与“不向 guest 暴露 VMX 能力”的模型一致。

11.5 为什么 VM-exit 中不能随意调用内核 API

关键原因不只是“现在在内核态”,而是:

  • VM-exit 时外部中断通常关闭。
  • 当前执行不处在 Windows 调度器正常管理的调用环境。
  • 获取会等待的锁、触发 IPI/TLB shootdown、分页或普通 pool 分配都可能死锁或破坏假设。
  • 所以项目预分配内存、使用自有 spinlock/bitmap/deque,并把 TraceLogging 作为高频输出通道。

12. examples/tests 的阅读价值

仓库没有独立 tests/。三个可执行工程就是示例和集成验证材料:

  • hvppdrv:展示 C++ handler 组合、设备 IOCTL、EPT hook。
  • hvppdrv_c:展示同一机制如何通过稳定 C ABI 使用。
  • hvppctrl:展示用户态如何触发 CPUID/VMCALL/IOCTL 并观察 hook 隐藏。

因此这里不能把 examples 当成低优先级样板;它们是理解公共入口和真实副作用的主要证据。

13. 第三方与低价值边界

本轮不重注释:

  • src/hvppctrl/detours/:Microsoft Detours 第三方源码。
  • src/hvppctrl/udis86/:udis86 第三方源码。
  • *.vcxproj.user:本机调试设置。
  • *.vcxproj.filters:Visual Studio 视图分组。
  • img/:文档图片。
  • 大量简单 VMCS getter/setter:由 vcpu.h/.cpp 和本文按分组解释,不对每个一行 wrapper 重复长注释;原始非 UTF-8 的 vcpu.inl 保持不改。
  • 硬件位域中的每一个 reserved bit:保留原始布局,只补充文件/结构层级的阅读说明。

14. 核心文件注释计划

批次 1:入口和最小示例

  • src/hvppdrv/main.cpp
  • src/hvppdrv/device_custom.h/.cpp
  • src/hvppdrv/vmexit_custom.h/.cpp
  • src/hvppctrl/main.cpp
  • src/hvppdrv_c/main.c
  • src/hvppdrv_c/vmexit_custom.h/.c

批次 2:驱动壳与生命周期

  • src/hvpp/hvpp/lib/driver.h/.cpp
  • src/hvpp/hvpp/lib/win32/driver.cpp
  • src/hvpp/hvpp/lib/device.h
  • src/hvpp/hvpp/lib/win32/device.cpp
  • src/hvpp/hvpp/lib/mp.h
  • src/hvpp/hvpp/lib/win32/mp.cpp
  • src/hvpp/hvpp/hypervisor.h/.cpp

批次 3:VCPU 与上下文切换

  • src/hvpp/hvpp/vcpu.h/.cpp/.inl
  • src/hvpp/hvpp/vcpu.asm
  • src/hvpp/hvpp/ia32/context.asm
  • src/hvpp/hvpp/interrupt.h

批次 4:VM-exit 分发与默认模拟

  • src/hvpp/hvpp/vmexit.h/.cpp
  • src/hvpp/hvpp/vmexit/vmexit_passthrough.h/.cpp
  • src/hvpp/hvpp/vmexit/vmexit_stats.h/.cpp
  • src/hvpp/hvpp/vmexit/vmexit_dbgbreak.h/.cpp

批次 5:EPT 与地址类型

  • src/hvpp/hvpp/ept.h/.cpp
  • src/hvpp/hvpp/ia32/ept.h
  • src/hvpp/hvpp/ia32/memory.h/.cpp
  • src/hvpp/hvpp/ia32/paging.h

批次 6:内存运行时

  • src/hvpp/hvpp/lib/mm.h/.cpp
  • src/hvpp/hvpp/lib/mm/memory_allocator*
  • src/hvpp/hvpp/lib/mm/memory_mapper*
  • src/hvpp/hvpp/lib/mm/memory_translator*
  • src/hvpp/hvpp/lib/mm/*descriptor*

批次 7:C 接口与硬件索引

  • src/hvpp/hvpp/hvpp.h/.cpp
  • src/hvpp/hvpp/vmexit/vmexit_c_wrapper.h/.cpp
  • src/hvpp/hvpp/ia32/vmx.h
  • src/hvpp/hvpp/ia32/vmx/vmcs.h
  • src/hvpp/hvpp/ia32/vmx/exit_reason.h
  • src/hvpp/hvpp/ia32/vmx/exit_qualification.h

15. 已完成注释批次

已完成批次 1:入口和最小示例

  • src/hvppdrv/main.cpp
  • src/hvppdrv/device_custom.h/.cpp
  • src/hvppdrv/vmexit_custom.h/.cpp
  • src/hvppctrl/main.cpp
  • src/hvppdrv_c/main.c
  • src/hvppdrv_c/vmexit_custom.h/.c

本批确认并写入源码的重点:

  • hvppdrv/main.cpp 是由公共 DriverEntry 回调的策略入口,不是 Windows ABI 入口。
  • compositor 对同一次 VM-exit 依次执行 stats、dbgbreak、custom,不会自动短路。
  • device_custom 通过 IOCTL 修改组合 handler 内的一次性 I/O 断点 bitmap。
  • EPT custom data 是每 VCPU 一份,因此用户态必须在每个逻辑处理器分别 VMCALL。
  • stealth hook 的 page_read 是原始备份页,page_exec 是已被 Detours 修改的目标页。
  • EPT violation 只修复本次访问的映射并抑制 RIP 前进,让原指令重试。
  • C wrapper 的 Passthrough 是调用对应 C++ 默认行为的上下文,不是普通 buffer。

随后已自动处理公共 Windows 驱动入口、公共 lifecycle、device/IRP、多处理器封装和 hypervisor::start/stop

已完成批次 2:驱动壳与全局 lifecycle

  • src/hvpp/hvpp/lib/driver.h/.cpp
  • src/hvpp/hvpp/lib/win32/driver.cpp
  • src/hvpp/hvpp/lib/device.h
  • src/hvpp/hvpp/lib/win32/device.cpp
  • src/hvpp/hvpp/lib/mp.h
  • src/hvpp/hvpp/lib/win32/mp.cpp
  • src/hvpp/hvpp/hypervisor.h/.cpp

本批确认并写入源码的重点:

  • hvpp-entry 提供真正的 Windows ABI 入口;DeviceExtension 保存 device*,公共 dispatch 再转到派生类虚函数。
  • buffered I/O 的 SystemBuffer 只在当前同步 IRP 期间使用,框架没有异步 pending 语义。
  • common lifecycle 先构造 system allocator,再预留 hypervisor allocator backing memory;销毁时顺序相反。
  • mm::allocator_guard 是按 CPU 切换 new/delete 路由,不是互斥锁。
  • KeIpiGenericCall 是同步全 CPU 广播,callback 在 IPI_LEVEL 且必须由目标 CPU 自己操作其 VMX 状态。
  • hypervisor::start() 的 VCPU 数组使用原始分配 + placement-new;所有 VCPU 共享 handler 引用但硬件状态独立。
  • 启动错误记录第一个失败,等待广播完成后统一 stop() 回滚。

随后已自动处理 vcpu_t 的对象布局、状态机、VMXON/VMCS/VMLAUNCH、VM-exit 汇编入口和 context capture/restore。

已完成批次 3:VCPU 与上下文切换

  • src/hvpp/hvpp/vcpu.h/.cpp
  • src/hvpp/hvpp/vcpu.asm
  • src/hvpp/hvpp/ia32/context.asm
  • src/hvpp/hvpp/interrupt.h

本批确认并写入源码的重点:

  • 一个 vcpu_t 对应一个逻辑处理器;handler 是共享的非 owning 引用,VMXON/VMCS/stack 是每 CPU 独立状态。
  • stack_context_/launch_context_ 的邻接布局与 vcpu.asm 固定偏移共同构成内部 ABI。
  • capture/restore 用 IRETQ 实现类似 setjmp/longjmp 的双次返回,分别支持启动握手、VM-exit 保存和强制 guest resume。
  • VMX 启动顺序是 VMXON → VMCLEAR/VMPTRLD → host/guest state → handler setup → VMLAUNCH。
  • host CR3 使用 System 进程 CR3,避免任意被 IPI 打断进程退出后留下失效地址空间。
  • entry_host() 的伪 machine frame 只帮助 WinDbg unwind,不是 VMRESUME 的真实状态来源。
  • 默认统一前进 guest RIP;EPT violation、异常注入等未完成指令必须显式抑制。
  • stop 时仍处于 non-root,需要 handler 先制造 VMCALL,再在 root 执行 VMXOFF。

src/hvpp/hvpp/vcpu.inl 保持原样:原文件含非 UTF-8 字节,补丁工具拒绝安全改写。为避免整文件转码和破坏原始字节,本轮把其 VMCS 五组访问器与中断注入语义写入了 vcpu.h/.cpp 和本文,而没有强行重编码该文件。

随后已自动处理 VM-exit 一级分发表、compositor、passthrough 指令模拟、统计 handler 和一次性断点 handler。

已完成批次 4:VM-exit 分发与默认模拟

  • src/hvpp/hvpp/vmexit.h/.cpp
  • src/hvpp/hvpp/vmexit/vmexit_passthrough.h/.cpp
  • src/hvpp/hvpp/vmexit/vmexit_stats.h/.cpp
  • src/hvpp/hvpp/vmexit/vmexit_dbgbreak.h/.cpp

本批确认并写入源码的重点:

  • vmexit_handler::handle() 用 basic exit reason 直接索引 65 项成员函数指针表;reserved reason 也占槽位。
  • compositor 会按模板参数顺序依次执行每个 handler,同一次 exit 不会因前一层“已处理”而自动短路。
  • passthrough 的语义是补做被 VM-exit 截断的默认可见副作用,不是忽略事件;例如 CPUID 要重建输出寄存器,MOV CR/DR 要按 qualification 找到源/目标 GPR。
  • 中断注入、等待 interrupt window、EPT violation 修复等“原 guest 指令尚未完成”的路径必须抑制统一 RIP 前进,让原指令或事件条件重试。
  • terminate()teardown() 不等价:前者发生在 root mode 的主动停止 VMCALL 中,负责执行 VMXOFF;后者是退出后的普通资源清理回调。
  • stats storage 按 reason 及 CPUID/CR/DR/I/O/MSR 子键计数;dbgbreak storage 用 atomic exchange 实现“一次设置、一次触发、自动清除”。

已完成批次 5:EPT 与地址类型

  • src/hvpp/hvpp/ept.h/.cpp
  • src/hvpp/hvpp/ia32/ept.h
  • src/hvpp/hvpp/ia32/memory.h/.cpp
  • src/hvpp/hvpp/ia32/paging.h

本批确认并写入源码的重点:

  • ept_t 只负责 GPA→HPA;guest CR3 页表负责 GVA→GPA。两套四级表层级相似,但普通页表用 Present/User/XD,EPT 用独立 R/W/X。
  • 构造函数只准备 4KB 对齐根表和 EPTP;map_identity() 才用 2MB large page 覆盖前 512GB。
  • EPT 的 present 不是独立位,而是 R/W/X 至少一位为 1;全 0 可故意制造 EPT violation。
  • split_2mb_to_4kb() 先清掉 large PDE,再建立 PT 并填 512 个 4KB entry;stealth hook 因此能只控制目标 4KB 页。
  • join() 不验证 512 个小页是否连续、同权限、同缓存类型,而是按调用者参数释放旧下级表并重建一个 large mapping。
  • 所有页表索引都来自 GPA,只有叶 entry 的 PFN 来自 HPA;这正是“同一 GPA 切到不同 HPA”的实现基础。
  • map/split/join 不自动 INVEPT。修改正在使用的 EPT 后,调用者必须处理缓存失效;当前 EPT violation 的修复则利用处理器对相关翻译的失效保证。
  • va_t::pt_entry() 读取当前 CPU 的普通 CR3,遇到 non-present 或 1GB/2MB large page 会提前返回实际终止 entry。

已完成批次 6:内存运行时

  • src/hvpp/hvpp/lib/mm.h/.cpp
  • src/hvpp/hvpp/lib/mm/memory_allocator*
  • src/hvpp/hvpp/lib/mm/memory_mapper*
  • src/hvpp/hvpp/lib/mm/memory_translator*
  • src/hvpp/hvpp/lib/mm/*descriptor*

本批确认并写入源码的重点:

  • 项目全局 new/delete 按当前逻辑处理器的 allocator 槽路由;allocator_guard 只做 RAII 路由切换,不提供互斥。
  • system allocator 是 Windows NonPagedPool 适配层;hypervisor allocator 在启动时预留的 backing store 内只改本地 bitmap/map,避免 VM-exit 中进入可能引发 IPI/TLB shootdown 的 OS 分配路径。
  • hypervisor allocator 最小分配单位为 4KB,用 bitmap 记录占用、用 uint16_t allocation map 在首页记录连续页数;碎片会使“总空闲量够但连续页不够”的请求失败。
  • memory_mapper 预留一个 host VA/PTE 窗口,反复改 PTE.PFN 并 INVLPG,以逐页读写任意物理地址;它不是 EPT 映射,也不能并发共享同一实例。
  • memory_translator(va, cr3) 从显式 CR3 开始逐层读取物理页表,可翻译当前未加载的 guest/进程地址空间;read/write 每到新 4KB 页都重新翻译。
  • ignore_errors=true 时,读洞页填 0、写洞页跳过;默认模式返回第一个无法翻译的 VA。
  • paging descriptor 依赖 Windows self-map:旧版本使用固定 PTE/PDE/PPE/PXE base,Windows 10 RS1 以后扫描 ntoskrnl!.data 的 KDBG/PteBase 推导,属于版本敏感内部机制。
  • physical memory descriptor 只包含实际 RAM,不等于完整物理地址空间;MTRR descriptor 则为 EPT 叶 entry 提供 UC/WC/WT/WP/WB 缓存类型依据。

已完成批次 7:C ABI 与硬件索引

  • src/hvpp/hvpp/hvpp.h/.cpp
  • src/hvpp/hvpp/vmexit/vmexit_c_wrapper.h/.cpp
  • src/hvpp/hvpp/ia32/vmx.h
  • src/hvpp/hvpp/ia32/vmx/vmcs.h
  • src/hvpp/hvpp/ia32/vmx/exit_reason.h
  • src/hvpp/hvpp/ia32/vmx/exit_qualification.h

本批确认并写入源码的重点:

  • hvpp.h 是 C ABI 镜像:opaque PVCPU/PEPT 最终仍是 C++ 对象指针,硬件位域必须和 C++ 类型保持大小、对齐、字段顺序与枚举值一致。
  • VMEXIT_PASSTHROUGH 是 wrapper 在当前调用栈构造的临时闭包,不是普通 buffer,也不能在回调返回后缓存。
  • C handler 槽为 NULL 时自动走 C++ passthrough;槽非空时 wrapper 不会自动补做默认行为,C 回调需要显式调用 HvppPassthroughHandler
  • VMCS field enum 是 VMREAD/VMWRITE component encoding,不是 vmcs_t::data 偏移;应按 control、exit、guest、host 四组和字段宽度查询。
  • vmx::adjust() 用 capability/fixed-bit MSR 把软件期望控制位裁成当前 CPU 合法值;VMREAD/VMWRITE helper 本身不自动 adjust。
  • INVEPT、INVVPID、INVLPG 分别面向 EPT 派生缓存、带 VPID 的 guest 线性翻译、普通单线性地址 TLB,不能互换。
  • HvppVmRead/HvppVmWrite 丢弃 VMX 指令错误;C API 更适合简易调用,深入诊断仍应回到返回 vmx::error_code 的 C++ 层。
  • 当前 C lifecycle 的 c_exit_handler 在 Start 失败时不释放,Stop 删除后也不置空;重复 Start/Stop 不能当作已验证的可重入协议。

16. 未决问题和阅读时应主动验证的点

  1. 代码年代较早,工程文件混合 VS2017-era WDK 与 hvppctrl 的 v142 设置;本轮不编译,因此不把“当前工具链可直接构建”当作已验证事实。
  2. hvpp.h 的 C ABI 直接镜像多个 C++/Intel 位布局;跨编译器或不同 packing 配置时应额外验证 sizeof/offsetof
  3. vmexit_handler::handle() 直接用 exit reason 作数组下标,依赖硬件 reason 在支持范围内;阅读扩展逻辑时要考虑新 CPU reason。
  4. 默认 identity EPT 只覆盖前 512GB;超出范围或特殊 MMIO 的行为要由自定义 violation handler 补足。
  5. 示例隐页状态只有一组 page_read/page_exec,是教学用单 hook 模型,不是通用多 hook 管理器。
  6. 电源睡眠/休眠时退出 VMX 是 README 明示的已知缺口。
  7. paging_descriptor 通过 KDBG 和 PsInitialSystemProcess 内部布局发现 self-map/System CR3;新的 Windows 构建是否仍兼容必须实机验证。
  8. hypervisor_memory_allocator::attach() 的非页对齐输入分支值得用边界值单测;默认大块 NonPagedPool backing 通常已页对齐,未覆盖该风险。
  9. mtrr_descriptor_t::type() 对首个非 UC variable range 和 MTRR 全局禁用状态的处理需要对照 Intel precedence 规则再做测试;本轮只静态记录,不改行为。
  10. pa_t{0} 同时可表示物理地址 0 和“失败空值”,translator 的布尔判断会把它当失败;若确实需要访问物理页 0,应单独设计可区分的返回协议。

17. 一页式阅读检查清单

读完核心链后,你应该能不看 README 回答:

  • Windows 实际调用的 DriverEntry 在哪里?
  • hvppdrv/main.cpp::driver::initialize() 是由谁回调的?
  • 为什么每个逻辑处理器都需要自己的 vcpu_t、VMXON region 和 VMCS?
  • VMLAUNCH 成功后 vcpu_t::start() 如何得到成功返回?
  • 一次 VM-exit 中 CPU、汇编 stub 和 C++ 各保存哪些状态?
  • suppress_rip_adjust() 什么时候必须调用?
  • compositor 中三个 handler 是否会互相短路?
  • passthrough 为什么是“模拟默认语义”而不是“什么都不做”?
  • EPT 隐藏 hook 为什么要先把 2MB 页拆成 4KB?
  • 为什么读取和执行可以看到同一 GPA 的不同 HPA?
  • 为什么停止 VMX 要先制造一次 VMCALL?
  • 为什么 VM-exit 内的 new 不能直接走普通 Windows pool allocator?
  • C 回调中的 Passthrough 指针实际封装了什么?

能回答这些问题后,再查具体 VMCS 位域和单条指令模拟,阅读效率会高很多。