虚拟机

Emulation

指令集模拟是在一个平台上模拟另一个(或同一)CPU 的指令与执行语义。常见场景包括:跨架构仿真(比如在 x86 上跑 ARM 二进制)、系统模拟(Bochs)、动态二进制分析

Interpretation

解释执行(Interpretation):逐条取出目标指令,解析并执行对应的宿主(host)操作。

simple interpreter

是一种执行程序的方法,它不直接在硬件上运行目标二进制,而是 逐条读取(decode)、理解(dispatch)、执行(interpret)指令的语义。Decode-and-Dispatch 模式

  • 核心机制:有一个大循环(interpreter loop),每次循环:
    1. 取指 (fetch):从“程序计数器(PC)”指向的内存里读一条指令。
    2. 解码 (decode):解析这条指令是什么类型(加法?加载?跳转?)。
    3. 分发 (dispatch):根据指令类型,跳转到对应的解释函数/分支逻辑。
    4. 执行 (execute):修改解释器内部维护的状态(寄存器、内存、标志位等)。
    5. 更新 PC:让 PC 指向下一条指令,然后继续循环。
  • 这种结构叫 decode-and-dispatch interpreter,因为它的循环主要做两件事:解码 + 分发

Threaded Interpretation

其中 dispatch(分派)开销很大

  • 每条 guest 指令都要走一次 switch-case,即使宿主已经知道下一条指令的类型。
  • 分支预测容易失败(尤其是 guest 指令随机混合时)。

Threaded Interpretation 的核心思想

  • 提前把“分派信息”存好,减少运行时的 switch-case 开销。
  • 通过 直接跳转表(jump table)函数指针数组,让解释器在执行完一条指令后,能直接跳到下一条指令对应的解释代码

(a) 指令预处理

  • 把 guest 指令流转成一组“解释器操作码(interpreter opcodes)”,
  • 每个操作码对应一个 函数指针 / 标签地址

(b) 执行循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 每个指令对应的函数(handler)
void op_add(State *s, Instr *pc) {
s->regs[pc->rd] = s->regs[pc->ra] + s->regs[pc->rb];
goto * (++pc)->handler; // 直接跳到下一条指令的 handler
}

void op_load(State *s, Instr *pc) {
s->regs[pc->rd] = s->mem[s->regs[pc->ra] + pc->offset];
goto * (++pc)->handler;
}

// 主循环
void run(Instr *pc, State *s) {
goto *pc->handler;
}

性能大幅提升:消除了大 switch 的分支预测开销,指令执行效率更接近 DBT。

结构紧凑:代码逻辑更接近汇编跳转流程。

易扩展:只要给新指令定义 handler 即可。

Predecoding and Direct Threaded Interpretation

Threaded Interpretation 优化掉了分派开销(通过 goto *pc->handler),但还有一个问题:解码成本仍然存在。真实 ISA 的二进制指令通常比较复杂(如 x86 可变长指令)。

每次执行前都要从原始二进制里解码 → 很浪费

Predecoding(预解码)

在程序加载或第一次扫描时,把原始二进制指令解码成一种更容易解释的“中间表示(IR)”。

  • 把 guest 二进制逐条读取 → 解码成一个结构体(保存操作码、寄存器编号、立即数、偏移量等)。
  • 在结构体里额外存一个指向 解释函数 handler 的指针。

这样一来,解释器执行时就不用再做复杂解码了,直接使用预先存好的 handler 地址和参数即可。

Direct Threaded Interpretation

  • 每个预解码后的指令条目都带有 直接跳转到对应 handler 的地址
  • 执行时:goto *pc->handler,执行 handler,handler 里再跳到下一条。

这样:

  • 无需中心调度循环。
  • 无需在执行时再查找 opcode → handler 的映射。

Interpreting a Complex Instruction Set

RISC(简化指令集):指令长度固定、格式统一、操作数数量有限、寻址方式少。比如 PowerPC、MIPS、RISC-V。→ 解释器很容易写:PC 每次 +4,解码也很简单。

CISC(复杂指令集):指令长度不固定、寻址模式多、单条指令可能做很多事(如加载、算术、存储、条件更新标志)。典型代表:x86

Interpretation of the IA-32 ISA

IA-32 的解释器通常分成 两步走

  1. 填充通用指令模板
    • 把原始字节流解码成一个“中间表示”(类似指令对象),包括操作码、操作数类型、寻址模式、立即数/位移等。
    • 这一步解决变长指令与复杂寻址的解析。
  2. 分发至专用例程
    • 根据解码结果(操作码、寻址模式等),调用相应的解释函数来模拟执行。

👉 实现方式通常依赖一个 大型查找表

  • 输入是操作码,输出是对应的解码/执行函数。
  • 遇到 ModR/M、SIB、位移、立即数时,还要进一步解析并存入模板。

Threaded Interpretation

