-
Notifications
You must be signed in to change notification settings - Fork 32
Step 4: EPT 与内存隔离
在本阶段,我们将配置 EPT,实现内存虚拟化,来对 guest 与 hypervisor 进行内存隔离。
代码:https://github.com/equation314/RVM-Tutorial/commit/a63808c4e35f206649e30e7891d071cb690bf3e3
架构手册:Intel 64 and IA-32 Architectures Software Developer’s Manual (SDM) Vol. 3C, Chapter 28
在启用分页机制的处理器中,软件先使用虚拟地址,经过页表的转换,转换为物理地址,才能访问物理内存。在虚拟化中,我们需要虚拟化 guest VM 的物理内存。此时 guest 上的软件使用的就是 guest 虚拟地址 (gVA),通过 guest 页表转换为 guest 物理地址 (gPA)。而 hypervisor 不能让 guest 直接用 guest 物理地址去访问物理内存,需要再经过一次转换,转换为 host 物理地址 (hPA) 才能访问。这就是内存虚拟化所涉及的三种地址。
实现内存虚拟化有两种方式:无需硬件支持的影子分页,和需要硬件支持的嵌套分页。
对于影子分页,无需专门的硬件,即无嵌套分页支持的硬件。早期的 VMX 虽然是硬件虚拟化,但还未实现 EPT,就需要通过影子分页实现内存虚拟化。影子分页只使用了普通页表就能实现 guest 物理内存的虚拟化。
对于 guest OS 来说,一般情况下意识不到 hypevisor 的存在,因此会像在真实机器上那样,配置好 guest 页表,负责把 gVA 转换为 gPA。而在 hypervisor 中也会维护一个 guest 物理内存映射,负责把 gPA 转换为 hPA,但不一定要是页表这样的分级结构,因为硬件不会直接使用。
影子页表 (shadow page table) 就是通过合并 guest 页表与 hypervisor 中的 guest 物理内存映射,直接得到 gVA 到 hPA 的映射,然后根据这一映射构造出的普通页表。运行 guest 时,只需将 host 页表基址 (CR3
) 设为影子页表,当 guest 使用 gVA 进行访问时,就经过 host 页表的转换,直接得到 hPA,从而正确进行内存访问。
当 guest 修改自身页表时,hypervisor 需要进行拦截,并修改对应的影子页表,这可以通过在影子页表中去掉 guest 自身页表的映射来实现。当 guest 切换 CR3
以切换地址空间时,hypervisor 也需要进行拦截,并切换对应的影子页表。
嵌套分页又称二维分页 (2-dimensional paging)、二级地址转换 (Second Level Address Translation),是由硬件提供了一个类似普通页表的结构,把 gPA 转换为 hPA,即嵌套页表 (nested page table)。
Intel VMX 中的嵌套页表即 EPT (Extended Page Table),而 AMD SVM 中的就叫 NPT (Nested Page Table),还有 ARM 中的叫 Stage-2 Page Table。
嵌套页表的基址 (NPT root),以及 guest 页表基址在硬件中都有对应的寄存器。嵌套页表和 host 页表是独立的,互不影响。当 guest 使用 gVA 进行访问时,硬件会先通过 guest 页表转换为 gPA,再通过嵌套页表转换为 hPA,才能进行内存访问,因此会比用影子分页多一次页表结构的遍历。当 guest 应用 TLB 缺失比较严重时,最坏情况下会触发多达 20 多次内存访问,带来严重的性能下降。在实际应用中我们可以通过在嵌套页表中使用大页来减少这种开销。
影子分页 | 嵌套分页 | |
---|---|---|
特殊硬件 | ✘ | ✓ |
实现方式 | 复杂 | 简单 |
TLB 缺失开销 | 小 | 大 |
映射修改开销 | 大 | 小 |
页表切换开销 | 大 | 小 |
内存空间占用 | 大 | 小 |
本项目会使用 Intel EPT 提供的嵌套分页机制,实现内存虚拟化。
EPT 与普通页表结构相似,一般都是 4 级,而且都可以设置大页来直接映射一块 1GB 或 2MB 大小的页面。guest 页表基址保存在 VMCS guest CR3 字段中,EPT 基址保存在 extended-page-table pointer (EPTP) 字段中。
下图展示了一个 guest 虚拟地址,如何通过 guest 页表转换为 guest 物理地址,再通过 EPT 转换为 host 物理地址。
与普通页表一样,EPT 页表的每个表项都有一些标志位,表示访问权限等信息。
下图分别给出了 EPT pointer 和几种页表项的格式,包括指向下一级页表的 table 表项、指向 1GB/2MB 大页的表项、以及指向 4KB 页面的表项:
其中低 3 位 RWX 分别为读写执行权限位;第 7 位指示大页还是中间级页表;3 至 5 位为内存类型,对于普通内存就是 6,启用 write-back cache,对于 MMIO 这样的设备内存就是 0 不启用 cache;从第 12 位开始都是一个 (host) 物理地址,表示下一级页表物理地址,或是目标的页表的物理地址。
当硬件使用 EPT 转换一个 gPA 时,如果中途发生了页面不存在,或是权限不匹配等错误时,会触发一个 VM exit,名为 EPT violation,类似普通页表中的缺页异常 (page fault)。
一般情况下,发生 EPT violation,就是 guest 非法访问了一个 guest 物理地址,应该杀掉整个 guest 或报错。但我们也可以利用 EPT violation 实现一些功能。如实现页面交换、按需分配 guest 物理内存、虚拟化对设备的 MMIO 访问等。
VMCS 中有提供了一些与 EPT violation 有关的信息,比如 Exit qualification 会保存访问者的权限信息,用 bit 0/1/2 分别表示是一个 读/写/执行 访问导致了这个 EPT violation (类似缺页异常时的 error code)。此外 Guest-physical address 表示出错的 guest 物理地址,Guest-linear address 表示出错的 guest 虚拟地址等。
最后,我们给出一个普通页表和 EPT 的对比:
Page Table | Extended Page Table | |
---|---|---|
地址转换 | VA → PA | gPA → hPA |
基址 | CR3 | VMCS EPT pointer |
转换失败 | 缺页异常 (#PF) | VM Exit: EPT violation |
Invalidate TLB |
MOV to CR3 |
INVEPT |
在 hypervisor/src/hv/mod.rs 中,我们增加了对 guest 物理内存的初始化。Guest 物理内存布局如下:
我们在代码中使用一个大数组 GUEST_PHYS_MEMORY
表示 guest 的所有物理内存 (16MB),并如上图所示配置 guest 页表、guest entry、guest RSP
等 guest 状态。为了便于实现,guest 页表使用对等映射和大页,映射了 0 ~ 0x4000_0000
这段 1GB 大小的 guest 物理内存,且 gVA 等于 gPA。我们用一个 GuestPhysMemorySet
结构来管理 guest 的所有物理内存段,并同时构造嵌套页表,其定义如下:
// hypervisor/src/hv/gpm.rs
pub struct GuestPhysMemorySet {
regions: BTreeMap<GuestPhysAddr, MapRegion>,
npt: NestedPageTable<RvmHalImpl>,
}
我们对上一阶段的 test_guest()
函数稍作修改,不再使用纯汇编编写,并在最后故意访问一个非法 guest 物理地址,手动触发 EPT violation:
unsafe extern "C" fn test_guest() -> ! {
for i in 0..100 {
core::arch::asm!(
"vmcall",
inout("rax") i => _,
in("rdi") 2,
in("rsi") 3,
in("rdx") 3,
in("rcx") 3,
);
}
core::arch::asm!("mov qword ptr [$0xffff233], $2333"); // panic
loop {}
}
在 hypervisor/src/hv/vmexit.rs 中,我们处理了 EPT violation。目前处理方法只是输出出错的 guest RIP
和 guest 物理地址,然后直接 panic。
在 rvm/src/arch/x86_64/vmx/vcpu.rs 中,我们对 VMCS 配置进行了一些微调。包括:
- 对外提供设置 guest
CR3
和RSP
的接口; - 设置 VMCS secondary processor-based VM-execution controls 的 Enable EPT 位 (bit 1),来启用 EPT。
- 在 VMCS extended-page-table pointer 中设置 EPT 基址,设置 EPT 级数为 4 级,再用
INVEPT
指令无效 TLB。
在 rvm/src/mm/page_table.rs 中,我们定义了一个通用的页表项接口 GenericPTE
,并用此实现了一个通用的 4 级页表结构 Level4PageTable
。不仅是 EPT,该结构还可用于实现对普通页表,甚至不同体系结构的 ARM、RISC-V 页表的操作。
Level4PageTable
中实现了页表的映射 map()
、取消映射 unmap()
、修改映射 update()
、查询映射目标 query()
,其核心是 get_entry_mut()
与 get_entry_mut_or_create()
这两个函数。
在 rvm/src/arch/x86_64/vmx/ept.rs 中,EPT 使用这套通用的页表结构实现。我们按照 EPT 页表项的格式,为 EPTEntry
结构实现了 GernericPTE
trait,将其作为泛型参数传给 Level4PageTable
即可得到 EPT 结构:
type ExtendedPageTable<H> = Level4PageTable<H, EPTEntry>;
- 假设一个 hypervisor 中运行有 4 个 guest VM,每个 guest VM 中运行有 10 个应用 (需要 10 个 guest 页表),请问在用影子分页方式实现内存虚拟化时,hypervisor 共需维护多少份影子页表?
- 假设在 guest OS 中启用了 4 级页表,如果 guest 的一次访存 (使用 guest 虚拟地址) 发生了 TLB 缺失,请问分别用以下方法实现内存虚拟化时,最多会导致多少次内存访问?(均为 4 级页表)
- 影子分页
- 嵌套分页
- **(需要编码) 在目前的实现中,我们一开始就预先分配好了 guest 的所有物理内存 (
GUEST_PHYS_MEMORY
),但这样也使得内存利用率不高。请将 guest 物理内存改为按需分配 (on demand),以提高内存利用率。- 提示:你需要在 EPT 中先不建立 gPA 到 hPA 的映射,直到发生 EPT violation 时才建立,并正确处理一些 guest 物理内存段的初始化。