🌐虚拟化概述
📌 什么是虚拟化
虚拟化 (Virtualization) 是一种资源管理技术,将物理资源(CPU、内存、I/O 设备)抽象为多个逻辑资源,使每个逻辑资源看起来像独立的物理资源。
- 隔离性 — VM 之间互不干扰,一个 VM 崩溃不影响其他 VM
- 封装性 — VM 的完整状态可保存、恢复、迁移
- 硬件无关性 — VM 看到的硬件是 Hypervisor 虚拟的,可跨物理机迁移
- 资源共享 — 多个 VM 共享物理资源,提高利用率
🔑 关键术语
VMM / Hypervisor
虚拟机监视器,管理物理资源并为每个 VM 创建虚拟硬件抽象。是虚拟化的核心软件层。
Guest OS
运行在 VM 中的操作系统,认为自己运行在真实硬件上。不需要知道自己被虚拟化。
Host / Root
对于 Type-1 Hypervisor,指 Hypervisor 本身运行在最高特权级。对于 Type-2,指宿主 OS。
vCPU
虚拟 CPU,由 Hypervisor 通过时间片轮转在物理 CPU 上调度执行。
📏 Popek & Goldberg 虚拟化要求 (1974)
Popek 和 Goldberg 提出了一个系统可虚拟化的充分条件:
- 等价性 (Equivalence) — VM 上的程序行为与真实硬件上完全一致
- 资源控制 (Resource Control) — VMM 完全控制所有资源
- 效率性 (Efficiency) — 绝大多数指令直接在硬件上执行(不经过 VMM)
经典 x86 架构不满足可虚拟化条件(存在 17 条敏感但非特权的指令),这也是为什么需要 VT-x/AMD-V 硬件辅助。
🏗️ 虚拟化的三个层次
CPU 虚拟化
让多个 vCPU 分时复用物理 CPU。处理特权指令、中断、异常。最核心也最复杂。
内存虚拟化
每个 VM 看到连续的物理地址空间。Hypervisor 需要维护 GVA→GPA→HPA 两级映射。
I/O 虚拟化
让 VM 共享物理设备。可通过设备模拟、半虚拟化 (virtio)、设备直通等方式实现。
📐虚拟化分类
🔷 Type-1 (Bare-Metal) Hypervisor
Hypervisor 直接运行在物理硬件上,没有底层宿主 OS。性能最好,是企业级虚拟化的标准。
🔶 Type-2 (Hosted) Hypervisor
Hypervisor 作为应用程序运行在宿主 OS 上,依赖宿主 OS 管理硬件。
性能较差,因为每次 VM 访问硬件都要经过宿主 OS 内核。适合开发测试,不适合生产环境。
⚡ 全虚拟化 vs 半虚拟化 vs 硬件辅助
| 方式 | 原理 | Guest 修改 | 性能 |
|---|---|---|---|
| 全虚拟化 | 完全模拟硬件,Guest 无需修改 | 不需要 | 中(有开销) |
| 半虚拟化 | Guest 知道自己被虚拟化,使用 Hypercall | 需要修改 OS | 好 |
| 硬件辅助 | CPU 提供 VT-x/AMD-V 扩展 | 不需要 | 接近原生 |
📜虚拟化发展历史
⏳ 发展时间线
🔧x86 架构速览
💍 特权级 (Privilege Rings)
x86 有 4 个特权级(Ring 0-3),Ring 0 最高,Ring 3 最低:
VT-x 引入了 VMX Root(Hypervisor)和 VMX Non-Root(Guest)两种运行模式,本质上是比 Ring 0 更高的特权级。
📖 关键系统寄存器
| 寄存器 | 作用 | 虚拟化相关 |
|---|---|---|
CR0 | 控制寄存器 0:保护模式、分页等 | 控制 Guest 的分页开关 |
CR3 | 页表基址寄存器 | Guest CR3 → EPT 转换 |
CR4 | 控制寄存器 4:PAE、VMXE 等 | 必须设置 VMXE 才能进入 VMX |
IDTR | 中断描述符表基址 | Guest IDTR 由 Hypervisor 管理 |
GDTR | 全局描述符表基址 | Guest GDT 由 Hypervisor 管理 |
MSR | 型号特定寄存器 | STAR、LSTAR、EFER 等需虚拟化 |
RFLAGS | 标志寄存器 | IF 位控制中断,需虚拟化 |
🏗️ 地址空间
GVA (Guest Virtual Address)
Guest OS 中进程的虚拟地址。经过 Guest 页表转换为 GPA。
GPA (Guest Physical Address)
Guest OS 认为的"物理地址"。实际上是 Hypervisor 分配的虚拟物理地址。
HPA (Host Physical Address)
真实的物理地址。EPT 将 GPA 映射到 HPA。
地址转换链 GVA ──Guest 页表──→ GPA ──EPT──→ HPA ──→ 物理内存 没有 EPT 时(影子页表) GVA ──影子页表──→ HPA (Hypervisor 合并两级页表)
⚙️ 特权指令 vs 敏感指令
POPF, SGDT, PUSHF),它们在 Ring 3 执行时不触发异常但会泄露或修改特权状态。这违反了 Popek & Goldberg 的可虚拟化条件。
解决方案的演进:
- 软件方案:二进制翻译(VMware)、半虚拟化(Xen)
- 硬件方案:VT-x/AMD-V(2005+),让所有敏感指令都可陷入
💍特权级与 Ring
🔒 特权级的工作方式
当 CPU 执行一条指令时,会检查当前 CPL (Current Privilege Level):
- 特权指令(如
HLT,LIDT,MOV CRx):只在 Ring 0 执行,Ring 3 执行触发#GP异常 - IOPL 敏感指令(如
IN,OUT,CLI,STI):受 IOPL 字段控制 - 敏感但非特权指令(如
POPF,SGDT):Ring 3 不触发异常,但行为不同
; 特权指令 — Ring 3 执行会 #GP mov cr3, rax ; 写页表基址 — 只能在 Ring 0 hlt ; 停机 — 只能在 Ring 0 lidt [rax] ; 加载 IDT — 只能在 Ring 0 ; 敏感但非特权 — Ring 3 执行不异常! sgdt [rax] ; 存储 GDT 寄存器 — 泄露内核地址 popf ; 恢复 RFLAGS — 可能改变 IF 位
🎯 VT-x 如何解决这个问题
VT-x 引入了 VMX Root 和 VMX Non-Root 两种操作模式:
- VMX Root:Hypervisor 运行模式。可以执行 VMXON, VMCS 相关指令
- VMX Non-Root:Guest 运行模式。某些指令会触发 VM Exit 陷入 Hypervisor
两个模式都可以使用 Ring 0-3,但 VMX Non-Root 模式下,即使在 Ring 0 执行某些指令也会触发 VM Exit。这完美解决了敏感非特权指令的问题。
🪤陷入与模拟 (Trap & Emulate)
🔄 基本原理
当 Guest 执行特权指令时,CPU 陷入(trap)到 Hypervisor,Hypervisor 模拟(emulate)该指令的效果,然后返回 Guest 继续执行。
⚡ 性能问题
每次陷入/模拟的开销约 1000-3000 个时钟周期。如果 Guest 频繁执行特权指令(如操作系统启动时),性能会严重下降。
这就是为什么:
- VMware 发明了二进制翻译来减少陷入次数
- Xen 用半虚拟化让 Guest 主动配合
- Intel/AMD 推出 VT-x/AMD-V 让陷入更高效
🔄二进制翻译 (Binary Translation)
🧠 核心思想
不直接执行 Guest 代码,而是在执行前扫描并替换敏感指令为安全的等价代码。普通指令直接在硬件上执行(直接执行),只有敏感指令被翻译。
优点:大多数指令零开销直接执行。缺点:翻译本身有开销,需要维护翻译缓存(Translation Cache)。
⚙️VT-x / AMD-V 硬件辅助虚拟化
🏗️ VMX 操作模式
VT-x 引入两种 CPU 操作模式:
📋 VMCS (Virtual Machine Control Structure)
VMCS 是 VT-x 的核心数据结构,存储 VM 的所有控制信息。每个 vCPU 有一个 VMCS。
Guest-State Area
保存 Guest 的寄存器状态:CR0/3/4, RSP, RIP, RFLAGS, 段寄存器, GDTR, IDTR 等。VM Entry 时加载,VM Exit 时保存。
Host-State Area
保存 Hypervisor 的寄存器状态。VM Exit 时自动加载,让 CPU 回到 Hypervisor 上下文。
VM-Execution Fields
控制哪些操作会触发 VM Exit:I/O 位图、MSR 位图、Exception 位图、EPT 指针等。
VM-Exit/Entry Fields
控制 Exit/Entry 行为:Exit 原因、Exit Qualification、Entry 控制、Entry MSR 加载列表等。
🚪 VM Exit 触发条件
以下事件会导致 CPU 从 VMX Non-Root 退出到 VMX Root:
| 类别 | 触发条件 | Exit Reason |
|---|---|---|
| 指令 | CPUID, HLT, INVD, INVLPG | 33, 12, 28, 14 |
| 控制寄存器 | MOV CRx, LMSW, CLTS | 28-30 |
| I/O | IN, OUT, INS, OUTS | 按 I/O 位图 |
| 中断 | 外部中断、NMI、异常 | 0-32 |
| MSR | RDMSR, WRMSR | 31, 32 |
| EPT | EPT 违规、EPT 配置变更 | 48, 49 |
| 定时器 | VMX Preemption Timer 超时 | 52 |
🔑 关键 VMX 指令
; 启用 VMX VMXON [vmxon_region] ; 进入 VMX 操作模式 ; VMCS 操作 VMCLEAR [vmcs_region] ; 初始化 VMCS VMPTRLD [vmcs_region] ; 加载 VMCS 指针 (当前 VMCS) VMREAD field, reg ; 读取 VMCS 字段 VMWRITE reg, field ; 写入 VMCS 字段 ; VM 切换 VMLAUNCH ; 首次启动 Guest VMRESUME ; 恢复 Guest 执行 ; 退出 VMX VMXOFF ; 退出 VMX 操作模式
📋VMCS 结构详解
📐 VMCS 布局
⚙️ 关键 VMCS 字段 (VM-Execution Controls)
| 字段 | 作用 | 典型配置 |
|---|---|---|
Pin-Based Controls | 外部中断、NMI 虚拟化控制 | 启用外部中断退出 |
Proc-Based Controls | 控制哪些指令触发 VM Exit | HLT/CPUID/CR 访问退出 |
Proc-Based Controls 2 | EPT、VPID、RDTSCP 等 | 启用 EPT, VPID |
Exception Bitmap | 哪些异常触发 VM Exit | 通常 0 或特定位 |
I/O Bitmap A/B | 哪些 I/O 端口触发 VM Exit | 全 1 = 所有 I/O 退出 |
MSR Bitmap | 哪些 MSR 读写触发 VM Exit | 选择性退出 |
EPTP | EPT 页表指针 | 指向 EPT PML4 |
CR0/CR4 Guest-Host Mask | 哪些 CR 位由 Hypervisor 控制 | PG, PE 等 |
🚪VM Entry / VM Exit
➡️ VM Entry 流程
执行 VMLAUNCH 或 VMRESUME 时,CPU 自动执行:
- 检查 VMCS 是否有效,VMX 控制字段是否合法
- 从 VMCS Host-State Area 加载 Hypervisor 状态的"回退地址"(保存到内部寄存器)
- 从 VMCS Guest-State Area 加载 Guest 状态:CR0/3/4, RSP, RIP, RFLAGS, 段寄存器等
- 如果配置了 MSR 加载列表,从内存加载 MSR 值
- 如果有 Event Injection(注入中断/异常),模拟其发生
- 切换到 VMX Non-Root 模式,开始执行 Guest 代码
⬅️ VM Exit 流程
当触发退出条件时,CPU 自动执行:
- 将 Guest 状态保存到 VMCS Guest-State Area
- 将 VM Exit 信息写入 VMCS:Exit Reason, Exit Qualification 等
- 从 VMCS Host-State Area 加载 Hypervisor 状态
- 如果配置了 MSR 存储列表,保存 Guest MSR 到内存
- 切换到 VMX Root 模式,RIP 指向 VMCS 中的 Host RIP
- CPU 开始执行 Hypervisor 代码
🔄 完整的 Hypervisor 执行循环
/* Hypervisor 主循环 */ while (1) { // 1. VM Entry: 硬件自动加载 Guest 状态 vmresume(); // 或 vmlaunch() 首次启动 // 2. Guest 执行,直到触发 VM Exit // (硬件自动保存 Guest 状态,加载 Host 状态) // 3. 处理 VM Exit uint64_t reason = vmread(VMCS_EXIT_REASON); switch (reason) { case EXIT_REASON_CPUID: handle_cpuid(); break; case EXIT_REASON_HLT: handle_hlt(); break; case EXIT_REASON_IO_INSTRUCTION: handle_io(); break; case EXIT_REASON_EPT_VIOLATION: handle_ept_violation(); break; default: panic("Unhandled VM Exit: %lu", reason); } // 4. 更新 Guest RIP (跳过触发 Exit 的指令) advance_guest_rip(); // 5. 回到步骤 1,继续执行 Guest }
🗺️内存虚拟化概述
🎯 核心问题
每个 Guest OS 认为自己拥有从地址 0 开始的连续物理内存。但实际上:
- Guest 的"物理地址" (GPA) 不是真实物理地址 (HPA)
- Hypervisor 需要维护 GPA → HPA 的映射
- 同时 Guest 自己维护 GVA → GPA 的页表
🔧 三种实现方式
1. 影子页表 (Shadow Page Table)
Hypervisor 将两级页表合并为一级,直接映射 GVA→HPA。每次 Guest 修改页表都需要同步。开销大,已较少使用。
2. EPT/NPT (硬件辅助)
硬件自动完成两级转换。CPU 先查 Guest 页表得到 GPA,再查 EPT 得到 HPA。现代主流方案。
3. 半虚拟化页表
Guest 主动将页表更新通知给 Hypervisor。需要修改 Guest OS。Xen 早期使用。
👻影子页表 (Shadow Page Table)
🧠 工作原理
Hypervisor 为每个 Guest 维护一份影子页表,直接映射 GVA → HPA。Guest 修改自己的页表时,Hypervisor 拦截并更新影子页表。
/* 影子页表结构 */ Guest 页表: GVA 0x1000 → GPA 0x5000 EPT: GPA 0x5000 → HPA 0x8A000 影子页表: GVA 0x1000 → HPA 0x8A000 // 直接映射! /* 当 Guest 修改 CR3 (切换页表) */ VM Exit → Hypervisor 捕获 → 解析 Guest 新页表 → 构建新的影子页表 → 将影子页表加载到物理 CR3 → VM Resume
📊EPT / NPT (嵌套页表)
🏗️ EPT 结构
EPT (Extended Page Tables) 是 Intel 的硬件辅助内存虚拟化。它是一个由 Hypervisor 维护的页表,将 GPA 映射到 HPA。
地址转换过程(硬件自动完成):
/* 完整的 GVA → HPA 转换过程 */ 1. CPU 使用 Guest CR3 指向的 Guest 页表 2. 查 Guest 页表: GVA → GPA 3. 使用 EPTP 指向的 EPT 页表 4. 查 EPT: GPA → HPA 5. 访问真实物理内存 /* 每一级 Guest 页表项都要经过 EPT 转换 */ /* 最坏情况: 4级 Guest × 4级 EPT = 24 次内存访问 */ /* 实际: 用 Page Walk Cache 优化到 ~6 次 */
⚡ EPT 违规 (EPT Violation)
当 Guest 访问的 GPA 在 EPT 中没有有效映射时,触发 EPT Violation(VM Exit reason 48)。
常见场景:
- 首次访问:Hypervisor 需要分配物理页并建立 EPT 映射
- MMIO 区域:设备寄存器映射,需要模拟设备操作
- 内存回收:Hypervisor 撤销了映射(如 ballooning)
- 写保护:用于脏页跟踪(热迁移时)
📊 EPT 页表项权限
| 位 | 名称 | 含义 |
|---|---|---|
| 0 | Read | 允许读 |
| 1 | Write | 允许写 |
| 2 | Execute | 允许执行 |
| 3 | EPT Memory Type | 0=UC, 6=WB |
| 5 | Accessed | 硬件置位,表示被访问过 |
| 6 | Dirty | 硬件置位,表示被写过 |
| 7 | Suppress VE | 禁止 Virtualization Exception |
🏷️VPID 与 TLB 管理
❓ 没有 VPID 的问题
每次 VM Entry/Exit 时,TLB 中的地址映射变得无效(因为 Root/Non-Root 的映射不同)。必须刷新整个 TLB,性能损失巨大。
✅ VPID 的解决方案
VPID 为每个 VMX 操作上下文分配一个唯一的标识符。TLB 条目标记了它属于哪个 VPID,不同上下文的 TLB 条目可以共存。
- VPID = 0:VMX Root 操作(Hypervisor)
- VPID = 1, 2, 3...:每个 vCPU 有唯一 VPID
- VM Entry/Exit 时不需要刷新 TLB
📌内存分配策略
📊 分配策略
静态分配
启动时为每个 VM 分配固定大小内存。简单但不灵活,可能导致内存浪费。
Memory Ballooning
Guest 中运行 balloon 驱动,主动释放不需要的内存回 Hypervisor。VMware 和 KVM 都支持。
内存超售 (Overcommit)
分配给所有 VM 的总内存超过物理内存。依赖不同时使用全部内存的假设。可能导致 swap。
KSM (Kernel Same-page Merging)
扫描相同内容的内存页,合并为一份(COW)。在相同 OS 的 VM 间效果显著。
🔌I/O 虚拟化概述
🔀 三种 I/O 虚拟化方式
🖥️设备模拟 (Device Emulation)
🔄 工作流程
Guest 执行 I/O 指令(IN/OUT)时,触发 VM Exit。Hypervisor 根据 I/O 端口和方向模拟设备行为:
/* 处理 Guest 的 IN 指令 */ void handle_io_exit(vmexit_info_t *exit) { uint16_t port = exit->port; // I/O 端口号 uint32_t size = exit->size; // 1/2/4 字节 bool is_in = exit->direction; // IN=true, OUT=false if (is_in) { // 从模拟设备读取数据 uint32_t val = emu_device_read(port, size); // 写入 Guest 的 EAX 寄存器 set_guest_reg(REG_RAX, val); } else { // 从 Guest 的 EAX 读取数据 uint32_t val = get_guest_reg(REG_RAX); // 写入模拟设备 emu_device_write(port, val, size); } }
📦 常见模拟设备
| 设备 | 类型 | 说明 |
|---|---|---|
i440FX | 芯片组 | 模拟传统 PC 芯片组(QEMU 默认) |
PIIX3 IDE | 磁盘 | 传统 IDE 控制器,兼容性好 |
e1000 | 网卡 | Intel 千兆网卡,多数 OS 有驱动 |
Cirrus VGA | 显卡 | 基础 VGA 显示 |
AC97 | 声卡 | 音频设备 |
RTC | 时钟 | 实时时钟 (CMOS) |
⚡VirtIO 半虚拟化驱动
🧠 核心思想
VirtIO 让 Guest 知道自己被虚拟化,使用共享内存 + 事件通知的高效通信机制,避免每次 I/O 都触发 VM Exit。
📦 VirtIO 设备类型
| 设备 | 说明 | 性能 |
|---|---|---|
virtio-net | 网络设备 | ~10 Gbps+ |
virtio-blk | 块设备 (磁盘) | 接近原生 SSD |
virtio-scsi | SCSI 控制器 | 支持多设备 |
virtio-gpu | GPU 设备 | 2D/3D 加速 |
virtio-fs | 文件系统共享 | 高性能共享目录 |
vhost-net | 内核态 VirtIO 后端 | 比用户态快 2-3x |
🔗设备直通 (Device Passthrough)
🛡️ IOMMU 的作用
IOMMU (Intel VT-d / AMD-Vi) 是 DMA 设备和物理内存之间的地址转换单元,类似于 CPU 的 MMU。
- DMA 地址转换:设备使用 GPA,IOMMU 转换为 HPA
- 隔离保护:防止设备 DMA 访问其他 VM 的内存
- 中断重映射:设备中断路由到正确的 VM
/* IOMMU 地址转换 */ 设备发出 DMA 请求: DMA addr = 0x1000 (GPA) ↓ IOMMU 查设备页表: GPA 0x1000 → HPA 0x8A000 ↓ 物理内存访问: HPA 0x8A000
🧩SR-IOV
🧠 核心思想
SR-IOV 让一个物理设备在硬件层面呈现为多个独立的虚拟设备(VF, Virtual Function)。每个 VF 可以直通给不同的 VM。
PF (Physical Function)
完整的 PCIe 功能,由 Hypervisor 管理。负责创建和管理 VF。
VF (Virtual Function)
轻量级 PCIe 功能,直通给 VM。每个 VF 有独立的 BAR 空间和 MSI-X 中断。
典型场景:一张 10GbE 网卡可以虚拟出 64 个 VF,每个 VF 分配给一个 VM,性能接近原生。
🔔中断虚拟化概述
📡 中断虚拟化的层次
外部中断
物理设备中断。Hypervisor 拦截后注入到对应的 VM。通过虚拟 LAPIC 投递。
虚拟中断
模拟设备(如 virtio)产生的中断。Hypervisor 直接注入到 Guest 的虚拟 LAPIC。
异常
Guest 内部的 CPU 异常(如缺页)。可以由 Guest 直接处理,或被 Hypervisor 拦截。
📡虚拟 APIC
🏗️ APIC 虚拟化
每个 CPU 核心有一个 LAPIC(Local APIC),负责接收和投递中断。Hypervisor 需要为每个 vCPU 虚拟一个 LAPIC。
- Guest 写 LAPIC 寄存器 → 可能触发 VM Exit
- Hypervisor 维护虚拟 LAPIC 状态
- 注入中断时,设置虚拟 LAPIC 的 IRR(中断请求寄存器)
- VM Entry 时,硬件自动检查虚拟 LAPIC 并注入中断
📬Posted Interrupts
🚀 原理
传统中断注入需要 VM Exit → 注入 → VM Entry,开销大。Posted Interrupts 允许直接将中断"投递"到正在运行的 vCPU,无需 VM Exit。
- 内存中维护一个 Posted Interrupt Descriptor
- 发送方(如设备驱动)直接写入描述符的 PIR(Posted Interrupt Request)位图
- CPU 在 VMX Non-Root 模式下检查描述符,自动注入中断
- 仅在目标 vCPU 不在运行时才需要 VM Exit
🏗️Hypervisor 架构设计
📐 设计模式
宏内核 (Monolithic)
所有功能在一个地址空间。性能好但 bug 可能影响所有 VM。如 VMware ESXi。
微内核 (Microkernel)
最小内核 + 用户态服务。隔离好但 IPC 开销大。如 L4, Bao, seL4。
混合型 (Hybrid)
核心功能在内核,设备驱动可分离。如 Xen (Dom0 驱动), KVM (Linux 内核)。
🧱 典型模块
Type-1 Hypervisor 组件: ├── 启动模块 // 从 bootloader 接管硬件 ├── VMX 管理 // VMXON/VMCS 初始化 ├── vCPU 管理 // vCPU 创建、调度、上下文切换 ├── 内存管理 // 物理页分配、EPT 管理 ├── VM Exit 处理 // 各类 Exit 的处理逻辑 ├── 中断管理 // 虚拟 APIC、中断注入 ├── I/O 子系统 // 设备模拟、virtio 后端 ├── 调度器 // vCPU 在物理 CPU 上的调度 ├── Hypercall // Guest-Hypervisor 通信接口 └── 调试/日志 // 调试支持
🚀Hypervisor 启动流程
📋 启动步骤
⏱️VM 调度
🔄 调度模型
Work-Conserving
物理 CPU 空闲时,立即运行就绪的 vCPU。不浪费 CPU 时间。大多数 Hypervisor 使用此模型。
Non-Work-Conserving
即使 CPU 空闲,也不立即运行 vCPU。用于严格的时间隔离和实时 VM。
Co-Scheduling
同一 VM 的多个 vCPU 尽量同时运行。减少锁竞争,但可能浪费 CPU。
📞Hypercall
📡 机制
Hypercall 是 Guest 主动请求 Hypervisor 服务的机制,类似系统调用:
/* x86 上的 Hypercall 实现 */ /* 方式 1: VMCALL 指令 (Intel) */ /* 方式 2: VMMCALL 指令 (AMD) */ /* 方式 3: 特定 MSR 写入 */ /* Guest 中发起 Hypercall */ static inline long hypercall(int nr, long a0, long a1) { long ret; asm volatile( "vmcall" : "=a"(ret) : "a"(nr), "D"(a0), "S"(a1) : "memory" ); return ret; } /* Hypervisor 中处理 */ case EXIT_REASON_VMCALL: int nr = get_guest_reg(REG_RAX); switch (nr) { case HYPERCALL_PUTCHAR: uart_putc(get_guest_reg(REG_RDI)); break; case HYPERCALL_SHUTDOWN: vm_shutdown(); break; }
📦热迁移 (Live Migration)
🔄 迁移流程
🔐VM 逃逸与防御
⚠️ 常见攻击面
Hypervisor Bug
VM Exit 处理中的漏洞。如 QEMU 设备模拟的缓冲区溢出。
共享组件漏洞
共享的虚拟设备(virtio)中的 bug。如 virtio-net 的越界读写。
侧信道攻击
利用共享硬件(缓存、分支预测器)泄露信息。如 Spectre, Meltdown。
🕵️侧信道攻击
🎯 典型攻击
| 攻击 | 利用机制 | 防御 |
|---|---|---|
| Spectre | 分支预测 + 缓存时序 | retpoline, IBRS |
| Meltdown | 乱序执行 + 缓存时序 | KPTI, 硬件修复 |
| L1TF | L1 缓存推测加载 | L1D flush on VM entry |
| MDS | 微架构数据采样 | MD_CLEAR, 调度隔离 |
🛡️TDX / SEV 机密计算
🔒 核心目标
即使 Hypervisor 被攻破,Guest 的数据仍然安全。Hypervisor 不可信。
Intel TDX
Trust Domain Extensions。CPU 硬件加密 Guest 内存。Hypervisor 无法读取 TD 内存内容。
AMD SEV-SNP
Secure Encrypted Virtualization。内存加密 + 完整性保护。防止 Hypervisor 篡改。
ARM CCA
Confidential Compute Architecture。ARM 的机密计算方案。Realm VM 概念。
💻开发环境搭建
🛠️ 工具链
# 交叉编译工具链 apt install gcc-x86-64-linux-gnu nasm qemu-system-x86 # 或使用 LLVM/Clang apt install clang lld # 调试工具 apt install gdb # 源码级调试 apt install bochs # Bochs 模拟器 (支持 VMX 调试) # QEMU (带 VT-x 透传) qemu-system-x86_64 \ -cpu host,+vmx \ # 透传 VMX 给 Guest -enable-kvm \ -m 512M \ -kernel hypervisor.bin
📁 项目结构
my-hypervisor/ ├── boot/ │ ├── boot.S # 汇编入口,设置栈、GDT、IDT │ └── linker.ld # 链接脚本 ├── core/ │ ├── vmx.c # VMX 初始化 (VMXON/VMXOFF) │ ├── vmcs.c # VMCS 配置和读写 │ ├── vcpu.c # vCPU 创建和管理 │ └── vmexit.c # VM Exit 处理 ├── mem/ │ ├── alloc.c # 物理页分配器 │ ├── ept.c # EPT 页表管理 │ └── mmio.c # MMIO 处理 ├── io/ │ ├── uart.c # 串口输出 (调试) │ ├── ioport.c # I/O 端口处理 │ └── virtio.c # VirtIO 后端 (可选) ├── intr/ │ ├── apic.c # 虚拟 APIC │ └── idt.c # IDT 设置 ├── guest/ │ ├── guest.S # 简单的 Guest 代码 │ └── guest.bin # Guest 二进制镜像 ├── Makefile └── run.sh # QEMU 启动脚本
⚙️VMX 初始化
📋 初始化步骤
/* 1. 检查 CPU 是否支持 VMX */ uint32_t eax, ebx, ecx, edx; cpuid(1, &eax, &ebx, &ecx, &edx); if (!(ecx & CPUID_VMX)) { panic("VT-x not supported"); } /* 2. 检查 MSR 是否启用 VMX outside SMX */ uint64_t feature = rdmsr(IA32_FEATURE_CONTROL); if (!(feature & FEATURE_CONTROL_VMXON_OUTSIDE_SMX)) { // 需要在 BIOS 中启用 VT-x panic("VT-x locked by BIOS"); } /* 3. 设置 CR4.VMXE = 1 */ uint64_t cr4 = readcr4(); writecr4(cr4 | CR4_VMXE); /* 4. 分配 VMXON region (4KB, 物理地址 4KB 对齐) */ uint64_t vmxon_pa = alloc_page(); // 物理页 uint32_t *vmxon = (uint32_t *)phys_to_virt(vmxon_pa); vmxon[0] = rdmsr(IA32_VMX_BASIC) & 0x7FFFFFFF; // Revision ID /* 5. 执行 VMXON */ uint64_t ret = vmxon(vmxon_pa); if (ret != 0) { panic("VMXON failed: %lu", ret); } // CPU 现在处于 VMX Root 操作模式!
⚠️ 关键注意点
VMXOFF。否则 CPU 会一直处于 VMX 模式,可能导致系统不稳定。
📋VMCS 配置
🔧 VMCS 初始化代码
/* 1. 分配 VMCS region (4KB, 4字节对齐) */ uint64_t vmcs_pa = alloc_page(); uint32_t *vmcs = (uint32_t *)phys_to_virt(vmcs_pa); vmcs[0] = rdmsr(IA32_VMX_BASIC) & 0x7FFFFFFF; /* 2. 清除 VMCS */ vmclear(vmcs_pa); /* 3. 加载 VMCS */ vmptrld(vmcs_pa); /* 4. 配置 Host State (VM Exit 后 CPU 回到的状态) */ vmwrite(VMCS_HOST_CS, read_cs()); vmwrite(VMCS_HOST_SS, read_ss()); vmwrite(VMCS_HOST_DS, read_ds()); vmwrite(VMCS_HOST_ES, read_es()); vmwrite(VMCS_HOST_FS, read_fs()); vmwrite(VMCS_HOST_GS, read_gs()); vmwrite(VMCS_HOST_TR, read_tr()); vmwrite(VMCS_HOST_GDTR, read_gdtr()); vmwrite(VMCS_HOST_IDTR, read_idtr()); vmwrite(VMCS_HOST_CR0, readcr0()); vmwrite(VMCS_HOST_CR3, readcr3()); vmwrite(VMCS_HOST_CR4, readcr4()); vmwrite(VMCS_HOST_RSP, (uint64_t)get_stack_top()); vmwrite(VMCS_HOST_RIP, (uint64_t)vmexit_handler); /* 5. 配置 Guest State (VM Entry 后 Guest 看到的状态) */ vmwrite(VMCS_GUEST_CS, GUEST_CS_SELECTOR); vmwrite(VMCS_GUEST_SS, GUEST_SS_SELECTOR); vmwrite(VMCS_GUEST_DS, GUEST_DS_SELECTOR); vmwrite(VMCS_GUEST_ES, GUEST_ES_SELECTOR); vmwrite(VMCS_GUEST_CR0, CR0_NE | CR0_ET | CR0_PE); vmwrite(VMCS_GUEST_CR3, guest_cr3); // Guest 页表 vmwrite(VMCS_GUEST_CR4, CR4_VMXE); vmwrite(VMCS_GUEST_RIP, GUEST_ENTRY_POINT); vmwrite(VMCS_GUEST_RSP, GUEST_STACK_TOP); vmwrite(VMCS_GUEST_RFLAGS, 0x2); // 固定为 1 的位 /* 6. 配置 VMX 控制字段 */ vmwrite(VMCS_PIN_BASED, PIN_EXT_INT); vmwrite(VMCS_PROC_BASED, CPU_HLT | CPU_CR3_LOAD | CPU_CR3_STORE | CPU_IO | CPU_CPUID | CPU_SECONDARY); vmwrite(VMCS_PROC_BASED2, CPU2_EPT | CPU2_VPID); vmwrite(VMCS_EPTP, make_eptp(ept_pml4_pa)); vmwrite(VMCS_VPID, 1); // 每个 vCPU 唯一
🎯Guest 启动
🚀 启动代码
/* 准备 Guest 镜像 */ // 将 guest.bin 加载到 Guest 的物理地址空间 load_guest_image("guest.bin", GUEST_LOAD_ADDR); /* 建立 EPT 映射 */ // GPA 0x0 ~ 0x100000 → HPA (分配的物理页) ept_map(ept_root, 0x0, va_to_pa(guest_mem), 0x100000, EPT_READ | EPT_WRITE | EPT_EXEC); /* VMLAUNCH */ uint64_t ret = vmlaunch(); if (ret != 0) { // VMLAUNCH 失败,检查 VMCS 错误 uint64_t err = vmread(VMCS_VM_INSTRUCTION_ERROR); panic("VMLAUNCH failed: error %lu", err); } // 如果成功,CPU 现在在执行 Guest 代码 // 不会从这里返回 —— 直到 VM Exit 才回到 vmexit_handler
VMLAUNCH 只能用一次(首次启动)。之后每次 VM Exit 后都用 VMRESUME 恢复 Guest。如果用错了会触发 VM Fail。
🔄Exit 处理循环
🔧 常见 Exit 处理
/* CPUID 处理 — 最简单的 Exit */ void handle_cpuid() { uint32_t leaf = get_guest_reg(REG_RAX); uint32_t sub = get_guest_reg(REG_RCX); uint32_t eax, ebx, ecx, edx; cpuid_count(leaf, sub, &eax, &ebx, &ecx, &edx); // 可以修改返回值来隐藏某些特性 set_guest_reg(REG_RAX, eax); set_guest_reg(REG_RBX, ebx); set_guest_reg(REG_RCX, ecx); set_guest_reg(REG_RDX, edx); } /* HLT 处理 — 暂停 vCPU */ void handle_hlt() { // Guest 执行了 HLT // 1. 标记 vCPU 为 halted current_vcpu->halted = true; // 2. 等待中断唤醒 // 3. 调度器可以运行其他 vCPU } /* MSR 处理 */ void handle_msr(bool is_write) { uint32_t msr = get_guest_reg(REG_RCX); if (is_write) { uint64_t val = get_guest_reg(REG_RAX) | (get_guest_reg(REG_RDX) << 32); // 处理或记录 MSR 写入 vcpu_set_msr(current_vcpu, msr, val); } else { uint64_t val = vcpu_get_msr(current_vcpu, msr); set_guest_reg(REG_RAX, val & 0xFFFFFFFF); set_guest_reg(REG_RDX, val >> 32); } }
🗺️EPT 内存映射
📊 EPT 页表构建
/* EPT 页表项结构 */ typedef struct { uint64_t read : 1; // 位 0: 允许读 uint64_t write : 1; // 位 1: 允许写 uint64_t execute : 1; // 位 2: 允许执行 uint64_t mem_type : 3; // 位 3-5: 内存类型 (0=UC, 6=WB) uint64_t accessed : 1; // 位 5: 访问位 (硬件设置) uint64_t dirty : 1; // 位 6: 脏位 (硬件设置) uint64_t rsvd : 1; // 位 7 uint64_t addr : 40; // 位 12-51: 物理地址 uint64_t rsvd2 : 12; // 位 52-63 } ept_entry_t; /* 建立 EPT 映射 */ void ept_map(uint64_t *pml4, uint64_t gpa, uint64_t hpa, uint64_t size, uint64_t flags) { // 4KB 粒度映射 for (uint64_t offset = 0; offset < size; offset += 0x1000) { uint64_t g = gpa + offset; uint64_t h = hpa + offset; // 查找或创建页表项 ept_entry_t *entry = ept_walk_create(pml4, g); entry->addr = h >> 12; entry->read = !!(flags & EPT_READ); entry->write = !!(flags & EPT_WRITE); entry->execute = !!(flags & EPT_EXEC); entry->mem_type = 6; // Write-Back } }
🔌I/O 处理
🔧 I/O Exit 处理
void handle_io_exit() { // Exit Qualification 包含 I/O 信息 uint64_t qual = vmread(VMCS_EXIT_QUALIFICATION); uint32_t size = (qual & 0x7) + 1; // 1/2/4 字节 bool is_in = !!(qual & 0x8); // IN=true bool is_str = !!(qual & 0x10); // 字符串 I/O uint16_t port = (qual >> 16) & 0xFFFF; // 端口号 if (is_in) { uint32_t val = ioport_read(port, size); set_guest_reg(REG_RAX, val); } else { uint32_t val = get_guest_reg(REG_RAX); ioport_write(port, val, size); } }
🔔中断处理
📡 中断注入
/* 注入中断到 Guest */ void inject_interrupt(uint8_t vector) { uint32_t info = 0; info |= vector; // 中断向量号 info |= (0 << 8); // 类型: 外部中断 info |= (1 << 31); // Valid: 有效 vmwrite(VMCS_ENTRY_INTR_INFO, info); // VM Entry 时硬件自动注入此中断 }
🐛调试技巧
🛠️ 调试方法
串口输出 (UART)
最可靠的调试方式。直接写 I/O 端口 0x3F8。QEMU 会输出到终端。不受 VM Exit 影响。
GDB + QEMU
qemu -s -S 启动后用 gdb target remote :1234 连接。可调试 Hypervisor 代码。
VMCS Dump
出错时 dump 所有 VMCS 字段。检查 Exit Reason、Exit Qualification、Guest RIP。
Bochs
Bochs 模拟器支持 VMX 指令模拟。适合在不支持 VT-x 的机器上开发。
📚参考项目
📖 推荐项目
Bao Hypervisor
轻量级 Type-1 Hypervisor,代码简洁,适合学习。支持 ARM 和 RISC-V。GitHub: bao-project/bao-hypervisor
KVM (Linux 内核)
最成熟的开源虚拟化。代码在内核源码 arch/x86/kvm/。复杂但功能完整。
Xen
经典的 Type-1 Hypervisor。代码结构清晰,文档丰富。适合学习虚拟化架构。
SimpleSVM (教学)
极简的 AMD-V 教学 Hypervisor。只有几百行代码,展示核心概念。
seL4
经过形式化验证的微内核。支持虚拟化扩展。适合学习安全 Hypervisor 设计。
Intel SDM
Intel 手册 Vol 3 Chapter 23-33。VMX 的权威参考。必读!