问题:如果每个解释例程都内嵌完整的“解码逻辑”,代码会非常庞大、冗余。

优化思路:混合模型

  • 简单、常见指令 → 用轻量级 direct threading 技术,直接跳转到 handler,避免大开销。
  • 复杂、少见指令 → 回退到 集中式解码器,统一解析。

Binary Translation

它是比“逐条解释”更高效的一种指令集模拟技术,同时涉及 代码发现与动态翻译(DBT, Dynamic Binary Translation)

二进制翻译:把一段源 ISA 的指令序列(通常是基本块 basic block)一次性翻译为目标 ISA 的等效代码。

代码块翻译与缓存

  • 单位:基本块(single-entry, single-exit),因为控制流简单,便于翻译。
  • 翻译后存入 代码缓存(code cache),下次遇到直接跳转到缓存的机器码。
  • 若源代码有 自修改(self-modifying code),需要失效并重新翻译。

寄存器状态映射

  • 源 ISA 的寄存器数量/功能 ≠ 目标 ISA。
  • 通过 寄存器映射,把源寄存器直接绑定到目标物理寄存器,减少访存。
  • 比如:IA-32 的 EAX 直接映射到 PowerPC 的 r5

指令序列优化

翻译不仅是逐条对等替换,还能做优化:

  • 死代码消除(eliminate dead code)。
  • 公共子表达式消除(common subexpression elimination)。
  • 寄存器复用(避免多余 load/store)。
  • 常量折叠(constant folding)。

Code Discovery and Dynamic Translation

The Code-Discovery Problem

翻译器必须知道哪些字节是 可执行的指令,才能正确解码。但在 CISC 架构(如 IA-32/x86) 上,这个过程非常棘手。

难点

  1. 变长指令
  2. 间接跳转(Indirect Jumps/Calls)
    • 目标地址不是立即数,而是寄存器值或内存取值。
    • 只有运行时才能知道跳到哪里。
    • 静态分析无法提前收集所有目标地址。
  3. 数据混杂在代码段中
    • 某些程序在代码段中嵌入数据(例如查表、只读掩码、对齐填充)。
    • 如果翻译器误把这些数据当成指令解码,就会出错。
  4. 指令对齐(Alignment)问题
    • 编译器可能在代码里插入对齐填充(如 0x90 NOP),
    • 甚至可能造成某些字节既是对齐填充又可能被跳转到作为指令入口。
    • 这使得静态预解码更加困难。

👉 结论:CISC 上几乎无法通过静态分析完整、准确地发现所有代码块。 因此,动态二进制翻译(DBT)一般采用 运行时发现 + 按需翻译

The Code-Location Problem

当源代码被翻译为目标 ISA 代码后,就存在 两套 PC(程序计数器)

  • SPC(Source PC):源 ISA 的指令地址。
  • TPC(Target PC):翻译后代码在宿主 ISA 中的地址。

问题:

  • guest 程序在执行时,寄存器保存的是 SPC 地址。
  • 但 DBT 翻译后的代码在宿主机器上运行时,需要跳转到 TPC
  • 如果直接用 SPC 跳转,会跳到错误的地方(因为 SPC ≠ TPC)。

解决办法:

  • 建立映射表(Mapping Table)
    • 记录每个 SPC 对应的 TPC。
    • 在执行间接跳转时:
      1. 取出 guest 代码给的 SPC;
      2. 查映射表找到对应的 TPC;
      3. 跳转到 TPC。
  • 优化手段
    • 直接插桩:在翻译后的代码里,用跳转表或内联检查,把 SPC → TPC 的转换直接嵌入。
    • 快速路径 + 慢路径
      • 常见的目标 SPC 直接内联成直接跳转。
      • 不常见的 SPC 则走查表。

Incremental Predecoding and Translation

动态二进制翻译系统(DBT)通常包含:

  • 解释器 (Interpreter):兜底执行,或者在翻译前先跑一段。
  • 二进制翻译器 (Translator):把源 ISA 指令块转换为目标 ISA 代码。
  • 仿真管理器 (EM, Execution Manager):调度运行时流程,负责 SPC→TPC 映射、桩代码管理、异常/中断恢复。
  • 代码缓存 (Code Cache):存放已经翻译好的目标代码。
  • SPC-TPC 映射表:源地址(Source PC)和目标地址(Target PC)的映射,用于间接跳转和异常处理。
Dynamic Basic Block

使用动态基本块作为翻译单元 ,从一个控制流入口(通常是分支/跳转目标)开始,顺序包含多条指令,直到遇到下一个分支/跳转结束。

静态基本块的区别:

  • 静态基本块由编译器分析决定,唯一划分。
  • 动态基本块依赖运行时的控制流路径 → 更大、更多样
  • 同一条指令可能出现在多个不同的动态块里(因为来自不同路径)。
image-20250923185037039

DBT 不会在运行前翻译整个程序(那是静态翻译的思路)。

