中国科学院大学 · 高级操作系统(315371)· 期中报告

虚拟化技术
从软件翻译到嵌套组合

同一条主线上的四次回答:指令由谁执行、地址由谁翻译、事件由谁投递、代价落在哪一段路径
报告人 王靳 2026E8013282153|2026 年 11 月
Roadmap

报告主线与统一框架

每一部分只回答同一组问题:资源与状态由谁控制、关键路径怎样执行、何时需要 hypervisor 介入、介入的代价何在。四个维度(CPU / 内存 / 中断 / 设备)是本文的工程分析框架,不是 Popek–Goldberg 原文的分类——原文的形式模型明确排除了 I/O 与中断。

00 判据P&G 三性质
敏感 ⊆ 特权
→
01 软件QEMU TCG
RISC-V → IR → x86-64
→
02 硬件Armv8.6-A + KVM
EL2 / S2 / GICv4 / SMMUv3
→
03 嵌套(一)Turtles
把多层语义做对
→
04 嵌套(二)HyperTurtle
把跨层往返削掉

为什么并置软件与硬件不是两代产品

二者是实现同一目标的两条路径,且长期共存:QEMU 既能独立提供整机模型,也能把 CPU 执行交给 KVM,自己保留内存后备、设备模型与整机编排。比较的是同一维度上两种机制的取舍,不是替代关系。

为什么分两章嵌套的判据不同

第 3 章问「缺乏架构支持时怎样把多层语义做对」,第 4 章问「已经能嵌套后怎样削减跨层往返」。两问先后相接,性能不可横向排名。

Related Work · 技术演进路径

五十年的四个转折从形式判据到跨层优化

1974 Popek & Goldberg CACM 17(7) 三条性质定义 VMM;给出「敏感 ⊆ 特权」这一可判定的充分条件,把「能不能虚拟化」变成形式问题。
2005 Bellard · QEMU USENIX ATC · FREENIX 动态翻译把 guest 指令变成宿主代码,跨 ISA 整机模拟成为可移植的工程方案(当时用 dyngen,非今日 TCG)。
2010 Ben-Yehuda 等 · Turtles OSDI '10 在无嵌套架构支持时,用软件合成 VMCS、折叠页表与多级设备分配,把多层语义压到单层硬件上。
2013 Intel Haswell VMCS shadowing 硬件给出 VMCS 影子副本,使 L0 不必在 L1 每次 vmread/vmwrite 时都陷入——第一次把「敏感」与「必须陷入」解耦。
2014 Dall & Nieh · KVM/ARM ASPLOS '14 把 hypervisor 放进 EL2、用两阶段翻译与 split-mode 组织 Arm 侧虚拟化;VHE 等后续工作延续这一分层。
2025 Ben Zur 等 · HyperTurtle USENIX ATC '25 以受验证的 eBPF hyperupcall 把 L1 的处理逻辑下放到 L0 就地执行,缩短嵌套的关键路径。

演进的分水岭有两条:一条是「谁执行 guest 指令」(翻译 → 原生执行),另一条是「谁承担跨层成本」(陷入转发 → 副本与就地处理)。

00形式判据 · P&G 1974

三条性质是定义,一个条件是充分条件

  1. 等价性:程序在 VMM 下与在裸机上不可区分,但允许两类例外——资源数量不同、时间依赖。
  2. 资源控制:VMM 对内存与外设握有完全控制;未分配的资源不可触及,已分配的可被收回。
  3. 效率:统计上占主导的那一子集 guest 指令必须由真实处理器直接执行,VMM 完全不介入。
第三条把解释器与纯软件模拟器排除在定义之外——QEMU 跨 ISA 时提高的是模拟效率,不是直接执行。

定理(充分而非必要)

敏感指令集合 ⊆ 特权指令集合 ⇒ 可构造出满足上述三性质的 VMM。

陷阱-模拟只是该条件下的一种实现,不是必要条件。

两个边界

机器模型只含处理器模式与 relocation-bounds 内存,显式假设无 I/O 与中断;递归虚拟化还要求无时间依赖,且不承诺低开销。

