虚拟化原理与IA虚拟化扩展

Published: 四 23 十二月 2021

In Kernel.

在过去一年的工作中,经常需要使用Vmware/Qemu等工具,使用过程中遇到了很多问题没有解决,而虚拟化技术的封装能力(快照)与特权能力(事件记录与回放,调试,监视)也能在下一阶段为自己提供便利,因此打算接下来写一点这方面的笔记...

基本概念

实现层次

现代系统的多任务并发其实就有虚拟化的思维,包括CPU与内存虚拟化,现在先看看根据实现层次的分类:

1.功能级虚拟化:这是为了充分运用硬件的每一个部分而提出的一种技术,如为了提高内存使用率(当然也有保护机制的功能)而实现的内存虚拟化,为了提高网卡各部件利用率而实现的SR-VIO等。

2.语言级虚拟化:这指语言虚拟机,它们对底层的CPU进行虚拟化,如Java虚拟机,Python虚拟机,此时在其上运行的是专用于该虚拟机的指令,一般为字节码,再由虚拟机将其翻译为实际物理机的指令,此时一般采用解释执行的方式,更进一步会采用JIT在运行时采样编译为机器码执行。

3.接口级虚拟化:这是一种在同架构下执行其他系统二进制程序的技术,Windows下的程序无法直接在同架构的Linux下系统执行,因为它们的ABI不一致,这涉及可执行文件格式与依赖库,前者只需要编写相应的加载程序,后者面临的问题更加复杂,它涉及很多闭源的依赖库,为此需要完全重新实现相同功能的库,此类软件有如Linux下的wine与mac下的crossover。

4.容器级虚拟化:这是一种利用操作系统隔离机制实现虚拟化的技术,在此技术下对于容器内的应用它仿佛运行在一台普通的虚拟机中,但在外部看来它只是一个普通进程,只是由命名空间等实现了一个jail,它和宿主系统使用了同样的内核,因此会有一些ABI兼容性限制,此处就是指LXC及其上的Docker。

5.系统级虚拟化:这是一种更完整的虚拟化技术,该技术下需要虚拟完整的硬件,其实对于软件来说,它只关注CPU/内存/外设(中断与IO)这三点,因此虚拟出这三点时就能实现完整的系统虚拟化。

img

注:1.也有把系统级硬件级,而把容器级叫做系统级...

2.还有种常见的分类,根据功能来分,如网络虚拟化,存储虚拟化等,感觉现在没什么人挖它...

实现方式

虚拟化技术有三个指标:同质,高效,资源受控,现在的虚拟化也围绕这三个目标展开,在硬件级虚拟化时有如下三类:

1.全虚拟化(Full Virtualization):全虚拟化技术能最好的实现一致性,它会完全模拟所有过程,此时对客户机来说它不需要做任何改变,因此适用性很强,但是原样的实现硬件功能会带来很多不必要的性能开销,因此该方式在某些地方性能较差。

2.类虚拟化(Para-Virutalization):类虚拟化能更好实现等效性,它需要对宿主机做一定的修改,或在宿主机里安装一些驱动,令其通过一些额外的方式与VMM通信从而实现性能提升,该技术会增加实现难度但能显著提升性能,然而很多虚拟机逃逸漏洞也出在这一块。

3.半虚拟化(Partial-Virtualization):它只实现了部分虚拟化,是个不完全体,没见过。

注:由于类虚拟化不满足三代虚拟机中提出的一致性,因此翻译叫"类"虚拟化,大多场景也会被翻译为半虚拟化,需意识他们可能是同样的东西。

而在实现方式上,有如下三种:

1.软件完全虚拟化:完全用软件实现虚拟化功能,不受硬件制约,但实现复杂效率偏低

2.硬件辅助虚拟化:Intel与AMD都提出了各自的CPU硬件虚拟化功能VT与AMD-V,硬件辅助的好处就是效率高实现简单

3.类虚拟化:完全虚拟化时VMM只能知道Guest的部分状态,存在语义鸿沟,而且不可避免的存在代码重复降低性能,类虚拟化用于弥补这两个缺陷

VMM架构

虚拟机系统里,会出现三个角色,宿主机Host,虚拟机监视器VMM(也叫Hypervisor),客户机Guest(本文也叫它VM),VMM(或与Host系统)向下管理各种资源,向上提供各种服务,即它们要实现两件事,物理资源的管理与虚拟资源的管理,从这种实现架构上可分为三种架构:

1.Hypervisor型:这两件事都由VMM完成,效率高更安全但是实现复杂,如Vmware ESX server,KVM+Kernel

2.Hosted型:物理资源管理由宿主机系统完成,虚拟化与虚拟资源管理由VMM实现,优点是复用现有系统实现简单,缺点嘛就是暴露面多啦,且效率相对低点,如Vmware Workstation

3.Mixed型:VMM实现物理资源管理,但再创建一个特殊虚拟机去复用已有系统的某些功能,优点就是简单与安全,缺点就是效率降低了,如XEN

传统上也把前面两种架构叫做一型和二型:

实现时,VMM可大致分为三部分:

dispatcher:VM退出时根据退出原因分发到不同模块去处理

allocator:分配各种所需资源,很多资源都需要为其建立一份虚拟拷贝,VM读写时都是操作这份拷贝(shadow structures)

interpreter:使用等效的代码解释导致VM退出的特殊指令/事件

Intel VT