采用 增量式

  • 第一次执行某段 SPC 时 → 翻译成目标代码,存入缓存。
  • 下一次再遇到同一 SPC → 直接跳转到缓存好的 TPC。
SPC-TPC 映射表
  • 功能:把源程序的 PC(SPC)转换成翻译后目标代码的入口(TPC)。
  • 典型实现:哈希表,查询快、可扩展。
桩代码(Stubs)
  • 动态块通常以 分支或跳转结束。
  • DBT 会在末尾插入 桩代码 (stub)
    • 保存下一个 SPC 值。
    • 把控制权交回仿真管理器(EM)。
  • EM 根据 SPC 查映射表:
    • 如果已翻译 → 跳到对应的 TPC。
    • 如果没翻译 → 调用翻译器生成新块。

👉 桩代码相当于 中转站,保证动态控制流始终能正确跳转。

自修改代码 (Self-Modifying Code)

  • 如果源程序修改了自己(写入代码段),则对应的翻译版本失效。
  • 需要检测并 重新翻译该代码块。

自引用代码 (Self-Referential Code)

  • 程序有时会读取自身指令字节作为数据。
  • 在这种情况下,必须返回 原始二进制,而不是翻译后的版本。
  • 因此,DBT 系统必须同时保留原始代码副本

源 ISA 可能要求某些异常/中断发生时,必须在精确的指令边界恢复上下文。

DBT 在翻译时必须保证:

  • 如果 TPC 执行时出错(如访问越界),系统能回溯到对应的 SPC。
  • 并且恢复源寄存器、标志位等上下文。

这要求翻译器在生成代码时插入 映射点 (mapping points),保证能从宿主状态恢复出精确的源状态。

Same-ISA Emulation

虽然源程序和宿主机器的 指令集架构(ISA)相同,理论上可以直接运行,但仍然通过一个“仿真层”来间接执行程序。这样做的目的,不是跨平台,而是为了 控制、监控和优化 程序运行。

应用场景
  1. 程序性能模拟
    • 例如 Shade 系统(早期的 SPARC 同 ISA 仿真器),通过插桩收集性能数据,评估程序行为。
  2. 操作系统调用模拟
    • 即使 ISA 一样,不同 OS 也有差异(如 Linux vs. BSD)。
    • 仿真层可以把源 OS 的系统调用转化为宿主 OS 的调用。
  3. 特权操作检测与处理
    • 捕获内核相关指令(如 I/O、内存管理指令),避免直接执行带来安全风险。
  4. 安全防护
    • 在仿真层中插入监控逻辑,比如控制流完整性检查,防止攻击者劫持跳转。
  5. 动态二进制优化
    • DBT 技术可以利用运行时信息(热点路径、分支预测结果等)来做动态优化。
    • 例如在运行时对频繁执行的循环进行指令级优化,提高整体性能。

同 ISA 仿真器往往不需要复杂的“翻译”工作,因为源/目标 ISA 一致。

实际做法类似于 源码复制 + 轻量包装

  • 普通指令 → 直接执行或简单转发;
  • 系统调用、特权指令等特殊情况 → 拦截并处理。

Control Transfer Optimizations

动态二进制翻译(DBT)中的控制流优化,目标是减少“翻译块之间的切换开销”,让程序在缓存好的翻译代码中更顺畅地执行。

Translation Chaining

问题:在基础的 DBT 中,每个翻译块(Translation Block, TB)执行结束时,通常会通过桩代码(stub)返回到仿真管理器(EM),再由 EM 决定下一个要执行的 TB。这样一来,每个跳转都会有 EM 介入,性能开销很大。

优化

  • 初始时,块末尾放一段跳转到 EM 的桩代码。
  • 当后续块被翻译出来后,可以 回填(patch) 前一块的桩代码 → 直接跳转到新块的入口。
  • 这样两个翻译块就被“链”在一起,运行时直接跳过去,减少 EM 调度。

限制

  • 仅适用于 直接跳转(目标固定可知)
  • 间接跳转(比如函数返回,目标依赖寄存器或内存值)不适合直接链接。

Software Indirect Jump Prediction

问题:间接跳转(如函数指针调用、虚函数调用、switch 表、返回地址)在运行时目标可能变化。没法提前静态链接,只能依赖查表 → 成本高。

优化:内联缓存 (Inline Caching, IC)

  • 在翻译块中直接嵌入“快速判断逻辑”:
    • 如果当前跳转目标 = 最近一次/常见的 SPC → 直接跳转到缓存的 TPC。
    • 如果不匹配 → 回退到 EM,通过映射表查找正确目标。

Shadow Stack

问题:函数调用/返回是最常见的间接跳转。如果每次返回都要查映射表,成本高。

优化

  • 函数调用时,除了正常把返回地址压入源栈,还把该返回地址(SPC)和对应的 TPC 压入 影子栈
  • 函数返回时
    • 比较源栈顶的 SPC 与影子栈顶的 SPC。
    • 如果匹配 → 直接用影子栈中的 TPC 跳转,避免查表。
    • 如果不匹配 → 回退到正常查表机制。

