一直想不通这两句话的含义,今天查了点资料、对着源码分析了一下,不一定对,写下来,分享:
将任何一种形式的资源抽象成另一种形式的技术都是虚拟化。
x86 CPU支持ring0-3 共4种不同的特权级别,下面分正常和虚拟化两种情况叙述、分析:
1.正常情况下:
特权级别是针对段来讲的,段描述符的最后两位标识了该段所位于的特权级别,比如,中断处理程序运行于ring0(),此时的内核程序是具有特权的,即ring0.
位于ring3用户程序可以通过系统调用的方式,int80,后特权翻转入ring0,然后就可以顺利执行中断处理程序(好像是用户程序调用内核程序的唯一途径)。
2.虚拟化情况下:
特权解除:是指解除正常情况下运行于ring0的段,比如中断处理程序,为了虚拟化需要,此时解除其特权,将其运行于ring1
当用户程序通过系统调用时,其跳转到的中断处理程序运行于ring1。但是,在中断处理程序中,有部分指令是必须在ring0才能执行的,比如(),此时,便会自动陷入,然后模拟。
也就是说,用户程序运行特权指令,会有两次特权下降,一次是通过系统调用进入ring1,第二次是通过特权指令陷入进入ring0。这说明,中断发生时的中断处理程序还是以前的位于内核的代码,但是其运行级别为ring1,部分指令还需要再次陷入,才能执行。
特权指令为什么会自动陷入呢?
这是由于特权指令的特性,即
1.特权指令 :只能在最高级别上运行,在低级别状态下执行会产生trap。例如:LIDT只能在系统模式下执行,在其他模式下都会产生trap,中止执行。
The Intel architecture defines "privileged"instructions and "sensitive" instructions. The privilegedinstructions may only be executed when the Current Privilege Level is zero (CPL= 0). Attempting to execute a privileged instruction when CPL != 0 willgenerate a general protection (GP) exception.
The sensitive instructions (also called IOPL-sensitive) may only be executed when CPL <= IOPL (I/O Privilege Level). Attempting to execute a sensitive instruction when CPL > IOPL will generate a GP exception.
特权指令只能在CPL=0的情况下才能执行;在CPL不等于0的情况下执行特权指令便会产生通用保护(general protection)异常。
敏感指令只能在CPL<=IOPL成立的前提下执行,当CPL>IOPL时会自动产生GP异常
2.非特权指令 :可以在各个级别的状态下执行。
在具体实现上,特权指令是否会验证CPL指,才决定是否继续执行,猜测是这样
CPL:正在执行程序或任务的特权级,由CS、SS的1~0这两个bit体现
DPL:段或门的特权级,被存储在段描述符或门描述符的DPL字段中
RPL:由段选择子的1~0这两个bit体现
另外,还有一个重要问题,就是部分敏感非特权指令无法陷入的问题:存在二进制翻译、超级调用等方式,强迫其陷入,然后模拟
对于“特权解除,陷入模拟”方法,其中存在大量的冗余代码,地址空间及跨特权级切换频繁、内存复制多,操作系统与VMM都会带有CPU调度、内存管理以及外设驱动等代码,功能逻辑重复,如在完全虚拟化系统中,一个磁盘读写请求要完整的精工虚拟机操作系统中的磁盘驱动和VMM的真实磁盘驱动两层代码逻辑。而在类虚拟化系统中,操作系统里冗余的设备驱动就可以被替换成精简的调用服务的钩子,从而缩短一个系统服务的代码路径。()
类虚拟化虚拟机能够使用的指令是实际CPU指令的一个子集,它不包括特权指令和敏感指令。对于敏感指令,更准确地说,只是这些指令对于敏感信息的操作不被纸质,如xen提供了与这些指令功能相对应的函数,客户机操作系统能够以类似于系统调用的同步通信方式来调用这些函数。如果客户机操作系统使用的指令超出了VMM支持的指令集,由于特权级保护机制,这些指令的执行会发生陷入。如HLT指令用于没有工作时停止CPU运行,进入省电模式,直到被下一个外部中断唤醒。类虚拟化操作系统不会使用这条指令,而是发起一个超级调用来告知VMM没有需要运行的工作了,VMM将客户机的当前VCPU移出等待队列,使得它不再被调度到硬件CPU上运行。再如,LGDT指令用于给GDTR寄存器装在GDT表,xen允许客户机操作系统装载自定义的GDT表,客户机操作系统发起一个超级调用来装在GDT表。在Xen中的超级调用服务函数会验证作为GDT表装在的页的类型是GDT,并验证这些GDT描述符项的完整性。xen同时在VCPU数据结构中保存指向这些GDT页的指针,当虚拟机切换时,xen会装载被调度执行的客户机的GDT表到硬件寄存器中。
在虚拟化环境下,一个虚拟机不会直接收到来自硬件的外部中断,而只会收到由VMM注入的虚拟中断。当vmm向虚拟机注入一个虚拟中断时,一个虚拟机可能正在CPU上运行,也可能在调度队列中处于等待状态。如果是后一种情况,只有在虚拟机下一次被调度运行时才能处理这个中断事件。因而,需要一种异步的通信机制将中断传递给客户机操作系统。
超级调用是从客户操作系统到VMM的系统调用。与系统调用类似,xen启用130号中断向量端口(十六进制的82H)作为超级调用的中断号。这一个中断向量的DPL被设置为类型为1,类型为中断门。这样,超级调用能够由处于特权级1的客户机操作系统发起,而不能从用户态发起。
简 而言之,处理器呈现给软件的接口就是一堆的指令(指令集)和一堆的寄存器(含用于通用运算的寄存器和用于控制处理器行为的状态和控制寄存器)。而 I/O 设备呈现给软件的接口也就是一堆的状态和控制寄存器(有些设备亦有内部存储)。这些都是系统的资源,其中影响处理器和设备状态和行为的寄存器称为关键资源 或特权资源,如 x86 之 CR0 ~ CR4,MIPS 的 CP0 寄存器,PowerPC 的 Privileged SPR(SPR 编号第 5 位为 1)。
可以读写系统关键资源的指令叫做敏感指令,如 x86 的 lgdt/sgdt/lidt/sidt/in/out,MIPS 的 mtc0/mfc0,PowerPC 的 mtmsr/mfmsr,SPARC 的 rdpr/wrpr 等,此类又可称为控制敏感指令。
还有一类行为敏感指令,该类指令的执行结果依赖于系统的状态(如 x86 之 popf)
现 代计算机体系结构一般至少有两个特权级,(即用户态和核心态,未加虚拟化扩展的 SPARC和PowerPC 即是,MIPS 有三个特权级(外加一个 Supervisor 态,没什么用),而 x86 有四个特权级 (Ring0 ~ Ring3))用来分隔系统软件和应用软件。
决大多数的敏感指令是特权指令,特权指令只能在处理器的最高特权级(内核态)执行,如果执行特权指令时处理器的状态不在内核态,通常会引发一个异常而交由系统软件来处理这个“非法访问”(陷入)。
少数敏感指令是非特权指令,如 x86 的 sgdt/sidt 等,非特权指令可以在用户态读取处理器的状态,如 sgdt/sidt 则可在用户态 (Ring3) 将 GDTR 和 IDTR 的值读取到通用寄存器中。
对于一般 RISC 处理器,如 MIPS,PowerPC 以及 SPARC,敏感指令肯定是特权指令,唯 x86 例外。
在 x86 上运行虚拟化的具体问题
虚拟化技术的一个重要要求之一就是通过虚拟机监控软件 (VMM) 运行的操作系统 ( 我们称之为客操作系统 ) 在运行时,其运行效果应该和直接在裸机上运行操作系统是一致的,即客操作系统不应该感觉到虚拟化技术的存在。 IBM 的主机采用 “trap-and-emulate” 的方法来实现 CPU 的虚拟化,即一般的指令直接运行,那些可能改变主机全局行为的“敏感指令”则会被截取,交由 VMM 通过仿真来完成其功能。 “trap-and-emulate” 被认为是实现 CPU 虚拟化的最好方法。 作者 Popek 和 GoldBerg 在 1974 年的一篇文章提出了 “classic virtualization” 的概念,该概念认为能够比较完好的实现 “trap-and-emulate”的硬件平台才是 “classically virtualizable” 的平台,即如果该平台上的 VMM 如果能够比较容易的捕获敏感指令,我们才认为该平台是典型的比较易于虚拟化的平台。 X86 平台由于其迅速的无处不在的统治力,在最初的设计上并没太多考虑虚拟化的要求,根据 GoldBerg 的标准,传统的 x86 属于非 “classically virtualizable”的平台。 Scott Robin 在 2000 年的文章以 Intel Pentium 为例详细介绍了 x86 平台在支持虚拟化方面存在的问题。GoldBerg 的标准并不排除用其他方法解决“trap-and-emulate”的问题, 如 X86 上的虚拟化提供商 VMware 和 XenSource 分别采用“Binary Translation” 和“Para-virtualization”来解决敏感指令问题。其中“Binary Translation”技术提前发现敏感的指令并通过插入断点来截获之,交由 VMM 来解释执行。 “Para-virtualization” 方法则直接修改客操作系统代码,修改其特权级,并将敏感指令改为 Trap Call 直接通知 VMM 来处理。 这两种在软件上处心积虑的方法会导致软件实现的复杂性,限制了 VMM 性能的提升空间,“Para-virtualization” 更是没法施用在 Windows 等私有操作系统上。
在说明 X86 平台对虚拟化的支持能力之前,我们有必要解释一下特权级的概念。 x86 硬件支持 4 个特权级 (Ring),一般内核运行在 Ring 0, 用户应用运行在 Ring 3, 更小的 Ring 有比更高的 Ring 能访问更多的系统全局资源,即更高的特权。 有些指令只能在 Ring 0 才能正确执行,如 LGDT、LMSW 指令,我们称之为特权指令;另外有些指令可以在 Ring 3 正确执行,如 SGDT、 SMSW、PUSHF/POPF,我们称之为非特权指令。
在传统的 X86 平台上支持虚拟化上存在如下问题 :
X86 指令集中存在 17 条敏感的非特权指令
“非特权指令”表明这些指令可以在 x86 的 ring 3 执行, 而 “敏感性” 说明 VMM 是不可以轻易让客操作系统执行这些指令的。 这 17 条指令在客操作系统上的执行或者会导致系统全局状态的破坏,如 POPF 指令,或者会导致客操作系统逻辑上的问题,如 SMSW 等读系统状态或控制寄存器的指令。 传统的 X86 没法捕获这些敏感的非特权指令
“Ring deprivileging” 带来的问题
除了那 17 条敏感的非特权指令,其他敏感的指令都是敏感的特权指令。 在 x86 虚拟化环境,VMM 需要对系统资源进行统一的控制,所以其必然要占据最高的特权级,即 Ring 0, 所以为了捕获特权指令,在传统 x86 上一个直接可行的方法是 “Ring deprivileging”, 如将客操作系统内核的特权级从 Ring 0 改为 Ring 1 或 Ring 3, 即 “消除” 客内核的特权,以低于 VMM 所在的 Ring 0, 从而让 VMM 捕获敏感的特权指令。 然而,采用 “Ring deprivileging” 又会带来如下问题 :
- “Ring aliasing”。 该问题是指客操作系统可通过读取 cs,ss 段寄存器的值而知道其自身已经不处于 Ring 0, 这一结果理论上可以让客操作系统改变自己的行为,违背了虚拟化应该对客操作系统透明的原则。
- (
(Ring Alias)
特权级别名是指 Guest OS 在虚拟机中运行的级别并不是它所期望的。VMM 必须保证 Guest OS 不能获知正在虚拟机中运行这一事实,否则可能打破等价性条件。例如,x86处理器的特权级别存放在 CS 代码段寄存器内,Guest OS 可以使用非特权 push 指令将 CS 寄存器压栈,然后 pop 出来检查该值。又如,Guest OS 在低特权级别时读取特权寄存器 GDT、LDT、IDT 和 TR,并不发生异常,从而可能发现这些值与自己期望的不一样。为了解决这个挑战,VMM 可以使用动态二进制翻译的技术,例如预先把 “push %%cs” 指令替换,在栈上存放一个影子 CS 寄存器值;又如,可以把读取 GDT 寄存器的操作“sgdt dest”改为“movl fake_gdt,dest”。
)
- “Ring compression”。 无论采用哪些特权级,传统 x86 都需通过分段或分页的方法来实现地址空间间的访问控制。然而传统 x86 上所有 64 位的操作系统都没使用分段,另外 x86 上的分页不区分 Ring 0、Ring 1 和 Ring 2, 也就是说 x86 上没法通过分段或分页机制来阻止 Ring 1 上的 64 位客操作系统内核来访问 VMM 的地址空间。 所以,客内核被迫采用 Ring 3, 也就是和客操作系统上的应用相同的运行级,这当然就会导致新的问题。
- “Adverse Impact on Guest system calls”。 该问题和用 sysenter/sysexit 指令实现的系统调用机制有关。Sysenter 不同于 “int 0x80”, 其不需要经过异常表的控制而明确切换到 Ring 0, 所以 sysenter 是 x86 平台上性能更高的系统调用实现方式。 但在虚拟化情况下 sysenter/sysexit 带来的问题是, sysenter 让客操作系统上的应用进入 Ring 0 而不是客内核的 Ring 1, sysexit 在 Ring 1 上执行直接导致 fault, 解决这一问题的方法只能是让 VMM 来仿真 sysenter/sysexit, 或者让 VMM 向客操作系统屏蔽掉虚拟 CPU 的 sysenter 能力,无论哪种做法,都会导致客操作系统上应用性能下降。
“Address-space Compression”问题
传统 x86 上的操作系统如 Linux 都采用统一的线性地址空间,通过页表和特权级来控制用户进程对内核地址区域的访问,用户进程执行系统调用,只是改变自己的特权级,并不改变自己的地址空间,所有进程可以通过多级页表机制共享内核地址区域的内容。 那么在虚拟化环境下需要考虑的问题是, 是应该让 VMM 占据客操作系统地址空间的一部分,还是让其采用独立的地址空间? 采用前一方法对只有 4G 地址空间的 32 位客操作系统难一解决,另外要考虑如何防止客内核对 VMM 地址区域的访问,及如何保持对客操作系统的透明性。 采用后一种方法需要考虑如何快速实现地址空间切换,如何建构用于客操作系统和 VMM 间互相切换的控制结构 ( 类似于 IDT 和 GDT)。传统的 x86 似乎还没有很好的机制支持这个问题的解决 。
中断的虚拟化问题。
X86 操作系统的内核通过修改 EFLAGS 的 IF 位来控制外部中断的投放。 在虚拟化环境下 VMM 有诸多理由希望能统一的控制中断的投放, 然而通常情况下客操作系统对 EFLAGS.IF 的修改是很频繁的行为, 如果 VMM 通过捕获客操作系统对 EFLAGS.IF 的修改而获得对中断的控制权,显然代价过于高昂。另外一个方面是虚拟化的中断,VMM 该如何向客操作系统发起一个中断?该如何控制虚拟的中断被客操作系统投放的时间 ?
对特权资源的频繁访问问题
VMM 通过特权级控制来捕获客操作系统对特权资源的访问,在一般情况下不是一个问题,但对某些特权资源,如 APIC 的 TPR 寄存器,一方面客操作系统可能会频繁访问,另一方面 VMM 为了统一控制而又不得不截取之,导致的巨大性能开销是一个不得不严肃考虑的问题。
Intel 和 AMD 的解决方法
Intel 和 AMD 通过对其 X86 硬件架构进行扩展,解决了 X86 架构不能很好支持 “Classic Virtualization” 的问题,Intel 和 AMD 的这种 x86 虚拟化扩展分别称为 Intel VT-x 和 AMD-V( 或从代码的角度分别称为 VMX 和 SVM)。 我们认为,具有 Intel VT-x 或 AMD-V 能力的处理器都是 “classically virtualizable”的, 当然 Intel 和 AMD 不会满足于此,除了解决了前面提到的 x86 平台虚拟化存在的问题外,目前的 Intel 和 AMD 的处理器在 MMU 虚拟化和 IO 虚拟化方面都会提供相当的支持,并且在许多细节问题方面都会有所考虑。
Intel VT-x 和 AMD-V 提供的特征大多功能类似,但名称可能不一样,如 Intel VT-x 将用于存放虚拟机状态和控制信息的数据结构称为 VMCS, 而 AMD-V 称之为 VMCB; Intel VT-x 将 TLB 记录中用于标记 VM 地址空间的字段为 VPID, 而 AMD-V 称之为 ASID; Intel VT-x 将二级地址翻译称之为 EPT, AMD 则称为 NPT, 等等一些区别。 读者必须注意,尽管其相似性,Intel VT-x 和 AMD-V 在实现上对 VMM 而言是不兼容的,我们后面的介绍只限于 AMD-V。
KVM 全称 Kernel-based Virtual Machine, 即基于 Linux 内核的虚拟化技术, 精确的说,就是 KVM VMM 的核心功能是通过一个 Linux 内核模块实现的。 “基于 Linux 内核”是 KVM 在软件实现上不同于其他 VMM 实现的最重要特点, 使得 KVM 在实现上能获得如下好处 :
- 利用 Linux 内核已有的功能和基础服务,减少不必要的重新开发。 如任务调度,物理内存管理,内存空间虚拟化,电源管理等功能,通常是一个 VMM 所必须具备的,但 KVM 可不必重新开发这些功能,直接使用 Linux 上已经相当成熟的技术。
- 利用强大的 Linux 社区,吸引优秀的 Linux 内核程序员参与到 KVM 的开发中, 壮大 KVM 的群体, 这些程序员以及红帽等 Linux 社区背后的厂商,也乐于在 Linux 上发展一个成熟的 VMM 技术。
- 可以长期享受 Linux 内核技术不断成熟和进步的好处,优化 KVM 的实现。 如 Linux 内核的 HugeTLBPage 技术可以用于削减 KVM 在虚拟机内存使用上的性能开销, eventfd 可以用于提升 KVM 内核执行路径和 Qemu-kvm 用户空间的交互效率。
KVM 在 VMM 的理论上属于硬件辅助的虚拟化技术, 即 KVM 需要利用 AMD-V 提供的虚拟化能力。AMD-V 让 KVM 上的虚拟机正常情况下运行在 “guest” 模式, 在执行敏感的的指令或行为时透明地切换到 “host” 模式,并在 “host”模式由 KVM VMM 的代码仿真那些敏感的指令或行为,完成后又回到“guest”模式由虚拟机运行其正常的代码。 KVM 的 VMM 代码实际上就是当虚拟机被捕获时才进入,执行仿真代码,然后又执行状态切换回到虚拟机代码直到其下一次被捕获,如此循环不断。 当然 KVM 的 VMM 在仿真复杂的行为时,可能需要用户空间的帮助, 所以 KVM 在此期间切换到用户空间。
IA-32架构的CPU将CPU的权限分为4级:ring0到ring3,其中ring0优先级最高。CPU的指令分为特权指令和非特权指令。设置CPU状态、模式的操作全部是特权指令,修改CPU 控制器寄存器如EFLAGS、CR0、CR1、CR2、CR3的指令也为特权指令。所有的特权指令只能在ring 0 级才能调用。
通常操作系统运行在ring 0,应用程序运行在ring 3。而在虚拟化环境下,VMM 要运行在ring0。所以在传统的IA-32 架构下,不得不修改操作系统内核,使其知晓VMM 的存在,并运行在ring1 中,应用程序仍然运行在ring3 中。这使得像windows 这种不开源的操作系统很难实现虚拟化(通过代码扫描和动态指令重写的方法解决)。
Intel VT-X技术
Intel VT-x 技术的诞生,简化了虚拟机的设计,并提高了VMM 对虚拟机的掌控灵活度和粒度。VT-x 是用在IA-32 体系结构上的,它对处理器进行了扩展,引入了两种新的CPU工作模式,VMX根模式和VMX非根模式,两者都可以支持所有四个等级。在VT-x 技术的支持下,VMM 和Domain0 运行在根模式下,其中VMM运行在ring0,Domain0 运行在ring1,应用程序运行在ring3。HVM Domain 运行在非根模式下,和通常的操作系统一样,内核运行在ring0,应用程序运行在ring3。两种模式可以相互转换,从根模式进入非根模式的称为VM 进入(VM entries),而反过来的称为VM 退出(VM exits)。VT-x 技术定义了虚拟机控制结构VMCS(Virtual Machine Control Structure),用来保存虚拟机的各种状态,控制VMX 非根操作的转换进出和处理器的行为。这个结构由指令VMPTRST,VMPTRLD,VMREAD,,VMWRITE 和VMCLEAR 来操作。VMM可以对每个VM 使用不同的VMCS,也可以同一个VM 内多个处理器使用不同的VMCS。
x86硬件辅助CPU虚拟化的过程
注:首先需要明确一点,英特尔的CPU虚拟化采用的技术别称为VT-x,但VT-x中并不仅仅只包含CPU虚拟化,还包括中断虚拟化和内存虚拟化等内容,而AMD在AMD-V在官方资料中也是把CPU、内存和中断等虚拟化技术全部放在了SVM技术规范中进行统一讲解的。
传统的IA32处理器架构并不是十分可靠的虚拟化架构,为了解决这个问题,英特尔通过VT-x技术对原有架构进行了扩展补充,其核心操作模式示意图如图11。
图11 支持VT-x技术的虚拟化架构
VT-x技术引入了两种专为虚拟化打造的操作模式,称为根操作模式(VMX Root Operation)和非根操作模式(VMX Non-Root Operation),其中VMM运行在根操作模式下,而客户操作系统则运行在非根操作模式下,每个模式都存在Ring0-3四个特权级别,所以在VT-x中,对特权级别进行描述时必须说明是在根模式还是在非根模式下。对照前面我们讲解的软件完全虚拟化时的示意图我们不难发现,客户操作系统(Guest OS)所运行的特权级别发生了变化,由Ring1变成了Ring0,而原本工作在Ring0的VMM则被注明是工作在根操作模式下的Ring0上(有一些文档中称之为Ring-1)。相信仔细阅读上期虚拟化文章的读者朋友,不难理解英特尔为什么要制造出两个新的操作模式来,因为客户操作系统重新回到了Ring0上(当然这里是非根模式下的),而且经过英特尔对相关指令的重新设计,使得原本不能通过先陷入后模拟的方式执行的指令都可以顺利执行,而在根模式下,所有指令的执行和传统IA-32相比不会有任何变化,从而保证了原有软件和虚拟环境的正常运转。
图12 英特尔VCPU创建,运行和退出示意图
在硬件辅助CPU虚拟化中,陷入的概念已经被VM-Exit操作取代,它意味着从非根操作模式切换到根操作模式,对应的从根操作模式切换回非根操作模式被称为VMEntry。我们在上一期的文章中提到了CPU虚拟化的基本原理,这里我们有必要温习一下。里面提到了“CPU虚拟化是为物理机器上的每一个虚拟机提供一个或者多个虚拟CPU(简称VCPU),每个VCPU分时复用物理CPU,在任意时刻一个物理CPU只能被一个VCPU使用,VMM要在整个过程中合理分配时间片以及维护所有VCPU的状态”,这里谈到的VCPU状态维护其实就是VCPU的上下文切换,而VCPU的环境结构主要有硬件使用部分和软件使用部分组成,软件部分主要由VMM控制,主要包括VCPU的状态信息,浮点寄存器等,而硬件使用部分指的是英特尔和AMD用来描述和保存VCPU状态信息的内存空间,它们分别存放在被称为VMCS(Virtual-Machine Control Structure,虚拟机控制结构)和VMCB(Virtual-Machine Control Block虚拟机控制块)的数据域中,VMCS和VMCB都是最大不超过4KB的内存块。在进行VCPU上下文切换的时候,要涉及硬件部分和软件部分两方面,而本篇主要是介绍和硬件部分关系密切的VMCS和VMCB。
介绍完两种操作模式,下面我们就以英特尔平台为例来对新指令和虚拟机控制结构进行进一步介绍。
如果暂不考虑EPT(内存虚拟化)相关指令,英特尔为VMX和VMCS共引入了十条指令,并且分别有明确的分工和定义,下面先简单介绍一下引入指令。
图13 每个VMCS对应一个虚拟CPU(假设每个虚拟机只用一个虚拟CPU)
VMX ON和VMX OFF是用来打开和关闭VMX操作模式的指令,在默认情况下,VMX是关闭的,当需要使用这个功能时,可以通过VMX ON随时进行VMX模式。在进入VMX模式后,VMM又会通过VMLAUNCH或者VM RESUME指令产生VMEntry,使CPU从根操作模式切换至非根操作模式,从而开始运行客户机相关软件。在运行软件的过程中如果发生中断或者异常,就会激活VM-Exit操作,此时CPU又进行了一次模式切换,只不过这次是切换到根操作模式,在处理完成后一般又会返回非根操作模式去运行客户机软件。如果不想运行虚拟机软件的时候,则会利用VMX OFF关闭VMX操作模式。除此之外,还有一条VMCALL指令,因为这个指令涉及到大家不常用的SMM(系统管理模式)VM Exit,所以这里就不多作介绍了。
而在VMCS方面,每个VMCS对应一个虚拟CPU(VCPU),在虚拟化软件的设置中,我们一般设置一个虚拟机对应一个虚拟CPU。
图14 VMCS的内部结构
而VMCS在使用时要与逻辑CPU绑定,一个逻辑CPU在任意的一个时间点都只能绑定一个VMCS,而VMCS在不同的时刻是可以和不同的逻辑CPU绑定的。VMCS用来绑定和解除绑定的命令分别是VMPTRLD和VMCLEAR,而用来对VMCS数据域进行读写的指令分别为VMREAD和VMWRITE。除此之外还有一条VMPTRST的指令,是指将当前的VMCS状态值存储到一个指定的内存空间。
最后我们来谈一谈VMCS结构,看一看这个4KB大小的内存空间里都有些什么。
在偏移0处是VMCS版本标识,偏移4处是VMX失败指示,这里将存放因VM-Exit执行不成功而产生的VMX失败原因,而我们下面要详细介绍的是在偏移8处的VMCS数据域。我们可以把这个数据域分成三部分:状态区域,控制区域和VM退出信息区域(见表1)。
表1 |
状态区域 |
客户机状态域 宿主机状态域 |
控制区域 |
VM执行控制域 VM-Exit控制域 VM-Entry控制域 |
VM退出信息区域 |
VM-Exit信息域 |
客户机状态域是用来保存非根模式VCPU运行状态的,当发生VM-Exit时,VCPU的当前运行状态将写入客户机状态域(并非全部,另有一部分为VMM控制的软件部分,下同),而当VM-Entry发生时,CPU会将客户机状态域中保存的状态加载到自己身上从而保证顺利地切换到非根操作模式。而宿主机状态域则用来保存在根操作模式下CPU的运行状态,它仅仅在发生VM-Exit时将状态值写入CPU中,而在VMEntry发生时不进行保存操作。
控制区域中VM-Entry控制域和VM-Exit控制域是对VM-Entry和VM-Exit操作的具体行为进行控制规定的地方,如VM-Entry控制域中的MSR加载、事件注入控制和VM-Exit控制域中的主机地址空间等,而VM执行控制域的作用是控制VM-Exit操作发生时的行为,比如某些敏感指令、异常和中断是否产生VM-Exit操作,也就是说只要是在这个控制域里列明的指令,都是可根据实际情况进行VM-Exit操作的开启和关闭操作的。当然没有写入控制域的一些指令也会产生VM-Exit操作,那些指令可以称之为无条件VM-Exit指令,凡是产生VM-Exit操作的指令都会由VMM来模拟完成。
VM-Exit信息域比较简单,存放的是VM-Exit产生的原因和具体的分类细化指标。
1、内核与系统调用基础
对于应用程序进程来说,操作系统内核的作用体现在一组可供调用的函数,称为系统调用(也成"系统服务")。
从程序运行的角度来看,进程是主动、活性的,是发出调用请求的一方;而内核是被动的,只是应进程要求而提供服务。从整个系统运行角度看,内核也有活性的一面,具体体现在进程调度。
系统调用所提供的服务(函数)是运行在内核中的,也就是说,在"系统空间"中。而应用软件则都在用户空间中,二者之间有着空间的间隔(CPU运行模式不同)。
综上所述,应用软件若想进行系统调用,则应用层和内核层之间,必须存在"系统调用接口",即一组接口函数,这组接口运行于用户空间。对于windows来说,其系统调用接口并不公开,公开是的一组对系统调用接口的封装函数,称为windowsAPI。
2、用户空间中的进程如何进行系统调用?
用户空间与系统空间所在的内存区间不一样,同样,对于这两种区间,CPU的运行状态也不一样。
在用户空间中,CPU处于"用户态";在系统空间中,CPU处于"系统态"。
CPU从系统态进入用户态是容易的,因为可以执行一些系统态特有的特权指令,从而进入用户态。
而相反,用户态进入系统态则不容易,因为用户态是无法执行特权指令的。
所以,一般有三种手段,使CPU进入系统态(即转入系统空间执行):
①中断:来自于外部设备的中断请求。当有中断请求到来时,CPU自动进入系统态,并从某个预定地址开始执行指令。中断只发生
在两条指令之间,不影响正在执行的指令。
②异常:无论是在用户空间或系统空间,执行指令失败时都会引起异常,CPU会因此进入系统态(如果原先不在系统空间),从而
在系统空间中对异常做出处理。异常发生在执行一条指令的过程中,所以当前执行的指令已经半途而废了。
③自陷:以上两种都CPU被动进入系统态。而自陷是CPU通过自陷指令主动进入系统态。多数CPU都有自陷指令,系统调用函数一般都是靠自陷指令实现的。一条自陷指令的作用相当于一次子程序调用,子程序存在于系统空间。
3、通过自陷指令调用系统服务流程:
windows系统通过自陷指令"int 0x2e"进入系统空间实现系统调用。
①CPU执行int 0x2e,CPU运行状态切换为系统态
②从任务状态段TSS装入本线程的系统空间的SS和ESP
③依次把用户空间的SS、ESP、EFLAGS、CS、EIP的内容压入系统空间堆栈
每个线程都有自己的系统空间堆栈,其堆栈段寄存器SS和堆栈指针ESP的内容保存在一个称为"任务状态段"既TSS的数据结构里面。
与此相应,CPU中有个 称为"任务寄存器"即TR的段寄存器.每当从用户空间进入系统空间时,CPU就自动根据TR的指引从TSS中获取当前进程的SS和ESP两个寄存器的值. 然后在把上面提到的几个寄存器中的内容压入这个堆栈.当然,还要根据IDTR即"中断描述符表寄存器"的指引获取CS和EIP. 这个过程所涉及的"时钟周期"显然不会少.正因为这样,后来才有了"快速系统调用"指令sysenter和sysexit的出现。
④从中断向量表中(Interrupt Descriptor Table)以0x2e为中断向量,开始执行系统空间中的程序。
⑤程序执行后,通过iret(中断返回)指令实现上述过程的逆过程
__declspec(naked) __stdcall NtReadFile(int dummy0,int dummy2,int dummy3)
{
__asm{
push ebp
mov ebp,esp
mov eax,152 //将NtReadFile()的系统调用号存入EAX
lea edx,8[ebp] //使EDX指向堆栈上参数块的起点,返回地址+Filehandle = 8 bytes
int 0x2E //进入内核
pop ebp
ret 9 //在堆栈上共有9个参数(win32汇编应该是9*4 :ret 36,)
}
}
//注意:此函数的参数在函数中并没有用到,因此其实是可有可无的
我们知道处理器一般存在应用编程接口和系统编程接口。对于x86处理器来说,应用编程接口仅向应用程序暴露了通用寄存器、RFLAGS、RIP和一组非特权指令,而系统编程接口向操作系统暴露了全部的ISA(Instruction Set Architecture)。传统的进程/线程模型也是对处理器的一种虚拟化,但只是对处理器的应用编程接口的虚拟化,而所谓的系统虚拟化(system virtualization)是要实现处理器系统编程接口的虚拟化。从这个角度讲,系统虚拟化与进程/线程模型相比并无本质的区别。
处理器虚拟化的本质是分时共享。实现虚拟化需要两个必要条件,第一是能够读取和恢复处理器的当前状态,第二是有某种机制防止虚拟机对系统全局状态进行修改。
第一个必要条件没有必要一定由硬件来实现,虽然硬件实现可能比软件实现更为简单。例如,x86处理器对多任务,也就是应用编程接口虚拟化,提供了硬件的支持,软件通常只需要执行一条指令,就可以实现任务切换,处理器硬件负责保存当前应用编程接口的状态,并为目标任务恢复应用编程接口的状态。但操作系统并不一定要使用处理器提供的这种虚拟化机制,完全可以使用软件来完成应用接口状态的切换。例如,Linux就没有使用x86处理器提提供多任务机制,完全依赖软件实现任务切换。
第二个必要条件一定要由硬件来实现,通常处理器采用多模式操作(multi-mode operation)来确保这一点。在传统x86处理器上,共有4种模式的操作,也就是常说的4个特权级。虚拟机(这里指进程/线程)通常运行在特权级3上,而虚拟机监控器(这里指操作系统)运行于特权级0上,进程/线程的所有访问全局的操作,如访问共享的操作系统所在的地址空间,访问I/O等等,均会导致异常的发生,被操作系统所截获并处理,使操作系统有机会向进程/线程提供一个虚拟的世界。
系统虚拟化与进程/线程模型相比并无本质的区别。x86处理器完全有机会以较小的代价提供对系统虚拟化的支持,但很可惜Intel没有考虑那么长远。x86的4个特权级对于实现系统虚拟化已经足够了,但传统的x86处理器上,许多特权指令要求必须在特权级0上执行,如LGDT,因此通常操作系统都占用了特权级0,也就没有特权级供虚拟机监控器使用了。为此,许多基于传统x86处理器的虚拟化软件不得不采用ring deprivileging方法,让操作系统运行于特权级1,而由虚拟机监控器使用特权级0。ring deprivileging方法带来了许多问题,包括:ring aliasing、address space compression、nonfaulting accessing to privileged state、adverse impact on guest transitions、interrupt virtualization、access to hidden state等问题,通常将以上问题统称为x86平台的虚拟化漏洞。
ring aliasing问题是指,采用ring deprivileging方法时,由于处理器的CPL保存在CS的低两位,所以操作系统通过执行PUSH CS指令和一条POP EAX指令可以很容易发现其目前不在特权级0上执行,这违背了虚拟化对操作系统透明的原则。
address space compression问题是指,操作系统通常期望能够访问整个4GB线性地址空间,但虚拟机监控器可能也需要占用操作系统的一部分线性地址空间,以便其能够方便地访问操作系统的地址空间。但如果操作系统是运行于特权级1,那么操作系统也同样可以访问虚拟机监控器的存储空间,对虚拟机监控器造成威胁。
nonfaulting accessing to privileged state问题是指,Intel的特权级机制不能确保所有的访问处理器状态的指令在低特权级状态下执行时都产生故障(Fault),这使得操作系统在访问某些处理器状态时虚拟机监控器无法获得控制,也就无法对这些指令进行仿真。例如,IA-32的GDTR, LDTR, IDTR, TR包含了控制处理器状态的指针,对这些寄存器的修改只能在特权级0进行,但IA-32允许在所有的特权级中读取这些寄存器的值。操作系统可以读取这些寄存器的值,如果与真实的计算机上的值不同,操作系统就可以认为自己正运行在虚拟机环境中。
adverse impact on guest transitions问题是指,为加快系统调用的速度,Intel引入了SYSENTER和SYSEXIT指令,但SYSENTER指令总是将特权级切换到0,且从0以外的特权级执行SYSEXIT指令将导致故障。因此,在采用ring deprivileging方法实现虚拟化时,SYSENTER和SYSEXIT指令总是先陷入到虚拟机监控器,经后者仿真后再交给操作系统,这使系统调用的速度减慢。
interrupt virtualization问题是指,IA-32使用EFLAGS.IF位来控制中断的屏蔽,修改IF位需要在CPL<=IOPL的情况下进行,否则将产生故障。操作系统可能需要频繁地修改IF位,会频繁地导致虚拟机监控器的陷入,影响系统性能。而且,有些情况下,虚拟机监控器需要向虚拟机注入事件,但如果虚拟机正处于中断屏蔽状态,虚拟机监控器就必须等待,直到虚拟机打开中断。虚拟机监控器为了及时得知虚拟机已打开中断,也必须截获操作系统对EFLAGS.IF位的修改。
access to hidden state问题是指,IA-32处理器的某些状态,例如段描述符高速缓存,是无法通过指令访问的。当虚拟机切换时,IA-32没有提供保存和恢复段描述符高速缓存的手段。也就是说,上文所述的实现虚拟化的第一个必要条件,能够读取和恢复处理器的当前状态,并不完全具备。
总之,虽然采用ring deprivileging方法可能实现系统虚拟化,但具有很多缺陷,且软件上比较复杂。为此,Intel提出了VT-x技术来解决系统虚拟化问题,其主要思路是增加一个新的比0还高的特权级,通常称之为特权级-1,并在硬件上支持系统编程接口状态的保存和恢复。
Xen的敏感指令陷入
Tag: XEN Virtualization
Xen虚拟机系统所采用的半虚拟化技术通过软件方法实现了x86架构的虚拟化,解决了x86架构所固有的虚拟化缺陷,即敏感和特权指令无法被VMM所捕获的缺陷。
G.Popek和R.Goldberg在1974年发表的论文中提到,作为向上层VM提供底层硬件抽象的一层轻量级的软件,VMM必须满足以下3个条件:
1.等价性(Equivalence) :应用程序在VMM 上的虚拟机执行,应与物理硬件上的执行行为相同。
2.资源控制(Resource Control) :物理硬件由VMM全权控,VM 及VM上的应用程序不得直接访问硬件。
3.有效性(Efficiency) :在虚拟执行环境中应用程序的绝大多数指令能够在VMM不干预的情况下,直接在物理硬件上执行。
CPU的指令按照运行级别的不同,可以划分为两类:
1.特权指令 :只能在最高级别上运行,在低级别状态下执行会产生trap。例如:LIDT只能在系统模式下执行,在其他模式下都会产生trap,中止执行。
2.非特权指令 :可以在各个级别的状态下执行。
引入虚拟化后,Guest OS就不能运行在Ring 0上。因此,原本需要在最高级别下执行的指令就不能够直接执行,而是交由VMM处理执行。这部分指令称为敏感指令 。当执行这些指令时,理论上都要产生trap被VMM捕获执行。敏感指令:Guest OS中必须由VMM处理的指令,因为这些指令必须工作在0环。
敏感指令包括:
1.企图访问或修改虚拟机模式或机器状态的指令。
2.企图访问或修改敏感寄存器或存储单元,如时钟寄存器、中断寄存器等的指令。
3.企图访问存储保护系统或内存、地址分配系统的指令。
4.所有I/O指令。
根据Popek和Goldberg的理论,如果指令集支持虚拟化就必须满足所有的敏感指令都是特权指令 。这样,当Guest OS运行在非最高特权级时,执行任意特权指令都能产生trap。该条件保证了任何影响VMM或VM正确运行的指令在VM上执行时都能被VMM捕获并将控制权转移到VMM上,从而保证了虚拟机环境的等价性和资源可控制性,保证虚拟机正确运行。
但是,x86架构并不满足这个条件。由于有些敏感指令不属于特权指令,从而阻碍了指令的虚拟化。(x86不满足的原因:有些必须由VMM处理的0环指令,工作在1环也不会产生trap,即敏感指令包含非特权指令。但是敏感指令必须工作在0环,所以这些非特权指令也必须陷入。)x86结构上的这些不是特权指令的敏感指令,称为临界指令 (Critical Instructions)。这些临界指令在x86架构下有17个,主要包含敏感指令的两类:敏感寄存器指令和保护系统指令(上面的2,3类)。
敏感寄存器指令:SGDT,SIDT,SLDT ;SMSW; PUSHF,POPF;
保护系统指令:LAR,LSL,VERR,VERW;POP ;PUSH ;CALL,JMP,INT n,RET ;STR ;MOV;
1.SGDT,SIDT,SLDT
三个指令分别表示:存全局描述符表(store GDT),存中断描述符表(store IDT),存本地描述符表(store LDT)。其中,SGDT和SIDT是将寄存器的值保存到一个6字节的存储单元中,SLDT是存到一个16或32位的寄存器中或存储单元中。
使用如下:SGDT m; SIDT m;SLDT r/m16
三 个指令只能被操作系统使用,但没有被设为特权指令,当处于低级别的客户操作系统执行它们时,能够直接获得寄存器的值。但是,由于在硬件平台上的寄存器都只 有一个,因而位于不同虚拟机中的Guest OS所获得的值只有一个。这显然是不正确的,因此VMM会为每个虚拟机配备一套虚拟的GDTR,IDTR, LDTR,以便客户操作系统访问寄存器的操作 被VMM捕获,并重新定向访问相应的虚拟寄存器。
2.SMSW
SMSW表示存机器状态字(store machine status word),即将机器状态字的值(CR0中低16位的值)保存到一个寄存器或存储单元中,设置该指令是为了向下兼容286处理器,而之后的处理器都使用MOV指令读取机器状态字的值。
使用如下: SMSW r/m16
同样,该指令没有被设定为特权指令。当Guest OS查询机器状态时,其得到的是实际物理寄存器的状态,即VMM的状态,并非是Guest OS的状态。所以也需要设置相应的虚拟寄存器CR0。
3.PUSHF,POPF
POPF(pop stack into EFLAGS register)和PUSHF(push EFLAGS register onto the stack)是一对相反的指令。PUSHF是将EFLAGS的寄存器的低16位压栈,并将栈指针减2。 POPF是从栈顶弹出一个字到EFLAGS的寄存器中,并将栈指针加2。对应的32位指令为:PUSHFD和POPFD。
4.LAR,LSL,VERR,VERW
四 个指令分别表示:加载访问权限(load access rights byte),加载段界限(load segment limit),验证段可读(Verify a segment for reading)、验证段可写(verify a segment for writing)。其中,LAR指令是从指定的段描述符中加载访问权限到另一个寄存器,并设置EFLAGS寄存器中的ZF标志位;LSL指令从指定的段描 述符中加载段界限到另一个寄存器中,并设置ZF标志位;VERR/VERW指令是在当前的特权级下验证指定的段是否可读/可写。若是,则设置ZF标志位。
使用如下:LAR r16,r16/m16 LSL r16,r16/m16
VERR r/m16 VERW r/m16
需要访问段描述符,但是Guest OS不处于最高级别,访问不能正确被执行。与CPL相关。
5.POP
同样,与CPL相关。当POP到段寄存器时,需要比较CPL。由Guest OS运行在ring 3上引起。
6.PUSH
同POP类似,PUSH不能应用于CS,DS 段寄存器,也与CPL相关。
7.Call,JMP,Int n,Ret
8.STR
Store Task Register,将当前任务寄存器的段选择符存入通用寄存器或存储单元中,这个段选择符指向TSS段。
9.MOV