最初的X86架构存在虚拟化漏洞(virtualization hole,见下文),为了方便实现且减小开销Intel和AMD都在后续通过新增处理器扩展来弥补该缺陷,本文只关注Intel VT技术,此处列出技术名称与简单描述,细节见后文:

技术名称 说明
Intel® VT-x CPU虚拟化扩展,通过新增非根工作模式实现执行环境隔离
Extended Page Table (EPT) 扩展页表,内存虚拟化扩展,通过增加一次内存地址转换实现内存虚拟化
Virtual Processor IDs (VPID) 虚拟处理器ID,辅助地址翻译缓存,具体来说就是为物理处理器与虚拟处理器编号,从而使TLB可以识别缓存的数据不必在切换CR3时全部刷新
VGuest Preemption Timer 客户抢占计时器,通过VMM编程设置计时,到期时VM会退出
Intel® VT-d 直接IO,IO虚拟化扩展,它其实位于芯片组上,实现DMA到MEM的地址翻译与保护,从而可安全的实现设备直通(pass-through),当然也可用于通常的驱动中,详见PCIe笔记。

与IO相关的技术还有很多,这些技术不仅用于系统虚拟化,也用于通常的网络/存储虚拟化或单纯的驱动中,用以提高性能:

技术名称 说明
Interrupt-Remapping Support 中断重映射,将中断重新映射与路由到vCPU
Queued-Invalidation Support Queued-Invalidation enables the VMM to batch digital media translation invalidations. This gives the end user better performance(没懂)
Address Translation Services (ATS) support 地址翻译服务使PCIe设备能直接缓存用于DMA重映射的地址(其实没懂,TODO)
Large Intel VT-d Pages The Large Intel VT-d Pages feature enables 2MB and 1GB pages in Intel VT-d page tables. It enables the sharing of Intel VT-d and EPT page tables. (也没懂,TODO)
Virtual Machine Device Queue(VMDq) 虚拟机设备队列实现了在硬件设备中将数据包排序分组,VMM只需根据分组分派给VM从而增大吞吐率(Offload是硬件优化的基操)
Single-Root I/O Virtualization (SR-IOV) 单根IO虚拟化通过共享配置空间独享BAR来提高硬件利用率,它将一个PF虚拟为多个VF,从而使VM或驱动直接使用多个设备,VMDc(Virtual Machine Direct Connect)也是基于此实现的。

另外还有几个没见过的,只列出:

Intel® VT FlexPriority :它通过优化VM中断处理来提升效率,具体说就是通过新虚拟的TPR表示当前可处理的中断优先级,VM可直接操作它,从而消除大多数Guest访问TPR(Task Priority Registers)时的退出来减少上下文切换次数,增加IO等吞吐率

Intel® VT FlexMigration:虚拟化灵活迁移技术可以帮助VMM建立改进的兼容性池,通过所有的服务器暴露一致的指令集以实现实时迁移(包括以后加入升级后的硬件)

Intel® Trusted Execution Technology:可信执行技术通过硬件提供安全性来保护免受软件攻击,具体上通过硬件实现可信启动,配置保护,数据销毁

虚拟化部分

CPU

在正式内容前先介绍几个相关概念:

上下文

下上下文在计算机中也是很常见的,如加解密,加解压等涉及状态的操作都会有,这里所谓上下文就是执行时的环境状态,以一个进程为例,它在某一时刻的上下文包含它的此时处理器寄存器值,内存数据等,若遇到进程切换,就需要保存换出进程的上下文并加载换入进程的上下文,这是以CPU为中心的,因此切换都需要保存与恢复寄存器,这些动作有些必须由硬件完成,有些一定需要的操作可能也会由硬件完成,其他可选的操作由软件来完成,从而最大限度节省切换开销。在接下来的理解中要想到总需要减少上下文切换来提高性能。 ​

敏感指令与特殊指令

1.敏感指令:用户模式和内核模式行为不同的指令,它们将会感知或影响Host环境,包括修改VM模式,读写Host状态,读写敏感寄存器/内存,IO操作等

2.特权指令:用户模式下执行会被内核捕获的指令 ​

虚拟化只关注且必须关注敏感指令,对其进行拦截与模拟。指令执行有如下四种,它们最终的效果一致:

CSIM(complete software interpreter machine):所有指令解释执行

VMM:它要求在真实处理器上执行虚拟处理器指令的“statistically dominant subset”,部分特权指令可本地执行

HVM(Hybrid VM):特权指令解释执行,其他本地(处理器直接)执行

RM(Real Machine):所有指令直接本地执行

软件

陷入与模拟(trap-and-emulate)

在实现时,最直观的方式就是使用降级(De-privileging),CPU虚拟化需要它的敏感指令和它的特权指令一致,这样就能将虚拟机放在低权限下运行,执行敏感指令时会由于权限不够的异常陷入宿主机,宿主机就可以对敏感指令进行监视执行,但有些架构由于设计问题存在虚拟化漏洞,像IA架构它在这方面就存在一些不一致,如下[10]总结了奔腾下的敏感非特权指令:

指令 说明
SGDT/SIDT/SLDT 可获取GDTR/IDTR/LDTR,它们里面会存地址等信息,但主要是写会被截获而读获取真实信息导致的读写不一致
SMSW 能读到CR0的低16位,可获取很多状态信息,如运行模式
PUSHF/POPF/PUSHFD/POPFD PUSHx可获取EFLAGS信息,POPx虽然敏感修改不生效但是也不会抛异常
LAR/LSR/VERR/VERW LAR与LSR可访问SR的权限与限制,VERR与VERW可检查并暴露特权级
PUSH/POP 它可以读写SR
CALL/JMP/INT n/RET 在CALL与JMP在执行远跳转时会加载新的SR/TR,特权SR会出问题,INT会把EFLAGS放栈上,RET会远跳可能会清SR
STR 它可以读任务寄存器,TR里有RPL
MOV 它可以读段寄存器,SR里的CPL可暴露运行特权级,另外写特权SS也会出现问题