安全性

  • 同时保存源栈指针(SP)值,检测栈截断或修改,保证影子栈和源栈同步。

Instruction Set Issues

动态二进制翻译(DBT)中与指令集差异相关的挑战。当源 ISA 和目标 ISA 不同的时候,必须解决寄存器、条件码和数据表示等方面的不匹配问题,否则翻译出来的代码要么不能执行,要么语义不对。

Register Architectures

挑战:目标架构的寄存器既要保存源程序的寄存器值,还要承担运行时环境管理(比如指向上下文块)和翻译过程中生成的中间值。如果目标架构寄存器数量少,就容易不够用。

情况一:寄存器宽裕(RISC 模拟 CISC)

  • 比如用 PowerPC/RISC-V 来模拟 x86,目标寄存器数量多,可以 静态分配
  • 常用策略:给源寄存器分配固定的目标寄存器,把一部分寄存器专门用来保存上下文指针或中间值。

情况二:寄存器紧张

  • 比如在寄存器少的目标 ISA 上模拟寄存器多的源 ISA。
  • 必须 动态管理:在翻译块入口/出口处,把源寄存器状态保存/恢复到内存的“上下文块”,在块内灵活分配寄存器。

Condition Codes

差异来源

  • IA-32 (x86):有隐式条件码(EFLAGS),每次算术操作自动更新。
  • SPARC / PowerPC:条件码是显式设置的(需要单独指令)。
  • MIPS:没有条件码,而是通过比较指令直接得到分支结果。

最难情况:源 ISA 有隐式条件码(x86),但目标 ISA 没有(MIPS)。这意味着必须模拟条件码更新,但不能直接映射寄存器。

常见优化

  • 惰性求值 (Lazy Evaluation):不立即计算条件码,而是记录“由哪条操作生成了哪些标志位”,只有当真正用到时才计算。
  • 数据流分析:如果确定某些条件码值根本没被用到,可以完全跳过,减少开销。

异常情况:如果程序在一条指令后触发异常,必须立即 物化(materialize) 出所有相关条件码,确保系统能恢复到与源 ISA 完全一致的状态。

Data Formats and Arithmetic

相对容易的情况

  • 整数:大多数 ISA 都用二进制补码。
  • 浮点:大多数遵循 IEEE 754 标准。
  • → 这部分通常能直接映射。

差异与挑战

  • IA-32:使用 80 位浮点中间结果(比常见的 64 位双精度更高)。在模拟时,可能需要额外保存精度,否则结果不一致。
  • PowerPC:部分浮点乘加指令精度高于 IEEE 标准 → 必须小心保持一致性。

缺失功能的替代

  • 目标 ISA 没有对应指令时,可以用多条指令组合实现。例如:
    • 除法缺失 → 用移位和减法迭代实现。
    • 复杂寻址模式(如 x86 的 [base+index*scale+disp]) → 拆解为加法 + 乘法 + 加载序列。
  • 立即数长度不够 → 用多条加载/移位/或运算组合出完整常量。

Memory Address Resolution

问题:不同 ISA 支持的最小存取单位不同。

  • 有的 ISA 是 字节寻址(x86、ARM 等),最小单位 1 byte。
  • 有的 ISA 是 字寻址(早期的 PDP-11、部分 DSP),最小单位是 word。

挑战

  • 如果目标 ISA 只支持 word 级访问,但源 ISA 允许 byte 访问,就需要用移位、掩码来提取特定字节。
  • 反之,如果目标 ISA 是 byte 寻址,模拟 word 寻址的源机就比较容易(属于“强模拟弱”)。

Memory Data Alignment

问题

  • 有些 ISA 要求 自然对齐(比如 4 字节 word 必须在 4 的倍数地址)。
  • 但像 x86 允许非对齐访问。

挑战

  • 如果目标 ISA 不支持非对齐访问,就要在 DBT 中拆成多个 byte load/store,或使用目标 ISA 的专用指令(如 ARM 的 LDRD/STRD 辅助)。

优化

  • 运行时分析访问地址是否对齐:
    • 若对齐 → 用高效的 word 访问。
    • 若不对齐 → 退化成字节级操作。

Byte Order

差异

  • 大端 (Big-endian):最高有效字节在低地址(PowerPC、SPARC)。
  • 小端 (Little-endian):最低有效字节在低地址(x86、RISC-V)。

挑战

  • 若源/目标 ISA 字节序不同,每次 load/store 都要做字节翻转。
  • 非对齐访问时更麻烦,往往需要逐字节重组,或者利用双字加载+移位来拼接。

优化

  • 如果目标 ISA 硬件支持双端模式(bi-endian),直接切换模式,避免软件翻转。
  • 系统调用数据交互时,也要用包装代码保证字节序一致。