00形式判据 · 反例(90 年代的 x86)

pushf / popf:敏感,却不特权

P&G 要求「敏感 ⊆ 特权」:能观测或改变机器状态的操作都必须陷入。x86 恰好在这里破了个口子——这就是「x86 不可虚拟化」的来历。

pushf        ; EFLAGS → 栈;IF = bit 9
popf         ; 栈 → EFLAGS:想关中断
             ; CPL > IOPL ⇒ IF 静默丢弃

cli          ; 对照:受 IOPL 保护
             ; CPL > IOPL ⇒ #GP,陷入 VMM

两条在 ring 3 直接执行、不陷入;cli / sti 却会陷入——VMM 想抓它,抓不到。

于是等价性两头都破

让 IF 生效,guest 就改到了宿主的开/关中断;不让它生效,guest 读回的又是宿主的 IF、写 IF 静默失效。两种情形都与裸机可区分。

后果:从翻译到 VT-x

trap-and-emulate 抓不到这条指令,VMware 改用二进制翻译把它改写成对 VMM 的调用;直到 2005 年 VT-x 把 IF 放进 VMCS 的 guest 状态,才从架构上补上。

一句话:敏感 ⊆ 特权 在 x86 上不成立——这是充分条件的失效,换个手段(翻译、硬件辅助)即可绕过。
01软件虚拟化 · 一个基本块,两个 TB

一条直线指令流,被切成两段

TB1 · 4 条0x100e8–0x100f0
100e8li t0,3 100eali t1,4 100ecadd t2,t0,t1 100f0rdcycle t5
切点:rdcycle 退 helper,这条指令即 TB1 的最后一条
TB2 · 6 条0x100f4–0x10108
100f4auipc t3,1 100f8addi t3,t3,24 100fclw t4,0(t3) 10100add a0,t2,t4 10104li a7,93 10108ecall
  1. rdcycle 的语义压不进 IR:它是 CSR 读,TCG 的 op 集合里没有对应操作,前端只能发一条 call csrr,把语义交回 C 层的 helper。
  2. 副作用之后,之前做的翻译假设不再可靠:CSR 写可能改 mstatus.MIE、改特权级、换 satp。translator_io_start 把 is_jmp 置成 DISAS_TOO_MANY,这条指令就是本 TB 的最后一条,随后 exit_tb 封块。
  3. 所以切点由语义决定,不由控制流决定:这段代码里一条跳转都没有,仍然被切成两个 TB;helper 本身也不是边界——call csrr 会返回,封块是转译函数的自选动作。
在 -d in_asm 日志里,这就是两段各自独立的 IN: 块。下面两页把两段各展开一遍。
01软件虚拟化 · TB1

TB1:三条算术 + 一次 CSR 读


; IN · guest 反汇编
      100e8  li       t0,3
      100ea  li       t1,4
      100ec  add      t2,t0,t1
      100f0  rdcycle  t5
; OP · TCG IR
      add_i64 x5/t0,$0x0,$0x3
      add_i64 x6/t1,$0x0,$0x4
      add_i64 x7/t2,x5/t0,x6/t1
      call csrr,$0x0,$1,x30/t5,env,$0xc00
      mov_i64 pc,$0x100f4
      exit_tb $0x0

; OUT · x86-64 机器码
      +0x00  mov  -0x10(%rbp),%ebx
      +0x05  jl   tb_exit
      +0x0f  movq $0x3,0x28(%rbp)
      +0x1f  movq $0x7,0x38(%rbp)
      +0x2b  mov  $0xc00,%esi
      +0x30  mov  %rbp,%rdi
      +0x33  call helper_csrr
      +0x3f  movq $0x100f4,0x1230(%rbp)
      +0x4a  jmp  tb_exit