这种不一致会为虚拟化带来很大的障碍,这使VMM必须通过某些方法让这些敏感非特权指令的执行受审查,软件完全虚拟化有如下两种思路:

1.解释执行(interpretive execution):它解释每条指令,可以应对不同ISA的VM,但可能每条指令都需要上百条指令去模拟,效率较低

2.扫描与修补(scan and patch):它在执行一个段代码(如代码块)前先扫描是否存在敏感指令,若存在则将其备份并替换为会退出到VMM的指令

二进制翻译(binary translation)

BT和IE有点像,但是它是按代码块进行翻译,只处理特权指令,而且存在缓存因此更高效,通过一些优化它可能再特定情况下取得优于Native的性能

如上,BT和IE都能实现不同ISA的执行,这部分很类似编译与二进制分析的技术,如可引入AOT/JIT技术等,是个很有意思的话题,之后会再介绍,本文主要关注硬件辅助部分。

硬件

操作模式

Intel的硬件虚拟化新增了一种状态,于是在开启VMX(虚拟机扩展)后原始的状态叫Root Mode而Guest执行时处于non-Root Mode,它们的特权级是正交的,即两种模式都有4个特权级且可以相互配对:

注:对于工作模式,最开始只支持开启分页的保护模式,之后增加了新的支持取消了这个限制,叫unrestricted guest,此时客户机可处于实模式或未启用分页的保护模式。

上图的vmx也是Intel处理器支持硬件虚拟化的能力(features)标志,AMD的叫svm,在Linux下可通过如下命令查看是否支持:

betamao@DESKTOP:~$ cat /proc/cpuinfo | grep -E 'svm|vmx'
flags           :  ...  cmp_legacy svm extapic cr8_legacy abm  ...

在支持VMX时存在如下转换:

要进入非根模式,需要执行以下步骤:

1.通过CPUID.1:ECX.VMX[bit 5] == 1确认CPU支持VMX

2.使CR4.VMXE[bit 13] = 1激活VMXE

3.判断IA32_FEATURE_CONTROL需要处于锁定状态且bit1或bit2需要置位:

4.设置VMXON region与VMCS region,包括设置支持的能力位,详见后文。

5.执行VMLAUNCH进入

VMX指令与环境

两个重要的区域,它们都起始于4K对齐的4K区域(即一整页,但实际使用的大小是结构相关的,VMCS大小由IA32_VMX_BASIC决定),当前该区域的缓存类型必须为UC或WB:

1.VMXON Region:每逻辑处理器(LP)一个,该区域完全由硬件操作,除了写入版本号外,我们不必在意它,它的生命周期贯穿VMXON与VMXOFF。

2.VMCS Region:虚拟机控制结构(virtual-machine control structure)是每虚拟处理器(VP)一个,它在使用时会和某个LP关联,称为该LP的当前VMCS,关注的重点 ​

Intel为虚拟化扩展新增了如下指令,它们分别用于不同目的:

指令 说明
INVEPT 类似INVLPG,但它作用于EPT TLB里的条目,它有两个参数表示刷新指定EPTP还是刷新所有
INVVPID 类似INVLPG,但它根据VPID来使其失效,在未使用EPT时作用域线性映射,否则作用域组合(combined)映射
VMCALL 类似SYSCALL,造成VM退出,用于在VM里调用VMM提供的功能
VMCLEAR 清除VMCS,无论它之前状态如何,此时不再是激活状态,Launch状态,也不再是当前VMCS,而且它会确保数据已同步到VMCS区域
VMFUNC 类似VMCALL,但它调用外部函数却不会退出VM,手册只介绍了EPT切换函数
VMLAUNCH 运行一个全新的虚拟机,当前VMCS必须处于未Launch状态(Clear),执行后会转为Launched状态
VMRESUME 恢复被挂起(由于各种原因退出到VMM)的虚拟机,当前VMCS必须处于Launched状态
VMPTRLD 从指定地址加载VMCS,会使其称为当前VMCS并为激活状态,之前的依然保持激活状态但不再是当前VMCS
VMPTRST 存储当前VMCS地址到指定位置
VMREAD 对当前VMCS里的域进行读操作
VMWRITE 对当前VMCS里的域进行写操作
VMXOFF 离开VMX操作
VMXON 进入VMX操作,它的操作数为VMXON区域地址

注:读写的索引需要进行编码,格式如下full指64位的低32部分,字段类型说明是宿主状态区/客户状态区/控制区/只读信息区等

这些指令执行出错会表现在RFLAGS里,若VMCS有效也会把错误号写入VM-instruction error里,细节详见手册,指令执行导致的状态切换如下:

在软件角度,大多操作都是围绕VMCS区域展开的,该区域是实现相关的,格式如下:

  • VMCS revision identifier 表明该区域对应的LP的版本,只有相同版本的LP才能使用它,该域由软件设置,通过IA32_VMX_BASIC获取,LP只能将版本一致的VMCS作为当前VMCS

  • shadow-VMCS表示这是一个影子VMCS,影子VMCS不能被root模式直接使用,而是被non-root模式使用,用于嵌套虚拟化加速

  • VMX-abort indicator表示发生了VMX abort与原因,软件也可修改它

  • VMCS data 存储了其他数据,它的结构和版本相关。