Addressing Architecture

问题:ISA 在地址空间、页面大小、特权机制上可能不同。

  • 比如源 ISA 支持 32 位虚拟地址,目标 ISA 是 64 位。
  • 或者源机有段式内存模型,目标机没有。

挑战

  • 这类问题不是单条 load/store 就能解决的,而是涉及 整个虚拟内存系统设计
  • 必须通过 虚拟机架构 来协调,例如 shadow page table、二级地址翻译。

结论:这个部分通常被延迟到更高层的虚拟机设计里讨论,而不是在单纯的指令翻译阶段处理。

System Virtual Machines

应用场景

  • 实现多道程序与时间共享:使用多个单用户虚拟机替代复杂的多道程序操作系统,提升效率与灵活性。
  • 多个单应用虚拟机:提高系统鲁棒性,一个应用或操作系统的故障不会影响其他虚拟机。
  • 多安全环境:提供沙箱机制,确保不同用户环境之间的隔离,防止数据泄露或监控。
  • 受控应用环境:核心应用运行于受保护虚拟机中,用户自定义应用运行于另一虚拟机,避免干扰。
  • 混合操作系统环境:同一硬件平台同时运行不同操作系统,满足多样化需求。
  • 遗留应用支持:在旧操作系统虚拟机中运行性能敏感的旧应用,在新系统中利用新特性。
  • 多平台开发:开发者可在同一硬件上测试跨操作系统软件,降低成本与复杂性。
  • 系统迁移过渡:逐步迁移到新操作系统,旧系统继续运行关键应用直至验证完成。
  • 系统软件开发:开发与生产环境分离,避免开发中的错误导致系统崩溃。
  • 操作系统培训:在虚拟机中进行参数调整实验,不影响真实系统用户。
  • 帮助台支持:模拟客户硬件配置以诊断问题,无需物理设备。
  • 操作系统仪器化:VMM 可监控硬件访问行为,记录事件详情用于研究与调试。
  • 事件监控:支持执行轨迹记录、状态快照与重放,便于分析异常行为。
  • 系统封装:完整保存虚拟机状态,支持检查点与迁移。

Key Concepts

  • 系统虚拟机通过虚拟机监控器(VMM)在主机硬件上创建多个隔离的虚拟系统环境,每个虚拟机运行独立的操作系统和应用程序。
  • VMM 拥有对真实硬件资源的控制权,并将其分配给多个客户操作系统,使每个客户系统以为自己独占资源。
  • 虚拟资源可对应真实物理资源(如CPU、内存、I/O设备),也可由VMM通过软件模拟实现。

Outward Appearance

虚拟机给用户的“错觉”是:它就像一台独立的计算机。

这种表现可通过软件模拟(纯软件复制)或硬件支持(部分功能直接由硬件加速)实现。

用户专用的设备(键盘、显示器等)可以:

  • 物理复制 → 每个虚拟机独立拥有。
  • 共享切换 → 多个虚拟机通过时分/窗口机制共享同一设备。

在宿主操作系统上,客户机界面甚至可以嵌入窗口,比如 DOS 程序运行在 Windows 的窗口中。

State Management

每个虚拟机需要独立的体系结构状态(寄存器、内存、I/O 设备状态)。

状态管理方式:

  • 间接访问:虚拟机的寄存器状态保存在主机内存中,通过指针访问。优点是容易实现,但性能较差。
  • 直接复制:切换虚拟机时,把客户机寄存器状态复制到物理寄存器中,以获得更高性能。

权衡点:使用频率高的状态(如寄存器)更适合直接复制;使用不频繁或不匹配的资源,可以存放在内存里,用间接方式访问。

Resource Control

  • VMM 必须拥有硬件的 最终控制权,哪怕资源在客户机手中。
  • 实现方式类似操作系统:
    • 操作系统依赖定时器中断来收回 CPU。
    • VMM 截获对特权资源(如定时器)的访问,向客户机提供 虚拟定时器
    • 这样可以防止某个客户机垄断资源,保证公平调度。
  • 调度权衡
    • 时间片过大 → 响应慢,不公平。
    • 时间片过小 → 上下文切换开销太大。

Native and Hosted Virtual Machines

原生虚拟机 (Native VM)

  • VMM 直接运行在最高特权级,直接控制硬件。
  • 客户机 OS 在较低特权级运行。
  • 优点:性能高,开销小。

托管虚拟机 (Hosted VM)

  • VMM 作为应用运行在宿主操作系统之上。
  • 用户态托管:VMM 全部运行在用户态,灵活但性能较差。
  • 双模式托管:部分组件(如驱动)运行在宿主内核中,性能更高。
  • 优点:易于部署和维护,不需要完全控制底层硬件。

对比:原生更高效,托管更方便。

IBM VM/370

历史意义:最早的系统虚拟机环境之一。

硬件基础:基于 IBM System/360 Model 40,后来发展到 System/370。