节选:qemu-riscv64 10.2.1 实测;OUT 为同一次运行的宿主机器码按 TB 内偏移反汇编
  1. t0 寄存器的「家」在 env:0x28(%rbp) 就是 gpr[5],写回直接以 rbp 为基址,中间没有宿主寄存器参与。
  2. 常量折叠 ≠ 逐指令替换模板:三条算术在宿主侧也只剩三条写回,但最后一条是算好的 0x7。
  3. call 会让 SysV caller-saved 失效:被分到这些寄存器上的临时值,追下去就是某一位 RISC-V 寄存器(宿主寄存器 → TCG temp → gpr[i]);所以发 call 前必须先写回 env 的 gpr 槽位,否则 helper 一返回就无处可寻。
  4. 块首三条是例行开销:mov/test/jl 是 icount 检查,和 guest 指令无关。
01软件虚拟化 · TB2

TB2:地址计算、访存与退出


; IN · guest 反汇编
      100f4  auipc  t3,1
      100f8  addi   t3,t3,24
      100fc  lw     t4,0(t3)
      10100  add    a0,t2,t4
      10104  li     a7,93
      10108  ecall
; OP · TCG IR
      mov_i64 x28/t3,$0x110f4
      add_i64 x28/t3,x28/t3,$0x18
      qemu_ld_i64 x29/t4,loc5,noat+un+lesl,0
      add_i64 x10/a0,x7/t2,x29/t4
      add_i64 x17/a7,$0x0,$0x5d
      call raise_exception,$0x8,$0,env,$0x8

; OUT · x86-64 机器码
      +0x0f  movq $0x1110c,0xe0(%rbp)
      +0x1f  movslq (%rbx),%rbx
      +0x22  mov  %rbx,0xe8(%rbp)
      +0x29  mov  0x38(%rbp),%r12
      +0x2d  add  %r12,%rbx
      +0x30  mov  %rbx,0x50(%rbp)
      +0x34  movq $0x5d,0x88(%rbp)
      +0x43  movq $0x10108,0x1230(%rbp)
      +0x56  call raise_exception
  1. 地址在译时就算成常量:auipc + addi 折叠成 0x1110c,一条 movq 写进 env 槽。
  2. lw 展开成 qemu_ld:user-mode 下 guest va == qemu va,x86_64 下用一条 movslq 即可完成访存; 全系统模式则要先做软件 TLB 的 tag 比较,未命中转 MMU helper。
  3. ecall 变成 noreturn helper:异常号 0x8 入参,不返回,后端无需再为它生成后续代码。
02硬件辅助 · CPU 维

vCPU 上/下物理 CPU,只有两条 primitive

vCPU 之于物理 CPU,正如线程之于 CPU:都是把一份硬件分时复用,靠保存与恢复现场。 KVM 决定此刻哪个 vCPU 在哪个物理 CPU 上跑,而这件事落地只用到两条 primitive。

vCPU 对象(内存)kvm_vcpu_arch.ctxt:
GPR、sys_regs、PC、PSTATE
vmentry →
物理 CPU(寄存器)EL1/EL0 原生执行
guest 指令不再被翻译
vmexit →
回到 KVM按退出码分流:
就地解决 / 交 QEMU / 致命