除此之外,还有一些MSR用于表示VMX的能力,

< tr>
类型寄存器描述
信息IA32_VMX_BASICbit31:0为VMCS ID bit44:32为VMCS大小,以1K为单位,最大4K bit48为1时VMCS相关的地址限制在32位,否则不限制 bit49为1表示支持SMM bit53:50为VMCS支持的缓存类型 bit54为1记录INS/OUTS导致的退出信息 bit55为1表示使用True系控制
IA32_VMX_MISCbit4:0为抢占计时,它表示值所指示的TSC位变化步伐 bit5为1保存LMA到IA-32e mode guest字段里,bit8:6为1分别表示支持HLT/shutdown/wait-for-SIPI状态 bit24:16为支持的CR3个数 bit27:25为支持的MSR load/store数
IA32_VMX_VMCS_ENUM在VMREAD/VMWRITE时会使用索引访问,bit9:1用于表示VMCS里字段的最高索引
VM-executionIA32_VMX_PINBASED_CTLS/
IA32_VMX_TRUE_PINBASED_CTLS[1]
pin-based执行控制能力
IA32_VMX_PROCBASED_CTLS/
IA32_VMX_TRUE_PROCBASED_CTLS
primary-based执行控制能力
IA32_VMX_PROCBASED_CTLS2secondary primary-based执行控制能力
VM-exitIA32_VMX_EXIT_CTLS/
IA32_VMX_TRUE_EXIT_CTLS
退出控制能力
VM-entryIA32_VMX_ENTRY_CTLS/
IA32_VMX_TRUE_ENTRY_CTLS
进入控制能力
CR0 IA32_VMX_CR0_FIXED0/
IA32_VMX_CR0_FIXED1[2]
表示在进入时CR0/CR4时它们哪些位必须被设置为固定值
CR4 IA32_VMX_CR4_FIXED0/
IA32_VMX_CR4_FIXED1
VM-function IA32_VMX_VMFUNC 用于表示当前可用的VM function函数索引,因此VMFUNC个数不超过64
MEM IA32_VMX_EPT_VPID_CAP 决定EPT与VPID的能力,详见手册

注:1.它们只有一个会生效,由IA32_VMX_BASIC的bit55说明,控制位为32位,MSR为64位,因此高32与低32分别作用于对应位,最终为1表示启用该能力,使用时,低32表示是否可关闭该能力,允许置0表示允许关闭,高32表示是否支持该能力,允许置1表示支持,用户需要根据这两项来设置最终值:

2.固定位和控制位含义不同,fixed0和fixed1同时生效,前者表示为0时必须设为0,后者表示为1时必须设为1,显然在硬件实现上是不允许它们冲突的,剩下的fixed0为1且fixed1为0时就是自由设置了!

接下来简单介绍了VMCS的数据部分,需要注意下面的图是旧版,新处理器会增加新的特性,这个以后遇到再说。

Guest State Area

客户状态域用于保存Guest的状态,在VM退出时保存,VM进入时恢复,如下:

如图大部分都很简单,注意通用寄存器等在状态切换时不会被修改的寄存器不在该区域,软件可根据需要自己实现保存与恢复,图里截至SMBASE为寄存器内容,特别说说IA32_DEBUGCTL,它可以提供很多很秀的调试能力,比如把之前的单步调试改成单分支调试(BTF),具体见手册。这里主要说明剩下的区域:

字段 说明
Activity state VM所处状态,有active=0,HLT=1,shutdown=2,wait-for-SIPI=3,当在inactive时,需要特定的事件唤醒它(不过更改状态需要手动)
Interruptibility state VM可中断性,有些指令会有延迟生效,在这个pending期间会处于特定的阻塞状态,当前会有STI=0,MOV-SS=1,SMI=2,NMI=3这四种阻塞状态
pending debug exceptions 存放VM的悬挂#DB异常,bit3:0表示DR3-DR0匹配导致的异常,bit12表示遇到了断点,bit14表示遇到了单步调试
VMCS link pointer 在启用VMCS shadowing时,保存影子VMCS的地址
VMX-preemption timer value VM的时间片计时器,递减步伐依赖于IA32_VMX_MISC
page-directory-pointer-table entries 用于启用EPT与PAE分页时
Guest Interrupt status 它分为两个16位的SVI(Servicing Virtual Interrupt)与RVI(Requesting Virtual Interrupt),它是RISR(Virtual Interrupt Service Register)与RIRR的有效值

Host State Arrea

主机状态域用于保存Host的状态,在VM退出时加载,如下:

该区域也很简单,注意VM进入时不保存它因为它里面的值基本不会变化,VMM在修改相关寄存器时同步到这里面即可。

Control Fields

执行控制域控制非根模式的行为,如下:

这里面的控制字段为1表示某功能生效(带exiting就表示当发生时VM退出,为偷懒下文的退出都是指VM退出),通过合理的配置它,就能在审查与性能见达到很好的平衡,如图看到有三个控制域(手册似乎还有第四级),其中二级需要在一级中激活,介绍几个:

控制位名 说明
process posted-interrupts 启用时普通外部中断将产生退出(external-interrupt exiting=1),但通知中断将不退出而被提交处理(Virtual-interrupt delivery=1),通知中断通过posted-interrupt notification vector查询posted-interrupt descriptor address查询向量号,并将中断向量放入VIRR待评估执行
Interrupt-windows existing 表示当中断窗口存在时退出,中断窗口指能响应外部中断的区间,在CTI或中断可用性描述的几种情况下是不能响应外部普通中断的
Use TSC offsetting 使RDMSR读TSC或RDTSC/RDTSCP指令时直接获取真实TSC+TSC-offset的值
CR3-load Exiting 在加载非指定CR3(CR3-target valueX)时退出,有几个指定CR3由CR3-target count来指示,最多4个,为0则退出
Use TPR shadow 启用虚拟LAPIC页,这时读TPR就可以直接读取而不退出,写入则根据收缩域来确定是否需要退出
Use I/O bitmap 使用位图来控制哪些位访问需退出,位图由I/O-Bitmap Address指示,1表示访问时退出
Monitor trap flag VM进入并执行一条指令后将产生MTF VM-exit事件
Use MSR bitmap 使用MSR bitmap address提供的位图判断是否需要退出,1表示退出,有4个位图,分别表示读写低地址与高地址的MSR情况
Virtualize APIC accesses 启用APIC虚拟化,这时在访问APIC页时,将根据TPR shadow等决定直接访问APIC-access address指向的区域还是退出
descriptor-table exiting 访问描述符表将退出,这里的描述符指GDT/IDT/LDT/TR,对应着它们的S与L指令
virtualize x2APIC mode 启用x2APIC,此时Virtualize APIC accesses必须禁用
enable EPT/VPID 内存虚拟化相关,详见下文
unrestricted guest 可使客户端进入其他模式,需启用EPT
APIC-register virtualization 启用LAPIC寄存器虚拟化
Virtual-interrupt delivery 启用时普通外部中断将产生退出(external-interrupt exiting),但改写LAPIC状态时将根据评估直接提交到处理器执行,它根据EOI-exit bitmap确定哪些位在处理结束时发送EOI不退出,此时则可以直接进行评估并可能让另一个虚拟中断被处理
VMCS shadowing 使用shadow VMCS,用于嵌套虚拟化加速,在此时VMREAD/VMWRITE将直接读它而不是退出

剩下的字段,可关注如下:

字段名 说明
exception bitmap 32位用于表示是否出现对应异常时退出,1表示退出
CR Mask/Shadow Mask为1表示host所有,读会读到Shadow对应位,写退出,而为0时可随意写
TPR threshold bit3:0表示TPR的收缩阈,存放优先级最高的且暂时不能注入VM的中断优先级,这样VM写TPR时只有在写入值小于收缩域时才退出,这时也就表明可以注入中断了
EPTP 启用EPT时,EPT的位置,它的bit2:0为内存类型,bit5:3值加一表示页表级数,bit6为EPT里access与dirty是否有效,bitN-1:12为PML4T的基址
EPTP-list address 使用EPTP switching时所使用的表地址

注:不支持基于硬件的任务切换机制(TSS切换),此时(当然像Linux本身也不使用它)会退出,注意EPT记录GPA到HPA转换,因此和CR3不存在一一对应关系,一般每Guest只拥有一个EPT。

VM Exit Control Fields

虚拟机退出控制域存放了虚拟机退出时的一些控制信息,表示在退出时由硬件保存哪些上下文:

Save表示将数据存入Guest状态区,Load表示从Host状态区读数据,在退出与进入时可通过这些位来增强惰性保存与恢复,其他都很简单只需说明acknowledge interrupt on exit,设置时表示当VM由于外中中断退出时,处理器将响应中断控制器的中断请求,若为0则会被pending,由VMM使用STI打开中断窗口再处理,此时将通过host-IDT分发。 ​

VM Exit Information Fields

虚拟机退出信息域存放了VM退出的原因与信息:

VM退出只会是指令或事件引起的,这个域就存放这两种类型的退出信息,它里面的信息特别繁杂,需要根据Exit reason去查表,详细的需见手册,这里简单说明:

类型字段名说明
基本信息类exit reasonbit15:0为退出原因类型,详见手册 bit31为1表示在VM-entry时由于配置错误引发的退出
exit qualification根据原因类型的不同含义不同,记录指令/事件的详细信息
guest-linear address记录某些指令导致退出时的线性地址,或EPT违规且线性地址有效时的线性地址
guest-physical addressEPT violation或EPT misconfiguration时的客户机物理地址,正常的访存指令都是使用虚拟机地址,而其他指令会使用物理地址(不经过MMU)
直接原因VM-exit interruption information直接引发VM退出的事件(可能不存在),bit31为1表示当前有效,此时bit7:0 中断向量号 bit10:8为中断类型,bit11为1表示有错误码
VM-exit interruption error code存在时, 硬件错误的错误码
间接原因IDT-vectoring information在Guest-IDT delivery时产生的退出,bit31 1表明该字段有效,此时该字段存储原始向量事件,类似直接原因字段
IDT-vectoring error code类似直接原因
指令信息VM-exit instruction length指令的VM退出时,指令长度
VM-exit instruction information指令的明细信息,根据exit reason有不同含义,再配合exit qualification解析指令
指令失败VM-instruction error在VMCS有效时,保存VMX指令错误的原因

VM Entry Control Fields

虚拟机进入控制域如下,它用于定义虚拟机进入时的行为,如寄存器加载,事件注入:

VM-Entry Controls有32位,通过查询IA32_VMX_ENTRY_CTLS和IA32_VMX_TRUE_ENTRY_CTLS来确定当前支持哪些设置,并将保留位设置成正确的值,下表当为1时生效:

Table 23-14. Definitions of VM-Entry Controls

Bit位 名称 说明
2 Load debug controls 进入时加载DR7和IA32_DEBUGCRK
9 IA-32e mode guest 在支持64位的架构上,表示VM进入后处于长模式(IA32_EFER.LMA=1)
10 Entry to SMM 进入后处于SMM模式(只有当前处于SMM才能使VM进入时也处于SMM)
11 Deactivate dual-monitor treatment 进入后返回executive monitor,关闭SMM双重监控
17 Conceal VMX from PT Intel PT在VM进入时不产生分页信息包(PIP),在从STM返回到executive monitor时不产生VMCS包
20 Load CET state 进入时加载控制流增强技术相关的MSR和SPP

VM-Entry Controls for MSRs有两个域,count域32位表示MSR-Load表长度,Intel推荐不超过512(否则可能出现非预期错误,笑死),address为表的地址,表里每项128位,结构如下:

注:有些特殊寄存器操作会受限,如IA32_FS_BASE/IA32_GS_BASE不能被写入,而如000008H和APIC相关需要特殊配置,详见手册。

VM-Entry for Event Injection允许在进入后执行任何指令前执行注入事件,它含三个域

字段名 说明
VM-entry interruption information 事件信息,bit7:0是中断向量,bit10:8是事件类型,bit11表示是否有错误码需要提交,bit31表示事件注入是否有效
VM-entry exception error code 若需要提供错误码时,填入该处
VM-entry instruction length 由指令相关产生的事件时,指定指令长度

其中可用的事件类型如下:

bit10:8 类型 说明
0 external interrupt 外部硬件中断,如外设产生的中断,向量号为32~255
2 NMI 不可屏蔽中断
3 handware exception 硬件异常,指fault或abort事件,除#BP与#OF外所有异常
4 software interrupt 普通INT指令产生的中断
5 privileged software interrupt 同上,但会被忽略特权级
6 software exception INT3和INTO产生的#BP与#OF异常
7 other event 注入的MTF VM-exit事件

上面主要介绍了VMCS各个域的作用,其实通过它也就能比较好的理解Intel-VTx了,总的来说它通过新增一个操作模式来修复之前的虚拟化漏洞,为此它新引入一系列虚拟扩展指令,并通过VMCS来存储信息,控制虚拟化行为,此时它一方面能实现让客户机完全无法感知处于虚拟机中,另外也提供了大量的选项供用户配置,在允许尽可能减少VM退出次数从而提高效的同时,也提供了充分的审查功能。

CPU类虚拟化

上面的基于软件的完全虚拟化在修补漏洞时是动态的,也可以静态的主动将客户系统的缺陷指令替换掉,另外也可以新增一些新的功能指令,此时把原来的指令替换为一个到VMM的调用(最简单的让VM退出即可,如使用无效指令),这种调用一般较超调用Hypercall。

内存

CPU重置后读取的是BIOS映射的区域,由BIOS探测内存后才能使用它,这一部分见BIOS虚拟化,此后就是内存虚拟化了,从软件视角,内存需要满足从0开始且地址连续(不严格,总之就是要和硬件一致),客户机与宿主机间共四种地址:客户机虚拟地址GVA,客户机物理地址GPA,宿主机虚拟地址HVA,宿主机物理地址HPA,已知CPU的寻址是物理实现的,即MMU,在保护模式下使用的VA需要经过MMU转换成PA,这是硬件固有的,内存虚拟化是要实现GVA->GPA->HVA->HPA间的转换,于是有两种思路,不改变硬件纯软件迂回实现,改变硬件地址翻译机制。

软件-影子页表

很明显,GVA->GPA->HVA->HPA之间的转换涉及到三种表,GVA->GPA使用客户机系统维护的页表,GPA->HVA是VMM维护的页表,HVA->HPA是VMM所在系统维护的页表。很明显可以制作一种新的页表实现GVA->HPA间的映射,这是满足原始MMU机制的,这就是影子页表(SPT),它在软件上合并了这个转换过程,客户机使用虚拟CR3指向虚拟页表(即在读写CR3时被宿主机拦截模拟),实际的物理CR3指向影子页表:

这里面的难点就是同步问题,当客户机切换页表时,VMM需要切换影子页表,客户机修改页表时也需要同步修改影子页表(像影子一样随原始页表变化而变化),对此VMM需要捕获三种情况:

1.读写CR3:读的话给虚拟CR3的值,写的话会刷新TLB,而写不同值还需要切换影子页,尽管影子页可以在每次切换时重建,但是为了效率一般都会缓存(如使用客户机的CR3作为键缓存)

2.INVLPG捕获:客户机执行该操作需要被同步

3.修改页:包括缺页,与修改页,可通过将页目录页表区域权限置为只读,在修改时捕获操作以进行同步

在初次缺页时需要两次退出,过程如下图:

在MMU翻译时出现缺页异常,若是换出则直接由VMM系统处理,这里是不存在页表项,则根据虚拟CR3与CR2检测客户机是否已有页表项,若存在则直接创建GPA->HVA->HPA的映射并同步到影子页表;若不存在则先将该异常注入回客户机,客户机建立好页表项时再此访问由于无物理内存映射,会再次出异常,此时根据GPA得到转换信息最终建立影子页。

缺点

1.每个进程一个影子页占用空间

2.一致性维护复杂,需要频繁的退出虚拟机性能较低

硬件-EPT