设计目标:把虚拟内存的概念推广到整个系统级别,让每个虚拟机都看到完整的 ISA。

模块化结构

  • 控制程序 (CP) = VMM,负责资源管理。
  • 单用户操作系统 CMS = 用户环境。
  • CP 和 CMS 分离,使得资源管理与用户服务职责清晰。

硬件演进:System/370 增加了专门的虚拟化支持,降低 VMM 开销。这种思路延续至现代 IBM z/VM。

Resource Virtualization — Processors

处理器资源虚拟化 的主题,核心是 客户指令如何在虚拟机上被执行

  • 关键问题:客户机(guest)指令要么直接执行在硬件上(原生执行),要么由 VMM 模拟(interpret/translate)。
  • ISA 相同时 → 尽量让无害指令直接跑在硬件上,性能最佳;只有涉及特权或敏感行为的指令才需要 VMM 截获并模拟。

Conditions for ISA Virtualizability

  • Popek 与 Goldberg 三条件
    1. 效率 (Efficiency):绝大多数无害指令无需 VMM 干预,能直接在硬件执行。
    2. 资源控制 (Resource control):客户机软件不能直接改系统资源,VMM 保持最终控制权。
    3. 等价性 (Equivalence):虚拟机上运行程序的效果必须和真实机器上一致(性能差异除外)。
  • 定理 1:若“敏感指令集” ⊆ “特权指令集”,则该 ISA 可高效虚拟化。
    • 敏感指令:分为控制敏感(改变资源配置)、行为敏感(结果依赖资源状态)。
    • 特权指令:只能在内核态执行,用户态执行会陷阱(trap)到 VMM。
  • 理想情况:所有敏感指令都是特权指令 → 可以通过陷阱交给 VMM 处理。

Recursive Virtualization

概念:在一个 VM 内再运行另一个 VMM,形成层次化虚拟化。

要求:VMM 本身不能依赖“绝对时间”,否则破坏等价性。

局限:每层 VMM 占用资源(尤其内存),递归层数有限;一般 ≤2 层可接受。

Handling Problem Instructions

问题:一些 ISA 不满足定理 1。

  • 例如 Intel IA-32 存在 敏感但非特权 指令(称为 关键指令),在用户态不会陷阱,但会暴露硬件状态 → 威胁虚拟化。
  • 例子:POPF(修改标志寄存器),在用户态也能执行部分操作。

解决办法:

  • 扫描并修补 (patching):运行前扫描代码流,把关键指令替换为会触发陷阱的指令,从而交由 VMM 处理。

Patching of Critical Instructions

  • 执行步骤:
    • VMM 扫描客户机代码流,遇到关键指令时 → 插入陷阱。
    • 陷阱触发后 → VMM 执行模拟逻辑,并继续扫描后续基本块。
  • 优化:
    • 已修补的基本块可以 直接链接(类似代码缓存),减少重复陷阱。
    • 间接跳转目标难预测 → 通常保留陷阱机制。
  • 范围:扫描仅限 已加载到内存的代码页,避免因换页带来开销。

Caching Emulation Code

问题:高频关键指令若每次都解释,会产生巨大开销。

解决:

  • 把模拟动作编译成 缓存代码块,下次执行时直接运行缓存逻辑,而不是重复解释。
  • 缓存以“陷阱地址”为索引。
  • 返回时直接跳过已模拟过的部分,提高效率。
  • 若客户机代码自修改 → 必须检测并使缓存失效,避免错误执行

Efficient Virtualizability of Common Instruction Sets

IBM System/370

  • 所有敏感指令 = 特权指令 → 天然可虚拟化。
  • VM/370 中大多数指令原生执行,特权指令陷阱交给 VMM。
  • 客户 OS(CMS)允许用户发特权指令,VMM 安全模拟。

Intel IA-32

  • 存在“敏感但非特权”指令(如 POPF),不满足定理 1。
  • 必须用 修补二进制翻译 (binary translation) 解决。

历史转折

  • 早期(硬件便宜时)一度认为虚拟机没价值。
  • 但随着安全、服务器整合、跨平台需求的兴起,虚拟化重新变得重要。

Resource Virtualization — Memory

Virtual Memory Support in a System Virtual Machine Environment

在普通系统中:

  • 应用程序 看到的是逻辑地址空间。
  • 操作系统 通过页表把逻辑地址映射到真实硬件的物理内存。

系统虚拟机 中:

  • 每个客户 VM 以为自己独占物理内存(称为“实内存”)。
  • 实际上,这个“实内存”只是 VMM 虚拟出来的错觉
  • VMM 把客户 VM 的“实地址”再映射到宿主机的 物理内存

资源过订阅问题:

  • 多个 VM 的实内存总和可能 > 宿主机实际物理内存。
  • VMM 需要维护独立的 交换区 (swap space),负责页的换入换出。

