System|隔离|虚拟化

2021-11-22 11:42:24 浏览数 (1)

ref:CSE,SE,SJTU

虚拟化

我们需要在一个物理机上运行多个不同的OS,从而实现软件上的

  • 隔离 - 如果OS崩了,那不会影响其他的OS
  • 可维护性 - 易部署克隆移植备份(容器化)
  • 安全 - 虚拟机自省

因此,我们在传统的OS的位置放置一个支持虚拟化的系统,称为VMM(VM Monitor) or Host. 每一个其上运行的虚拟机成为一个Guest。(当然也可以在传统OS和VM之间加入一层VMM,但是这样性能会比较差)

VMM具有高优先级,管理多个硬件。但是和传统OS具有不同的抽象,仅负责调度VM。

为了实现虚拟化,需要在三个层面进行隔离

  • CPU - 每个虚拟机各自拥有自己的user/kernel mode
  • Memory - 每个虚拟机各自拥有自己的虚拟MMU
  • IO - 每个虚拟机各自拥有自己的虚拟设备

CPU 虚拟化

尝试I - VM as 进程

对于每个进程而言,CPU似乎都只被自己占有。如果我们直接把VM作为传统的进程,结果会如何?

宿主机作为一般的应用

问题在于,如果按照这样的方式,VM处于user mode,某些特殊的指令(如关中断,change CR3)就无法执行。VM是无法执行高优先级指令的。

尝试II - Trap & Emulation

解决方式显而易见,如果VM无法执行,那么只需要转交给VMM执行即可。

  • Trap - 调用高优先级指令时,由于处于user mode,因此trap到宿主机
  • Emulate - 实现对应的函数,将VM的系统状态存储到VMM的内存,VM并没有操作硬件的CR3,只是修改了宿主机的内存而已。

问题在于,Intel的指令集一开始并不天然支持虚拟化。

如popf,将pop出的内容存到flag寄存器里。对于IF(中断)而言,如果处于kernel mode,那么popf会更新IF,如果处于user mode,popf只会被忽略掉(而不是发生trap)。然而,我们无法区分同样是在user mode运行情况下,VM究竟是在

这类指令共有17条。

SGDT, SIDT, SLDT, SMSW, PUSHF, POPF, LAR, LSL, VERR, VERW, POP, PUSH, CALL, JMP, INT n, RET, STR, MOV

尝试III - 处理特殊指令

1.指令解释器:软件模拟

软件写个CPU模拟器,容易实现,但是性能非常低。(因为所有指令都是在对内存操作)

2.二进制翻译: 翻译为其他指令

每个basic block进行翻译,将这些指令翻译成VMM提供的函数调用,并存在code cache中。

Inline Call

然而问题依然存在,

首先是某些语言如Java会修改源代码(SMC),因此需要一个机制通知cache已经过期,否则执行的就是错误的代码;

其次,这就导致了每次Guest转交整个basic block,如果发生PC同步的中断,只会在边界处发生,而真实机器则可能在每个指令发生。

总结一下,虚拟机的kernel执行这些代码时,发生二进制翻译,而应用程序执行这些代码时则进行Trap&Emulate

3.半虚拟化: 直接在源代码中替换

这要求虚拟机中的程序清楚地意识到自己处于虚拟化运行

修改OS的代码,使得其中的指令直接变成对VMM的函数调用(hypercall)(eg. Xen)

4.硬件支持虚拟化: 改变 CPU(主流)

CPU分为两种模式,Root和非Root,每个模式都从ring-0至ring-3有四个优先级。由CPU本身存储虚拟化所需求的bit。VMM负责处理Exit

内存虚拟化

在虚拟化设计中,存在着三种地址,GVA, GPA,HPA。(G-guest,H-host)

然而对于虚拟机而言,他并不知道哪些HPA是属于自己的。如果设置VM的CR3直接指向GPT的话,很有可能使得VM访问越界。

尝试I-Shadow Page

我们并不让CR3指向属于VM管理的GPT,而是属于VMM管理的SPT(注意,软件模拟虚拟化的时候VM的虚拟CR3也在VMM上)。相当于两表合一,通过SPT直接寻址到宿主机的物理地址

代码语言:javascript复制
set_cr3 (guest_page_table):
    for GVA in 0 to 220
        if guest_page_table[GVA] & PTE_P:
            GPA = guest_page_table[GVA] >> 12
            HPA = host_page_table[GPA] >> 12
            shadow_page_table[GVA] = (HPA<<12)|PTE_P
        else
            shadow_page_table = 0
     CR3 = PHYSICAL_ADDR(shadow_page_table)

那么,如何同步GPT和SPT呢?答案很简单,就是将GPT设置为只读,如果要写的话,就会触发Page Fault,然后由VMM处理并且同步SPT。

如果Guest上的应用通过SPT访问的话,同时也能访问到Guest的kernel space。所以需要对两种模式分别创建SPT,其中user态缺失了kernel space。

对于每个VM,需要一张HPT,对于每个VM上的进程,又需要一张GPT,并且两张SPT。

尝试II-直接映射

修改guest OS的源代码,让GVA直接映射到HPA。对于页表的修改只能通过hypercall进行,而对于guest OS只读。

但是这样做对guest OS不那么透明,并且guest OS会了解很多HPA相关的信息,可能会导致rowhammer攻击(见下节)

尝试III-硬件支持虚拟化

Intel EPT(extended)和AMD NPT(nested)为每个VM单独创立了一个表,用于将GPA映射到HPA上,由VMM管理。

IO 虚拟化

for OS lecture

尝试I -Device emulation

尝试II-Para-virtualization driver, e.g., virtio

尝试III- Hardware support, e.g., SR-IOV

0 人点赞