EPT(Extended Page Table)是intel提出的硬件虚拟化方案,amd的叫NTP(Nested Page Table),说真的intel的起名真比不上amd,它们俩都是同样的意思--嵌套页表,其实就是多加了层转换,从而在硬件上实现了GVA->GPA->HPA转换,如下图:

这里演示了4级页表和4级EPT,此时CR3指向客户机的页目录,而EPTP(EPT Pointer)会指向GPA->HPA的转换表EPT,可见在每次访问转换时,MMU是直接把GVA转换为GPA,而由EPT MMU再将其转换为HPA,这个过程会显著的增加访存次数,不过有EPT TLB在那里缓存其实也还好,总的来说,这种方案实现起来特别简单,且在客户机修改CR3和页表时不必退出了!

另外还有个与内存虚拟化相关的,专用于优化,即VPID(Virtual Processor IDendifier),原来TLB是存的线性地址到物理页地址的映射,因此只要换页表就要重置缓存,原来虚拟机的切换/退出进入就会有这种行为,CPID是为每个虚拟机分配一个虚拟ID,宿主机的是0,存储在TLB中的信息就不止线性地址还有对应的VPID,这样虚拟机的切换也就不会导致所有缓存失效... ​

优化方案

1.页共享,虚拟机和普通进程差不多,进程可以共享只读区域,甚至用写时拷贝共享所有区域,这种思路也能用在虚拟机上,若运行多台虚拟机则可对它们的页做匹配,已知则共享,这也叫消除重复页(Memory de-duplication),这可能使Guest受RowHammer影响…

2.客户机在申请内存时,新内存可通过异常来感知,但释放内存难以感知,客户机实际使用内存只有客户机知道,因此可使用类虚拟化技术,向客户机注入驱动以收集内存使用信息,从而释放掉空闲内存,此时有一种气球模型,它的思路就是尽可能多的申请空间,申请成功的就是客户机空闲的,这些空间就可以被释放。

中断

中断原因

IA处理器是中断驱动的,所以虚拟化肯定少不了它,中断虚拟化也需要完全根据硬件行为来模拟出等效的操作,由于增加了一层,VM产生的中断/异常将会有两种原因:

1.VM自身造成的,如外设产生需要VM处理的中断,自身执行了INT指令,调试指令,除零指令等

2.由于虚拟化造成的,如为了审查而修改EPT权限造成的#GP,降权时的#GP等

在VMM退出时,必须区分这两种原因并正确处理它。

中断与异常

中断是由外部硬件产生,它是异步的/随机的,而异常是由软件执行时遭遇某意外(有意或无意的)情况或执行的特殊指令产生的,它是同步的,在某种程度上是可预测的,相对后者要简单。

1.中断源:它可能来自外部设备,也可能来自CPU内部的LAPIC计时器,性能监视器,处理器本身或其他处理器等

2.中断控制器:它负责接收外部中断,对中断进行评估(evaluation)并提交给处理器

3.中断分发:对中断进行分发(delivery),最终由处理器根据运行模式选择对应的中断服务例程处理

而中断虚拟化就是要模拟后两者的功能,如下图:

img

在IA下中断控制器主要有PIC与APIC,由于APIC可兼容PIC,其实一般也可以只模拟APIC,即虚拟LAPIC与虚拟I/O APIC,物理中断源与中断控制器是通过专用或公用的线路连接的,而在虚拟化时就是直接通过函数调用的方式连接,中断可能来源于虚拟设备,它直接调用IC提供的函数即可,而对来自物理设备的中断,它可能需要先由VMM处理,再将其通过函数调用转发给IC,IC收到中断后需要进行评估,选出优先级最高的并于当前正在执行的优先级进行比较,判断是否可分发。中断的分发就是让VM到达中断服务例程的入口点。

注:中断是个复杂的话题,关于IA的中断与中断控制器可查看之前的文章。

中断的处理

中断(其实是所有退出事件)的处理只需遵循一个原则,谁的原因谁负责:

1.Guest自身产生的异常一般直接由Guest处理。

2由于VMM存在产生的异常需要处理/修复后恢复Guest运行,此时Guest是无感的。(硬件任务切换/监视事件/配置错误等)

一般硬件辅助时Guest本身产生的异常不需要VM退出,若需要审查那么审查结束再将对应的错误码从VM-exit(Intel VT-x)对应位复制到VM-entry即可,而软件实现的需要模拟Guest的分发过程。

中断软件虚拟化

中断控制操作截获

从操作方式上,在IA上涉及三类中断控制器:

1.PIC:它使用IO端口(主控0x20/0x21,从控0xA0/0xA1),因此拦截对应的IO操作即可

2.APIC/xAPIC:它们都是使用MMIO做的映射(默认0xFEE0000的4k区域,根据IA32_APIC_BASE确定),对这片内存做访问保护即可拦截

3.x2APIC:它使用MSRs控制(0x802~0x83F),对相关的MSRs操作进行拦截即可

中断评估

中断分发

在分发前,它需要判断是否可注入中断,如VM屏蔽了中断(IF=0),VM处于inactive状态,处于由于STI造成的不可中断状态,若不可注入则先缓存,在取消屏蔽时再注入,在中断不能注入时,需要考虑:

1.是否允许丢失中断,允许则合并同类型中断,否则一一注入

2.阻塞期间中断源取消中断如何处理,如注入一个假中断

分发时它对中断与异常一视同仁,都是模拟与硬件等效的操作,它首先要让VM退出,之后开始如下操作:

1.判断目标模式,如目标运行在兼容模式而服务例程运行在64位模式则需要切换模式