影子页表机制 (Shadow Page Tables)

  • 问题:如果直接使用两层映射(虚→实→物),性能会很差。
  • 解决方案:影子页表
    • 客户 OS 仍维护 虚拟地址 → 实地址 的页表。
    • VMM 维护 虚拟地址 → 物理地址影子页表,交由硬件直接使用。
  • 机制
    • VMM 虚拟化页表寄存器:客户机切换页表时,VMM 让硬件实际加载影子页表。
    • 客户机对页表寄存器的读写会触发陷阱,由 VMM 拦截并维护同步。
    • 保证等价性:如果客户 OS 认为某页不可用,影子页表中也必须保持不可用。
  • 缺页异常处理
    1. 如果客户 OS 已经映射该页 → VMM 从 swap 恢复物理页,更新影子页表。
    2. 如果客户 OS 未映射该页 → VMM 把缺页异常传递给客户 OS,由其加载 I/O;VMM 再同步更新影子页表。
  • I/O 映射问题
    • I/O 操作常用实地址,VMM 必须转换成宿主物理地址。
    • 可能涉及非连续页 → VMM 需要拆分请求或提前换页。

Virtualizing an Architected TLB

背景:TLB 是硬件缓存,用来加速地址转换。

问题:当 ISA 采用 软件管理 TLB 时,客户 OS 可能直接写 TLB,这就需要 VMM 进行虚拟化。

方法一:重写物理 TLB

  • 每次 VM 切换时,VMM 重写 TLB,把客户的虚拟 TLB 条目翻译后写入物理 TLB。
  • 缺点:频繁刷新,开销大。

方法二:ASID 映射机制

  • 硬件支持 ASID (Address Space Identifier) → 区分不同地址空间。
  • VMM 维护 虚拟 ASID → 物理 ASID 的映射表。
  • 多个 VM 的地址空间可以共存于物理 TLB,不必频繁刷新。
  • 当客户 OS 写 ASID 寄存器时:VMM 截获,更新虚拟副本,并调整映射关系。
  • 例如:两个 VM 都认为自己用 ASID=1,VMM 实际分配给它们不同的物理 ASID,避免冲突。

Resource Virtualization — Input/Output

I/O 虚拟化比 CPU、内存更复杂,因为设备种类多、操作系统抽象不同。基本策略是 构建虚拟设备模型,拦截客户机的 I/O 请求并转发/模拟。

Virtualizing Devices

  • 专用设备:如键盘、鼠标、显示器 → 必须分配给特定 VM,VMM 负责转发请求和中断。
  • 分区设备:如磁盘 → VMM 把物理磁盘划分成多个虚拟磁盘,维护参数映射(磁道、扇区)。
  • 共享设备:如网卡 → 多 VM 共享,VMM 维护虚拟状态(虚拟 MAC/IP),负责路由数据包。
  • 缓冲池设备(spooled devices):如打印机 → 客户 OS 先写入 VMM 的缓冲池,由 VMM 统一调度输出。
  • 无物理对应的虚拟设备:如虚拟网卡 → 专供 VM 间通信,VMM 拦截 I/O 请求并注入中断。

Virtualizing I/O Activity

image-20250923212414427
Virtualizing at the I/O Operation Level (操作级虚拟化)
  • 机制
    • I/O 操作通过 内存映射 I/O (MMIO)特权 I/O 指令 (如 IA-32 的 in/out, System/360 的 SIO) 来完成。
    • 用户态程序不能直接执行这些操作,必须通过系统调用进入内核。
    • 在虚拟机中,这些特权操作触发陷阱,交给 VMM 来处理 → 所以 VMM 很容易“截获”到。
  • 难点
    • 一次完整 I/O 动作(例如“磁盘读一块”)通常不是一条 I/O 指令完成的,而是由设备驱动发出的 一系列小粒度操作(读写寄存器、加载地址、分段传输)。
    • VMM 截获到的只是零散的低级指令,必须 反向推导 (reverse engineer) 才能理解高层 I/O 意图。
    • 这种推导非常复杂,在实践中几乎不可行。

📌 总结:截获简单,但难以理解语义。

Virtualizing at the Device Driver Level (驱动级虚拟化)
  • 机制
    • 操作系统把系统调用(如 read())转换成 驱动调用,交给设备驱动。
    • 如果 VMM 能拦截驱动层接口,就能把 虚拟设备请求 → 物理设备驱动请求
    • 实现方式:
      • 为每个客户 OS 提供一套 虚拟设备驱动(例如 VirtualBox/VMware Tools 中的“虚拟网卡/虚拟磁盘驱动”)。
      • 安装客户 OS 时,虚拟驱动也会随 VMM 一起安装。
  • 优点
    • 截获点自然,不必拼凑零碎的 I/O 操作。
    • 可以直接利用客户 OS 的抽象层次。
  • 缺点
    • 需要对客户 OS 内部接口有了解。
    • 对未知/任意 OS 很难做到,但对主流 OS(如 Windows, Linux)是可行的。
  • 扩展
    • 原生 VM 系统 中,VMM 需要同时拥有 虚拟设备驱动物理设备驱动
    • 托管 VM 系统 中,VMM 可以直接“借用”宿主 OS 的驱动(例如 Linux 的驱动),减少开发成本。