; vmentry —— __guest_enter 的尾部
  ldp x0, x1, [x29, #CPU_XREG_OFFSET(0)]
  ...
  restore_callee_saved_regs x29
  eret          ; 按 SPSR_EL2/ELR_EL2 回 EL1/EL0
上 CPU:把 ctxt 逐批装进物理寄存器,最后一条 eret 才真正离开 EL2

; vmexit —— VBAR_EL2 → __kvm_hyp_vector
el1_sync:                ; guest 同步异常
  mrs x0, esr_el2
  mov x0, #ARM_EXCEPTION_TRAP
  b   __guest_exit       ; 存回 vcpu->arch.ctxt
el1_irq: / el1_fiq: / el1_error:
下 CPU:四类入口把退出码编码进 x0,汇到 __guest_exit 存回 vCPU
硬件只自动保存 SPSR_EL2 / ELR_EL2 / ESR_EL2 三条返回线索;GPR、浮点与系统寄存器由 __guest_enter / __guest_exit 逐批存取——所以 vCPU 是一个内存对象,物理 CPU 只是它临时的落脚点。
02硬件辅助 · 内存维

两级翻译,两个管理者

VAguest 虚拟地址
→
Stage-1 · guest 管TTBR0/1_EL1、TCR_EL1
→
IPAguest 眼里的「物理地址」
→
Stage-2 · hypervisor 管VTTBR_EL2、VTCR_EL2
→
PA真实机器内存

两级各自维护页表,最终生效的是两阶段权限的交集;guest 页表自身的访存也要过 Stage-2——页表项躺在 IPA 上,读表项本身就要再翻译一次。

同一件事的两次出场:影子页表
没有 Stage-2 的年代 影子页表 hypervisor 把 guest 页表与 IPA→PA 的映射在软件里合成一张,写进硬件。运行时零开销,代价全在维护期:guest 每次写页表都要 trap。
Stage-2 进入架构 EPT / NPT 硬件给出第二级翻译,hypervisor 只需管 IPA→PA 这一张表。影子页表退出关键路径,"敏感写入"重新变成普通存储。
嵌套虚拟化 影子化回归 L0 又要为 L1 的 EPT 建影子:EPT02 是 EPT12 的滞后副本。影子页表没有消失,只是被影子化的对象从 guest 页表升级成了 L1 的 EPT。
02硬件辅助 · 中断维

两条路径:落到 EL2 再注入,或直接落进 EL1

中断设备 / 定时器
硬件路由 →
EL2 · KVM查表、置 pending、写 List register
软件注入 →
EL1 · guest走自己的向量表
另一条路:中断 —硬件路由→ EL1——vLPI 直接投给驻留 vPE,全程不进 EL2。前提是该 vPE 正驻留在本 CPU 的 Redistributor 上。
ITS 的表结构与两条路径
右上是虚拟 PE 情形:vINTID/vPEID 经 vPE table 落进 Virtual pending table;右下是物理 PE 情形:经 Collection table 落进 Pending table。IHI 0069D, Fig. 6-4
  1. vPE 是 vCPU 的字段:struct its_vpe 内嵌在 kvm_vcpu.arch.vgic_cpu.vgic_v3 里,与 vCPU 一一对应、不单独分配,生命周期跟着 vCPU 走。
  2. 绑定 vPE = 一次调度动作:vCPU 上物理 CPU 时,把该 vPE 的 pending / property table 基址写进本 CPU 的 Redistributor(GICR_VPENDBASER / VPROPBASER)并置 Valid,此后硬件自己查表置位。
  3. 下 CPU 就撤回绑定:清 Valid、收回状态;此后的 vLPI 由硬件记进 pending 表,并打一条 doorbell—— 每 vCPU 一条的宿主物理中断,只用来叫醒 vCPU,不投给 guest;重新驻留后硬件再逐个投递。
02硬件辅助 · 设备维

SMMUv3:DMA 这条绕开 CPU 的路径也要过边界

问题DMA 用物理地址,guest 手里只有 IPA
于是
软件解:trap-and-emulateVMM 代填描述符,每次 I/O 都要过一遍
但太贵
硬件解:I/O 侧也放 MMUARMv8 里就是 SMMUv3
SMMUv3 的 STE 与 CD
StreamID 索引 STE;STE 的 Config 是总开关,S2TTB 给 Stage-2 页表,S1ContextPtr 指向 CD(TTB0/1、ASID、MAIR)。SubstreamID(PASID)只影响 Stage-1 的选择。 引自 Arm IHI 0070E.a
「等价性」在这里的要求:guest 的 DMA 只能落在它自己的页上;而 CPU 侧的页表对这条通路鞭长莫及。

# 建映射(软件):VFIO 把 guest 内存锁进设备 DMA 域
  def arm_smmu_attach_and_map(master, dom, iova, pa, prot):
      ste = stream_table[master.streamid]
      ste.config = S2_TRANS          # 只做 stage-2
      ste.s2vmid = dom.s2_cfg.vmid   # 所属 VM
      ste.s2ttb  = dom.pgtbl_ops.vttbr
      dom.pgtbl_ops.map_pages(iova, pa, prot)

  # 运行时(硬件):设备 DMA 查表,软件不参与
  def smmu_translate_dma(streamid, addr):
      ste = stream_table[streamid]
      pa  = walk(ste.s2ttb, addr)   # IPA -> PA
      check_permission(ste, pa)     # 越界则 fault

一例:网卡 StreamID=0x10、s2vmid=7,IPA 0x4000_0000 → PA 0x1_2000_0000;描述符填 0x4000_1000 就走到 0x1_2000_1000,填域外地址则 translation fault。

02硬件辅助 · 协作

硬件给机制,软件给管理与组合

维度 QEMU 软件(只展开 CPU 维) Arm + KVM 硬件辅助 仍是软件的责任
CPU TCG 翻译为宿主代码,TB 缓存复用,敏感操作退 helper EL1/EL0 原生执行,EL2 用 HCR_EL2 选择性陷入 陷入配置、vCPU 上下文与调度
内存 软件 MMU 与软件 TLB 解释 guest 页表 Stage-1 + Stage-2,MMU 做缓存与遍历 memslot 换算、S2 补表、TLB 失效
中断 控制器模型维护 pending,经架构异常投递 GICv3 虚拟 CPU 接口;GICv4 直注 vLPI 映射配置、驻留切换、doorbell 唤醒
设备 MemoryRegion 回调响应 MMIO,后端完成请求 SMMU 约束真实 DMA;模拟 / virtio / 直通并存 设备模型、复位、可迁移性、路由
硬件能削掉的是高频路径上的软件介入;决定「何时用哪种机制」的策略仍在软件手里。这条分工下一部分会再被推上一层。
03嵌套(一)Turtles · 问题建立

难点不在「多一层特权环」,而在资源归属不对称

嵌套虚拟化总体架构
L1 与它管理的 L2 同处 L0 的一个 VM 内(虚线框)。L0 为运行 L1、L2 各维护一份状态(0-1、0-2), L1 自己维护的 1-2 只存在于软件中,从不进入处理器。 引自 Ben-Yehuda et al., OSDI '10, Figure 3
L2退出
→
L0判归属
→
L1处理
→
L0再介入
→
L2恢复
  • 不是所有事件都走完这条链:外部中断、NMI,以及只在 VMCS02 里 trap 的事件属于 L0 自己,就地处理后直接恢复 L2——从 L1 视角这些事件根本不存在。
  • 「L0 保留最终控制」的准确含义是归属判定权在 L0,而不是层层转发。这条判定会贯穿四维:退出归谁处理、缺项(lazy-alloc)归谁填。
  • 合成的统一形状:L1 的要求(VMCS12 / EPT12)+ L0 的约束(VMCS01 / EPT01)→ 一份硬件当下可执行的产物(VMCS02 / EPT02)。
03嵌套(一)Turtles · CPU 维

三份 VMCS 的分工与四条合成规则

  1. host state 整组替换:VMCS02.host 必须是 L0 自己的(HOST_RIP 指 vmx_vmexit),因为从 L2 退出要回 L0,不是回 L1。
  2. VMCS12.host 抄进 VMCS01.guest:硬件只有 guest/host 两个槽位,没有第三个承载「下层 hypervisor 的 host 状态」,只能借 guest 槽位。
  3. guest state 大多直抄,例外是 CR0/CR4:有 guest/host mask 机制,装进 VMCS02 的是有效值再补写 CR0_READ_SHADOW。
  4. control data 按位或、且严格单调:VMCS02 的 trap 集合 ⊇ VMCS01 ∪ VMCS12。多出来的正是「L1 想要而 L0 不想要」的事件。
陷阱集合的并集就是嵌套多出退出的结构性来源——这也是第 4 章要优化的对象。

# L0 处理 L1 的 VMX 指令:两条路
  # ① 改状态:L1 --exit--> L0 --entry--> L1
  def handle_vmwrite(vcpu, field, val):
      vmcs12.field = val
      nested_vmx_succeed(vcpu)   # 清 CF/ZF
      # 改三处:结果寄存器 / RFLAGS / GUEST_RIP

  # ② 换环境:L1 --exit--> L0 --entry--> L2
  def handle_vmlaunch(vcpu):
      prepare_vmcs02(vcpu, vmcs12)  # 合成
      vmx_vcpu_run(vcpu)            # 不返回 L1
VMLAUNCH 与 VMRESUME 共用退出原因号 27,硬件不给区分——KVM 必须解码 guest 指令字节才能分流。

术语边界:Turtles 所说的「VMCS shadowing」是 L0 在软件里影子维护 VMCS12,与 Intel Haswell 之后的同名硬件特性不是一回事。

03嵌套(一)Turtles · 内存维

三层地址,压进硬件能走的两层

三种嵌套 MMU 模型
① 影子页表叠影子页表 ② 影子页表叠在 EPT 上 ③ 多维分页(EPT on EPT)。 引自 Ben-Yehuda et al., OSDI '10, Figure 4

前两栏的共同点:L1 用影子页表跑 L2,于是 L2 的每次页故障与页表写都要 L1 处理并经 L0 转发。本节只展开第 ③ 栏。

  1. 能力虚拟化:L0 把 EPT 能力暴露给 L1,L1 相信自己能建 EPT,于是建出 EPT12——但硬件只有一张 EPT。
  2. 折叠:L0 把 EPT12 与自己的 EPT01 折叠成 EPT02
  3. 缺项分流:EPT12 有映射 → 拼 EPT01 写入 EPT02;没有 → 说明该页尚未由 L1 分配,转给 L1。
数一数:三张表(L2 页表、L1 的 EPT12、L0 的 EPT01),而硬件一次只走两级。 多出来的一层必须折——折出来的 EPT02 就是影子页表:影子化没有消失,只是被影子化的对象从 guest 页表换成了 L1 的 EPT。
04嵌套(二)HyperTurtle · 问题定位

先分清两张表各缺什么

嵌套 EPT fault 九步
普通嵌套下的九步:(a) 流程,(b) EPT12 与 EPT02 对应项状态——“皆缺” → “EPT12 有而 EPT02 缺” → “皆有”。全程六次 world-switch,非嵌套只需两次。 引自 Ben Zur et al., ATC '25, Figure 4

# P = L2-PA,M = L1 帧,H = 机器地址
  EPT12: P->M   EPT01: M->H   EPT02: P->H

  def handle_l2_ept_fault(P, access):
      if ept12_lookup(P).present:
          install_ept02_from_ept12(P)  # 自补
          resume_l2(); return
      # EPT12 也缺 → 传统路径进 L1
      if hyperupcall_ept_fault(P) == OK:
          install_ept02_from_ept12(P)
          resume_l2()               # 不进 L1
      else:
          inject_ept_fault_to_l1(P)
          resume_l1()          # 慢路径
只有 EPT12 也缺项时,hook 才接管——EPT02 缺项本身不等于要跑 hyperupcall。
成功时路径被压成 L2 → L0 → L2:省掉进入 L1 建蓝本、L1 恢复被截、以及 L2 第一次重执行后的第二次 fault。L0 对影子表的建立并未取消。
03嵌套(一)Turtles · 边界

决定是否退出的,不是「敏感」这个标签

而是这条指令的效果落在哪里: trap-and-emulate 下 L1 执行 vmread/vmwrite 必然退出,根因是处理器手上没有一份能替 L1 接收这次访问的控制结构。一旦硬件能提供副本,同一条指令就能原地执行。

x86VMCS shadowing(Haswell)

L0 在真实 VMCS01 的 link pointer 里填入另一块 shadow VMCS,并准备两张各 4 KB 的 VMREAD/VMWRITE 位图,每个字段编码一位:位为 0 的字段不退出,直接读写副本。

筛选粒度:可写字段 24 项、只读字段 12 项(vmcs_shadow_fields.h)。影子里的改动由 copy_shadow_to_vmcs12() 在 L1 进 L2、vmclear/vmxoff、导出状态三个时机拷回。

ArmFEAT_NV / NV2

NV(v8.3)把 EL1 访问「只在 EL2 定义的寄存器」从 UNDEFINED 改成陷入 EL2;NV2(v8.4)再进一步:HCR_EL2.NV=1 且 NV2=1 时,硬件把访问改写成对 VNCR_EL2 所指页面的 load/store。

「读一个 EL2 寄存器」从一次异常变成一次普通访存;页面仍在 L0 手里,建立、失效与同步由软件负责。

结论与边界

两者不宜推成同构——一边让字段访问落到影子控制结构,一边把寄存器访问改写成内存访问,指令、状态与失效规则都不同。

能说的是同一件事:只要架构能提供一份 L0 仍能约束的副本,退出就能从关键路径上挪走。代价没消失,只是转成了副本一致性维护——VMEntry/VMExit、TLB 失效与中断投递仍要回 L0。

04嵌套(二)HyperTurtle · 机制

注册路径与热路径要分开看

安装(一次性)

L1 经 hypercall 把编译后的 eBPF 交给 L0,经 KVM 与用户态组件验证、加载,再 link 到 EPT fault hook。load 与 link 拆成两个 hypercall,同一套 map 实例可挂多个 hook。

共享状态(四类 + 一把锁)

预分配帧池(4096 帧)、QEMU 页表根 cr3、两条日志(hyperupcall / page fault),加一把双方都能取的锁。

能力受限

bpf_probe_read_hyperupcall 只读有效、已映射且属于发起它的 L1 的物理内存:verifier 管程序形态,helper 管地址授权。


def hyperupcall_ept_fault(P, access):
      if not try_lock(shared): return FALLBACK
      if fault_log.is_full():   return FALLBACK
      V = qemu_va_for(P)          # memslot 关系换算
      M = walk_qemu_page_table(shared_cr3, V)
      if M is not None:        # 复用已有帧
          ept12_install(P, M, access)
          fault_log.append(P); return SUCCESS
      if page_fault_log.contains(P):
          return FALLBACK       # L1 正并发处理
      M = frame_pool.pop()
      if M is None: return FALLBACK
      ept12_install(P, M, access)
      fault_log.append(P); return SUCCESS
返回成功后仍由 L0 读新 EPT12、经 EPT01 补 EPT02,再 VM-entry 重执行原指令。

一致性靠延迟补偿:L1 下次因其他 VM-exit 进入时消费 fault log 补自己的页表;shmem 缺页路径则包一层,先查 EPT12 是否已被填好。锁、双日志、帧池与原有 L1 路径共同兜住回退。

References

主要文献与出处正式报告共引 11 项,此处列主要 8 项

  • [1]Popek & Goldberg. Formal Requirements for Virtualizable Third Generation Architectures. CACM 17(7): 412–421, 1974.
  • [2]Bellard. QEMU, a Fast and Portable Dynamic Translator. USENIX ATC (FREENIX), 2005: 41–46.
  • [3]Ben-Yehuda 等. The Turtles Project: Design and Implementation of Nested Virtualization. OSDI '10, 2010.
  • [4]Ben Zur 等. Accelerating Nested Virtualization with HyperTurtle. USENIX ATC '25, 2025: 987–1002.
  • [5]Dall & Nieh. KVM/ARM: The Design and Implementation of the Linux ARM Hypervisor. ASPLOS '14, 2014: 333–348.
  • [6]Arm Ltd. Armv8-A Architecture Reference Manual, DDI 0487F.b, 2020.
  • [7]Arm Ltd. GIC Architecture Specification(IHI 0069D); SMMU Architecture Specification(IHI 0070B)。
  • [8]QEMU(commit 56afd82b…)与 Linux 7.0.14 源码:accel/tcg、 arch/arm64/kvm、arch/x86/kvm/vmx、drivers/iommu。

图中原始图元引自 OSDI '10 Fig. 3/4、ATC '25 Fig. 1/4、IHI 0069D Fig. 6-4 与 IHI 0070E.a。本文未做性能复现。