CPU 虚拟化
CPU虚拟化基础知识
CPU虚拟化是一种技术,它允许多个操作系统在同一个物理CPU上运行。这种技术可以提高计算机的性能和安全性。
CPU 虚拟化是系统虚拟化技术中最核心的部分,因为 CPU 是计算机中最核心的组件,直接控制着整个系统的运行。同时内存访问(内存虚拟化)与 I/O 操作(I/O 虚拟化)也都直接依赖于 CPU,因此 CPU 虚拟化是系统虚拟化技术中的核心。
CPU 虚拟化技术通过虚拟机监控器(Virtual Machine Monitor,VMM)或称为 Hypervisor 来实现。
在 Gerald J. Popek 与 Robert P. Goldberg 的合作论文《Formal Requirements for Virtualizable Third Generation Architectures》 中提出了满足虚拟化系统结构的 VMM 的三个充分条件:等价性,资源控制,效率性。为了满足这个条件, CPU 虚拟化使用的经典模型是「Trap & Emulate」,使用特权级压缩(Ring Compression)的方式来实现虚拟环境:
Hypervisor 具备最高权限,而 Guest VM 则处于较低权限。在硬件层面上,Guest VM 直接执行非敏感指令。当涉及敏感操作时,Guest VM 会被引导至拥有最高权限的 Hypervisor,从而由 Hypervisor 模拟并处理敏感指令。
在进行虚拟CPU调度时,我们先保存当前vCPU的状态。然后,恢复Hypervisor的状态,让它继续执行其任务。完成后,Hypervisor将开始下一轮的虚拟CPU调度,恢复下一个vCPU的状态并重新开始执行。
x86 虚拟化遇到的问题
在虚拟化技术起步阶段,x86架构并未能充分支持经典的虚拟化架构「Trap & Emulate」,导致系统虚拟化面临诸多挑战。这一问题限制了直接且高效的系统虚拟化实现。
Intel 分级保护环将权限分为 ring0~ ring3,其中操作系统内核运行在 ring0 权限而用户进程运行在 ring3 权限。
Ring0是指CPU的运行级别,是最高级别的权限,可以访问所有数据和内存。Ring1、Ring2、Ring3分别是较低的权限级别,只能访问本层以及权限更低层的数据。
在系统虚拟化的经典架构「Trap & Emulate」中,Guest OS 全部运行在 ring3,当涉及到一些敏感指令时,VM 触发 General Protection 异常,由 VMM 进行截获并处理,但不是所有敏感指令都是特权指令,不是所有的敏感指令都有触发异常以让 VMM 介入的机会, x86 架构中一共有 17 条非特权敏感指令。
在x86架构下,若尝试在用户态使用popf指令修改eflags寄存器的中断开关位(IF),系统将不会引发任何异常,这使得虚拟机管理器VMM无法介入。
"硬件有限,软件补足。" 在硬件尚未对虚拟化提供充分支持前,Hypervisor 转而从软件层面施展拳脚。于是,两种纯软件虚拟化技术应运而生:模拟执行(VMWare)与直接源代码改写(Xen)。
在软件虚拟化技术已经发展成熟多年之后,x86 架构对虚拟化的支持才姗姗来迟:「硬件辅助虚拟化」(Intel VT)开始出现在人们的视野当中。
纯软件实现 CPU 虚拟化
在探讨虚拟化技术的过程中,我们曾提到x86架构的非特权敏感指令限制了虚拟机监视器(VMM)对虚拟机(VM)敏感行为的有效拦截。这一问题违反了Popek和Goldberg提出的虚拟化需求。因此,在硬件层面对虚拟化的支持尚未实现之前,虚拟化厂商不得不从软件层面寻求解决方案。
模拟 & 解释执行
「模拟」技术早在虚拟化之前就已经出现了。纯软件的模拟本质上是通过编写能够呈现出与被模拟对象相同行为的应用程式来达到运行非同构平台应用程序的效果。
虚拟机模拟技术可以应用于程序级别的模拟,也可以应用于系统级别的模拟。CPU 运行的本质行为其实就是从 PC 寄存器所指内存区域中不断取出指令解码执行。实现一个虚拟机最简单粗暴的方法便是通过模拟每一条指令对应的行为,从而使得 VM 的行为对 VMM 而言是完全可控的。
您好,模拟技术的原理是通过解释执行的方式来实现模拟技术,即模拟器程序不断地从内存中读取指令,并模拟出每一条指令的效果。从某种程度而言,每一条指令在执行时都完成了 “陷入”,因此我们可以使用模拟技术解决虚拟化的漏洞,同时还能模拟与物理机不同架构的虚拟机。
Qemu,即Quick Emulator,是一款强大的模拟器,完整地模拟了一套包括各种外设在内的计算机系统。
模拟技术在解释执行时存在致命弱点:性能低下。每条指令都需经过VMM解析并由其模拟执行,即使最简单的指令也可能需分解成多步骤和多次内存访问,导致效率极低。
让我们深入探讨为何在x86架构上应用模拟技术以实现虚拟机:Popek和Goldberg的虚拟化要求因非特权敏感指令的存在而受到挑战,但这类指令仅占少数。大部分指令仍可直接在物理硬件上运行,因此基于模拟技术的改进虚拟化技术应运而生:扫描与修补以及二进制翻译。
扫描 & 修补
在虚拟化环境中,虚拟机通常与物理机共享相同的指令集架构(ISA)。因此,我们无需采用纯模拟技术来实现虚拟机。相反,我们可以让非敏感指令直接在硬件上执行,并通过某种方法使非特权敏感指令进入虚拟机管理器(VMM),从而重新实现Trap & Emulate模型。
扫描 & 修补技术是一种让非敏感指令直接在硬件上执行,同时将系统代码中的敏感指令替换为跳转指令等能陷入 VMM 中的指令,从而让 VM 在执行敏感指令时能陷入 VMM,使得 VMM 能够模拟执行敏感指令的效果的技术。这种技术可以用于保护计算机系统的安全,防止恶意软件对计算机系统造成损害。
基本执行流程如下:
VMM在VM执行每段代码之前对其进行扫描,解析每一条指令,查找特权与敏感指令。 敏感指令是指操作特权资源的指令,包括修改虚拟机的运行模式或者下面物理机的状态;读写时钟、中断等寄存器;访问存储保护系统、地址重定位系统及所有的I/O指令。
补丁代码执行结束后,再跳转回 VM 中继续执行下一条代码
扫描修补技术可以在物理CPU上运行大部分代码,性能损失较小。但是,扫描修补技术也存在一些缺陷,例如特权和敏感指令被模拟执行,因此有的指令模拟时间会较短有的会非常长;特权和敏感指令引入的额外的跳转开销等 。
特权指令与敏感指令仍通过模拟执行的方式完成,仍可能造成一定的性能损失
代码补丁当中引入了额外的跳转,这破坏了代码的局部性
局部性原理是指CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。这个原理是计算机科学中的一个基本概念,它可以帮助我们更好地理解计算机内存的使用方式和优化计算机内存的使用效率。
如果您需要优化文章内容,可以考虑以下几点:
- 简化文章语言,使其更容易理解。
- 突出文章重点,去除无关信息。
- 增加吸引力,例如添加图片或图表等。
VMM 需要维护一份补丁代码对应的原始代码的副本,这造成了额外的开销
二进制翻译
为了提升虚拟化性能,一种名为「二进制代码翻译」(Binary Translation)的技术应运而生。这种技术与「扫描 & 修补」相似,都能在运行时动态修改代码。不过,BT 技术以基本块(具有单一入口和出口的代码块)作为翻译单元。
Emulator 是一种软件,它可以将输入的二进制代码翻译成对应 ISA 的不包含特权指令和敏感指令的子集,从而使其在用户态下安全运行。
Emulator通过动态创建翻译缓存(translation cache)来为即将执行的基本块提供空间。这个翻译缓存存储了经过转换的代码,每一块翻译缓存(TC)都与原始代码以哈希表等映射方式相联系。
二进制代码翻译与扫描修补技术在原理上具有相似性,但后者仅针对敏感和特权指令进行修复。扫描修补技术保持代码结构不变,仅替换敏感和特权指令为能触发VMM的指令;而二进制代码翻译直接改变基本块的代码结构,如将长度40B的基本块翻译为100B,内部代码位置也发生变化。
翻译方法大致分为以下两种:
在相同 ISA 架构上大部分指令都是可以直接进行等值翻译的,除了以下几种:
PC相对寻址指令是一种与PC相关的寻址方式,但在进行二进制翻译后会更改代码基本块的结构。因此,这类指令需要额外插入一些补偿代码来确保寻址的准确,从而造成一定的性能损失。
运行时动态目标地址的间接控制指令,如间接调用、返回和跳转,导致在翻译时无法确定跳转目标。
特权指令处理。对于简单的特权指令,可直接转换为等值代码(如 CLI 指令可转换为置 VCPU 的 flags 寄存器 IF 位为 0)。然而,对于稍复杂的指令,则需进行深度模拟,借助跳转指令进入虚拟机管理器(VMM),这通常会带来一定的性能损耗。
例如这是 QEMU 中的一个基本块代码翻译的例子:
自修改代码(Self Modifying Code)是一种在运行时能够修改自身执行代码的程序。为了适配这种特性,我们的模拟器需要对新生成的代码进行重新编译和解释。
自参考代码(Self Referential Code)是一种特殊的程序设计,它会在运行过程中读取自身的代码段。为了避免误解,我们需要对其进行特殊处理,使其能够正确读取原始代码段而非翻译后的版本。
精确异常(Precise Exceptions)是指在代码翻译执行过程中出现的中断或异常。为了恢复到原状态,需要将运行状态回滚至异常点,然后交给Guest OS处理。由于翻译后的代码与原代码已失去逐条对应关系,BT技术难以有效应对此类情况。为解决此问题,可采用回滚策略并重新使用解释执行方式。
硬件辅助虚拟化
硬件辅助虚拟化技术(Hardware-assisted Virtualization Technology,简称HVM)是一种在芯片级别支持全虚拟化的技术,能够在一台电脑内同时运行多个操作系统。Intel VT-x是Intel公司推出的一种硬件辅助虚拟化技术,可以在Windows系统中开启VT-x模式,从而提高系统的安全性和性能。
概述
Intel VT-x 技术是 Intel 为 x86 虚拟化所提供的硬件支持,其中用于辅助 CPU 虚拟化的是 Intel VT-x 技术,其扩展了传统的 IA32 处理器架构,为 IA32 架构的 CPU 虚拟化提供了硬件支持。
VT-x 技术为 Intel CPU 额外引入了两种运行模式,统称为 VMX 操作模式(Virtual Machine eXtensions),通过 vmxon 指令开启。这两种运行模式都独立有着自己的分级保护环:根操作模式和非根操作模式。
根操作模式是 VMM 运行所处的模式,而非根操作模式是客户机运行所处的模式。 通过 VM-Exit 和 VM-Entry,CPU 在 VMM 和客户机间切换,并利用 VMCS 保存和管理虚拟 CPU 的状态。
VMX Root Operation:Hypervisor 在此模式下,能访问所有计算机资源并对虚拟机进行调度。
VMX Non-Root Operation是一种虚拟化技术,它可以让虚拟机在非根模式下运行。在这种模式下,虚拟机只能访问非敏感资源,例如I/O操作。当虚拟机执行一些访问全局资源的指令时,将导致虚拟机退出操作(VM-Exit),从而使虚拟机监控器获得控制权,以便对访问全局资源的指令进行模拟 。
Root模式和Non-Root模式是虚拟机监视器(VM)中的两种CPU操作级别。在Root模式下,CPU可以访问所有的硬件资源,而在Non-Root模式下,CPU只能访问部分硬件资源。当虚拟机需要访问某些硬件资源时,它会从Non-Root模式切换到Root模式。这个过程称为VMX转换(VMX Transition)。
VM-Entry:Hypervisor 保存自身状态信息,切换到 VMX Non-Root 模式,载入 VM 状态信息,恢复 VM 执行流。
在 VM-Entry 阶段,Hypervisor 会保存自身状态信息,并切换到 VMX Non-Root 模式。接着,它会载入 VM 状态信息,并恢复 VM 的执行流。
VM-Exit 是指虚拟机在运行过程中暂停并保存自身状态信息,然后切换到 VMX Root 模式,加载 Hypervisor 状态信息,并执行相应的处理函数。这是一个非常重要的过程,可以确保虚拟机的稳定性和安全性。
以下是一些关于 VM-Exit 的
VM-Exit 是虚拟机在运行过程中的一种重要机制,它可以让虚拟机在暂停时保存自身状态信息,并切换到 VMX Root 模式,以便加载 Hypervisor 状态信息并执行相应的处理函数。这个过程对于保证虚拟机的稳定性和安全性非常重要。
希望这些信息能够帮到你。如果你还有其他问题,请随时问我。
Non-Root 模式与 Root 模式各自拥有独立的分级保护环境,使得 Host OS 和 Guest OS 无需修改即可在各自的模式下直接运行于硬件。仅在涉及敏感资源访问以及 Host OS 对 VM 调度时,才会出现切换。这种设计既确保了 VM 的高性能,又满足了 Trap & Emulate ` 模型实现的需求,同时解决了 x86 架构的虚拟化漏洞问题。
VMCS
VMCS是保存虚拟机的虚拟CPU的相关状态的一块内存,每个virtual CPU都有一个VMCS,同一时刻物理CPU只能和一个VMCS绑定,即一个物理CPU对应一个虚拟CPU,但不同时刻可以将VMCS绑定到不同物理CPU上,即一个虚拟CPU可以选择对应的物理CPU,称为VMCS的迁移。
VMCS(Virtual-Machine Control Structure)是用以保存 CPU 虚拟化所需要的相关状态的一块内存,每个 virtual CPU 对应有一个 VMCS,同一时刻一个物理 CPU 只能与一个 VMCS 绑定,反之亦然,但在不同的时刻我们可以将 VMCS 绑定到不同的物理 CPU 上,称之为 VMCS 的迁移(Migration)。
与 VMCS 的绑定与解绑相关的是以下两条指令:
Instruction
Description
VMPTRLD
将指定的 VMCS 与执行该指令的 CPU 进行绑定
VMCLEAR
将执行该指令的 CPU 与其 VMCS 进行解绑
struct VMCS { /* 版本号,4字节 */ uint32_t vmcs_revision_identifier:31, shadow_vmcs_indicator:1;
/* 中止标识,4字节 * 当 VM-Exit 失败时便会产生 VMX 中止,并在此处存放原因 */ uint32_t vmx_abort_indicator;
/* 数据域 */ struct VMCSData vmcs_data;};
VMCS 数据域 存放着 VMCS 主要的信息,分为以下六个子域:
Host-state area: 保存 Hypervisor 寄存器状态,在 VM-exit 时恢复。
"VM-execution control fields: 管理非Root模式下的处理器行为,提升系统稳定性与安全性。"
VM-entry control fields是控制VM-Entry过程中的某些行为。在vm-entry时,如果CPU检查到这些字段没有被正确填写,将会抛错并退出 。
VM-Exit控制字段:管理VM-Exit过程中的关键行为,确保系统稳定与高效运行。
VM-exit information fields:保存 VM-Exit 的基本原因及其他详细信息,在一些处理器上该域为只读域。
我们可以通过以下两条指令读写 VMCS:
Instruction
Description
VMREAD <索引>
读 VMCS 中 “索引” 指定的域
VMWRITE <索引> < 数据 >
向 VMCS 中 “索引” 指定的域写入数据
在数据域中,Intel为每个字段赋予了独特的索引值,而非偏移量。以Guest State Area中的ES段选择子为例,其索引值为0x00000800。
虽然将所有域的索引背下来并不切实际,但最佳策略仍然是频繁查阅表。推荐阅读Intel SDM中的《Intel 64和IA-32架构软件开发者手册》第3C卷:系统编程指南,第三部分。
VMX 操作模式
VMX 操作模式是 Intel VT-x 扩展的一种,它有两种操作模式:VMX 根操作模式和 VMX 非根操作模式。当处理器运行于 VMX 根操作模式时,它类似于运行在没有打开 VMX 功能的环境下,但是可以执行 VMX 相关指令,并且有某些寄存器的写被限制了。
默认情况下,VMX 操作模式是关闭的,只有当 VMM 需要使用硬件辅助虚拟化功能时才会使用 Intel 提供的两条新指令来开关 VMX 操作模式:VMXON 和 VMXOFF。
VMXON:开启 VMX 操作模式。
VMXOFF:关闭 VMX 操作模式。
在 Intel SDM 中描述的 VMX 生命周期如下:
软件通过 VMXON 指令进入 VMX 操作模式。
VMM通过VM entries轻松进入Guest VM,但请注意,单次只能执行一个VM。要启用VM entry,您可以使用VMLAUNCH(首次进入VM)和VMRESUME(从VMM恢复到VM)指令。完成VM操作后,通过VM exits即可重新获得控制权。
"VM exits通过VMM设定的入口点,将控制权移交。在VMM对退出原因做出响应后,它使用VM entry返回到VM中。"
VM entry:从 Hypervisor 切换到 VM
检查 VMCS 合法性(各字段值是否合法)。
加载 VMCS 的 Guest-state area 中的各字段到对应的寄存器。
加载指定的 MSR。
设置 VMCS 的状态为 launched。
根据需求,通过编写VMCS的VM-entry Interrucption-Information,向虚拟机(VM)注入事件,如异常和异步中断等。
VM exit:从 VM 切换到 Hypervisor
将VM退出原因及详细信息记录于VMCS的VM-exit information fields,以便后续处理。
将 VM 的寄存器保存至 VMCS 的 Guest-state area 。
从 VMCS 的 Host-state area 中恢复 Host 寄存器。
加载指定 MSR。
Model Specific Register,简称 MSR,是 x86 下的一组用来控制 CPU 运行、功能开关、调试、跟踪程序执行、监测 CPU 性能等方面的寄存器。
例如 syscall 指令便是通过 MSR 寄存器来获取到内核系统调用的入口点。
每个 MSR 寄存器都有一个独特的 ID,称为 MSR Index。借助 RDMSR 和 WRMSR 指令,我们可以轻松读写指定的 MSR 寄存器。
KVM & QEMU-KVM
Kernel-based Virtual Machine(KVM)是一种集成在Linux内核中的开源系统虚拟化模块,自Linux 2.6.20版本起便开始预装。它本质上是一个硬件辅助的、位于内核层的虚拟机管理程序(Hypervisor)。通过将Linux内核转变为Hypervisor,KVM提供了用户态操作虚拟机的接口,即/dev/kvm。我们可以通过ioctl指令对KVM进行控制。
KVM 本身仅提供了 CPU 与内存的虚拟化,不能构成一个完整的虚拟化环境。为了提高虚拟机性能,我们可以复用现有的全虚拟化方案,将模拟 CPU 与内存的工作交由 KVM 完成,这样便能直接通过 KVM 来借助硬件辅助虚拟化。
QEMU 支持通过 KVM 来创建与运行虚拟机,利用 QEMU + KVM 进行虚拟化的方案如下:在 QEMU/KVM 中,客户机可以使用的设备大致可分为三类:模拟设备、Virtio 设备和 PCI 设备直接分配。
在 VM-Exit 产生时,KVM 接管并分析原因,决定是继续运行还是交由 QEMU 处理。
QEMU的基本执行框架源自accel/kvm/kvm-all.c中的kvm_cpu_exec(),它为虚拟化提供了核心支持。
int kvm_cpu_exec(CPUState *cpu){ //... cpu_exec_start(cpu);
do { //... /** * 开始运行 VM,本质上就是 ioctl(kvm_fd, KVM_RUN) * 当产生 VM-Exit 时,首先在 KVM 中完成处理, * 若产生 IO,则退出内核态,即恢复到这里,接下来进入到用户态的处理 */ run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);
//... if (run_ret < 0) { /** * 返回值小于 0 说明 VM 运行出了些问题, * 这里会简单处理后 break 打破大循环 */ //... }
trace_kvm_run_exit(cpu->cpu_index, run->exit_reason); /* 这里就是一个大的 switch,根据退出的原因进行不同的处理,就不放完整代码了 */ switch (run->exit_reason) { case KVM_EXIT_IO: DPRINTF("handle_io\n"); /* Called outside BQL */ kvm_handle_io(run->io.port, attrs, (uint8_t *)run + run->io.data_offset, run->io.direction, run->io.size, run->io.count); ret = 0; break; case KVM_EXIT_MMIO: DPRINTF("handle_mmio\n"); /* Called outside BQL */ address_space_rw(&address_space_memory, run->mmio.phys_addr, attrs, run->mmio.data, run->mmio.len, run->mmio.is_write); ret = 0; break; //... default: DPRINTF("kvm_arch_handle_exit\n"); ret = kvm_arch_handle_exit(cpu, run); break; } } while (ret == 0);
/* 运行结束,收尾处理 */ // ...
-对此,您有什么看法见解?-
-欢迎在评论区留言探讨和分享。-
领取专属 10元无门槛券
私享最新 技术干货