📌 总结:适合常见 OS,开发成本较高,但效果好。

Virtualizing at the System Call Level (系统调用级虚拟化)
  • 机制
    • 在 ABI 层直接截获系统调用(例如应用程序的 read())。
    • VMM 模拟整个 ABI 接口,由它直接完成 I/O 请求。
  • 优点
    • 截获在最上层,一次性理解完整的 I/O 意图,不需要反向推导。
  • 缺点
    • VMM 必须为每个客户 OS 编写一整套 ABI 模拟库
    • 开发难度极高,因为需要完整理解客户 OS 的系统调用语义,并正确模拟与 OS 其他部分的交互。
    • 只有在 客户 OS 结构非常清晰且被深度掌握 的情况下才可能实现。

📌 总结:理论上最简洁,但实际难度最大,几乎只适合非常有限的场景。

网络虚拟化示例
  • 场景一:客户VM发送数据包至外部主机。
    • 客户OS生成OUTS指令 → VMM截获并转换为目标物理NIC端口指令。
    • 数据地址转换至VMM空间,由物理NIC驱动发送。
  • 场景二:客户VM间通信。
    • VMM识别目标为本地VM,不触发物理信号,直接在内部转发数据包。
    • 目标VM的中断处理程序接收数据,实现高效零拷贝通信。
  • 可通过VMM内核层重定向进一步提升效率,如z/VM的minidisk缓存减少磁盘I/O。

Input/Output Virtualization and Hosted Virtual Machines

  • 托管型 VM:运行在已有宿主操作系统 (host OS) 上,依赖宿主 OS 提供底层服务。
  • 原生型 VM (native VM) 不同,它不直接管理所有硬件,而是把很多工作交给宿主 OS 完成。
  • 优势:省去了 VMM 自己实现完整设备驱动的负担(PC 生态下设备种类繁多,驱动维护难度大)。

为了实现高效虚拟化,托管型 VM 被分成三个主要部分:

  • VMM-n (native 部分)
    • 运行在硬件的最高特权级(内核态)。
    • 类似于原生 VMM 的角色,负责拦截客户机执行的特权指令或关键指令(通过陷阱或补丁)。
    • 可能为少数性能关键或宿主 OS 不支持的设备提供专用驱动。
  • VMM-u (user 部分)
    • 运行在宿主 OS 的用户态,表现为一个普通进程。
    • 负责代表 VMM-n 向宿主 OS 发起 资源请求(例如内存分配、I/O 调用),通常调用宿主 OS 的库函数(如 read())。
    • 等于把客户机的请求转译为宿主 OS 的请求。
  • VMM-d (driver 部分)
    • 是安装在宿主 OS 内核中的一个特殊驱动。
    • 它让 VMM-n 看起来像宿主 OS 上的一个“设备”,从而使 VMM-u 能与 VMM-n 通信。
    • VMM-u 是唯一能访问这个“设备”的用户程序。
4. 优点
  • 驱动复用:不需要 VMM 自己维护庞大的驱动库,直接利用宿主 OS 的现有驱动。
  • 易部署:用户可以在已有操作系统上快速安装虚拟机系统,非常适合 PC 桌面环境。
5. 缺点
  • 安全风险:VMM-n 与宿主 OS 都运行在特权级,彼此可能互相干扰;宿主 OS 并未以 VMM 共存为前提来设计。
  • 资源不可控:资源调度完全由宿主 OS 决定,VMM 难以掌握或预测实际资源分配策略。
  • 性能损耗
    • 频繁发生 世界切换 (world switch):在 VMM-n 与宿主 OS 之间切换时,需要保存和恢复大量状态。
    • 特别是在 I/O 密集型工作负载下,性能下降比 CPU 密集型场景更显著。

Input/Output Virtualization in VM/370

背景:VM/370 是 IBM 的经典系统虚拟机,直接运行在硬件上(原生型 VMM)。

机制

  • 通道 (IOP) + CCW (通道命令字):I/O 操作通过 CCW 链描述。
  • 客户机构建 CCW 链 → 执行 SIO → 陷入 VMM (CP)。
  • CP 查影子页表,生成对应的物理 CCW,保证数据页在内存中。
  • 如果跨页,要拆分为多个 CCW。
  • CP 把 CCW 交给物理设备执行,I/O 完成后通过中断返回,再由 CP 注入到客户机。

优化点

  • spooling:对于慢速设备(如打印机),采用缓冲池,避免阻塞,提高并发。
  • 虚拟设备:比如 minidisk,没有直接物理对应,方便资源高效共享。