2.根据硬件行为进行安全检查

3.保存上下文

4.切换到对应的服务例程

中断硬件虚拟化

在CPU的硬件虚拟化时已经有涉及,VT-x对中断虚拟化提供了一定的支持。待续...

设备

设备(I/O)虚拟化

从CPU角度,(1)CPU通过PIO/MMIO访问设备,(2)设备可向CPU发起中断,除此之外设备可以直接和Mem间通信,(3)但这也是由CPU通过PIO/MMIO进行设置的,因此处理好这三类操作就能进行I/O虚拟化,即vCPU在通过PIO/MMIO操作外设时,拦截并模拟行为,在需要向vCPU提交信息时向其注入中断,在必要时直接与Mem进行数据传输,完成同样的效果即可欺骗Guest完成设备虚拟化

img

裸机时要使用外设,需要有这个设备硬件(device)及和设备相应的专用或通用驱动(driver),在不修改driver时可使用全虚拟化于透传技术,否则可使用类虚拟化技术,下面详细描述

img

IO全虚拟化

为了不修改driver,需要实现和硬件设备完全一致的接口定义,太麻烦...

IO类虚拟化

从全虚拟化看,为了满足driver期待的行为,需要模拟与硬件完全一致的逻辑,此时会存在大量冗余的操作,还会有很多没有必要的VM退出事件,例如要进行一次文件写入操作,可能需要先向某端口写入地址,接着向某端口写入命令,最后再向某端口写入数据,这会涉及三次VM退出操作,而这时一次可能只能写如很少量的数据,这是可以避免的,如定制专用的驱动,它能将要执行的操作和所需的所有信息/数据全部打包,并通过一次VM退出,VM再将数据一次性取完,如下时virtio的架构:

img

IO透传

直接把设备给虚拟机,这里主要介绍硬件虚拟化:

DMA重映射

img

中断重映射

它在外设与CPU间添加了中断重映射硬件单元,VMM配置它后,外设产生的中断在此处将会查表进行转换,并注入给指定的VM,再配合posted-interrupt机制可使VM不退出直接处理中断:

img

特殊设备

时钟

时钟硬件分为两种:周期性时钟(Periodic Timer)会以固定频率产生时钟中断,而单次计时时钟(One-shot Timer)在配置后到达指定阈值会产生单次中断,常见的有:

计时器 类型 说明
PIT(Programable Interrupt/Interval Timer) 周期 频率约1000Hz,每毫秒产生一次中断(节拍),常接IRQ0,通过0x40~0x43端口访问,16位容易溢出
RTC(Real Time Clock) 单次/周期 通常和CMOS集成,关机可计时,常接IRQ8,通过)0x70~0x71端口访问
TSC(Time Stamp Counter) Nil 频率和CPU频率相关,在降频的时候会变化,但频率高,很常用
LAPIC Timer 单次/周期 基于总线时钟信号,32位,传递给本地CPU
HPET(High Precision Event Timer) 单次/周期 最低频率10MHz,它有一个主计数器和32个比较器,匹配器一起,可配置32个子时种,每个可设置单独的中断频率
ACPI PMT Nil 类似TSC不产生中断,只是计数,它通过端口读取,端口号在启动时有BIOS指定

操作系统离不开时间,主要分两种用法:统计时间(如进程用户态/内核态时间,系统时间)和定时(如测量某一时间间隔,定时器)。时钟虚拟化是基于时钟硬件的,上面提到对时钟的访问有三种方式,PIO/MMIO/特殊指令(RDTSC),显然很容易拦截,问题是怎么给Guest时间,如虚拟机在退出时是否也计时,如TSC若保持计时,则直接返回实际值加偏移(硬件支持,可不退出),而PIT这种周期性的则需要在下次VM进入时,连续为其注入退出期间应该产生的中断...

BIOS

这东西本来在主板上,从软件角度就是实现bios的功能,在“Linux启动之bootloader”已简单介绍过它的作用,在虚拟化时就没必要再完全照其功能实现,如内存探测直接生成结果即可,真正需要做的是实现BIOS调用

本文只简单介绍了虚拟化基本原理,为了加深理解,可查看Sina & Shahriar的Hypervisor From Scratch系列文章,他演示了在windows下通过Intel-vt实现一个简易的虚拟机,关于CPU硬件的软件操作,可参考cpu-internals,不过它很简略,详细的还是要看手册。

参考

1.系统虚拟化-原理与实现 -- 英特尔开源软件技术中心[著];复旦大学并行处理研究所[著]

2.深度探索Linux系统虚拟化 原理与实现 --王柏生,谢广军[著]

3.Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B, 3C & 3D): System Programming Guide

4.WHITE PAPER Intel® Virtualization Technology

5.[译] [论文] 可虚拟化第三代(计算机)架构的规范化条件(ACM, 1974) -- Popek, Gerald J., and Robert P. Goldberg[著]; ArthurChiao 译];

6.Intel Virtualisation: How VT-x, KVM and QEMU Work Together -- binarydebt

7.MMU Virtualization Via Intel EPT: Technical Details -- Daax Rynd

8.x86 virtualization -- wikipedia

9. Comparison of Software and Hardware Techniques for x86 Virtualization -- Keith Adams, Ole Agesen[著]

10.Analysis of the Intel Pentium’s Ability to Support a Secure Virtual Machine Monitor -- John Scott Robin, Cynthia E. Irvine[著]

11.处理器虚拟化技术 -- 邓志[著]

12.QEMU/KVM源码解析与应用 -- 李强[著]