-
Notifications
You must be signed in to change notification settings - Fork 32
Step 3: 第一段 Guest 代码
在本阶段,我们将在我们的 hypervisor 之上运行一个最小的 guest 软件,并处理一些简单的 VM exit。
代码:https://github.com/equation314/RVM-Tutorial/commit/169e8d2bdc8bc67daf8f28b5338694d0be1cae5a
架构手册:Intel 64 and IA-32 Architectures Software Developer’s Manual (SDM) Vol. 3C, Chapter 24 ~ Chapter 27
#[naked]
unsafe extern "C" fn test_guest() -> ! {
core::arch::asm!(
"mov rax, 0", // RAX = 0;
"mov rdi, 2", // RDI = 2;
"mov rsi, 3", // RSI = 3;
"mov rdx, 3", // RDX = 3;
"mov rcx, 3", // RCX = 3;
"2:", // while (true) {
"vmcall", // VMCALL(RAX, RDI, RSI, RDX, RCX);
"add rax, 1", // RAX += 1;
"jmp 2b", // }
options(noreturn),
);
} // hypevisor/src/hv/mod.rs
我们要运行的目标 guest 代码很简单,就是不停地执行 hypercall,并每次把 hypercall ID (RAX
) 加 1。其中没有访问指令、栈操作,没有 I/O、MSR、控制寄存器访问,也没有中断和异常处理,一般情况下也不会发生异常。
在 VM entry 和 VM exit 时,我们需要切换 host 和 guest 的状态,这一过程即 vCPU 上下文切换 (context switch),与传统 OS 中线程间、用户内核间的上下文切换类似。
对于 VMCS 中的状态,发生 VM entry 或 VM exit 时,硬件会自动完成切换。但对于不在 VMCS 中的状态就需要软件手动进行切换,如下列通用寄存器 (RSP
已在 VMCS 状态中):
GPRs | |
---|---|
RAX |
R8 |
RCX |
R9 |
RDX |
R10 |
RBX |
R11 |
R12 |
|
RBP |
R13 |
RSI |
R14 |
RDI |
R15 |
因此,我们需要在 VM entry 时,先手动从内存载入 guest 通用寄存器,再执行 VMLAUNCH
/VMRESUME
,让硬件自动切换 VMCS 中的状态,然后进入 guest。在 VM exit 时,硬件先自动完成了 VMCS 中状态的切换,但此时的通用寄存器还是 guest 的,需要将它们保存到内存中,以便处理 VM exit 时能够访问。
由于在我们的实现中,通过 VMLAUNCH
进入 guest 后,不再执行之后的指令。当发生 VM exit 回到 host 模式时,也是跳转到 VM exit 处理函数,并不会回到 VMLAUNCH
之后。VM exit 时也无需用到之前保存的 host 通用寄存器。因此,我们可以无需保存与恢复每次 VM entry 前的 host 通用寄存器。
为了方便实现 guest 通用寄存器的保存与恢复,我们可以将栈临时切换到 guest 通用寄存器所在的内存,然后通过 push
/pop
指令实现保存与恢复。但在 VM exit 保存完 guest 通用寄存器后,需要进行栈的恢复,因此我们还需保存 VM entry 前的栈指针 (host RSP
) 并在之后恢复,使得处理 VM exit 时能继续使用进入 guest 前的那个栈。详见下图和相关代码。
我们还需要对上阶段配置的 VMCS 进行一些微调,使其适应本阶段的 guest 执行环境。
我们希望这段 guest 代码运行在 64 位,因此必须提供一个 guest 页表。由于在本阶段还未实现 guest 与 host 的内存隔离,所以为了简单,可以直接让 guest 和 host 在同一地址空间,共用一个页表。
因此我们对 VMCS guest-state area 进行以下调整:
-
RIP
设为test_guest()
函数的地址。 -
RSP
可不设置 (无栈操作)。 -
RFLAGS.IF = 0
(关中断)。 -
CR3
设为 hostCR3
(与 host 共用页表) - 64 位 guest:
-
CR0.PE = 1
,CR0.PG = 1
,CR4.PAE = 1
-
IA32_EFER.LME = 1
,IA32_EFER.LMA = 1
CS_ACCESS_RIGHTS.L = 1
-
对于 VMCS host-state area,需要设置:
-
RIP
设为 VM exit handler 地址 (开始 push guest 通用寄存器的指令地址)。 -
RSP
设为保存 guest 通用寄存器区域的末尾 (Vcpu::guest_regs
)。
此外,还需设置 VM-entry controls 中的 "IA-32e mode guest" 使得 VM entry 时切换到 64 位 guest。
首先通过 VMCS VM-exit information fields 中的 Exit reason 字段,获取 VM exit 的基本信息,其中的主要位有:
- Basic exit reason:基本原因,见 SDM Vol. 3D, Appendix C。
- VM-entry failure:表明 VM-entry 失败,如发生了原因为 "VM-entry failure due to invalid guest state" 或 "VM-entry failure due to MSR loading" 的 VM exit。
其他常用的 VMCS VM-exit information fields 有:
VMCS 字段 | 描述 |
---|---|
Exit qualification | 因 I/O 指令、EPT violation、控制寄存器访问等导致的 VM exit 的详细信息 |
VM-exit instruction length | 因执行特定指令导致 VM exit 时的指令长度 |
VM-instruction error field | VMX 指令执行失败时错误码 |
Guest-physical address | EPT violation 时的出错 guest 物理地址 (Step 4) |
VM-exit interruption information | 因外部中断导致 VM exit 时的详细信息 (Step 6) |
本阶段只需处理原因为 VMCALL 的 VM exit,即 hypercall 的处理。
一般情况下,类似 syscall 的处理,我们需要根据 guest 通用寄存器,取出 hypercall ID、参数等信息,然后根据 hypercall ID 调用相应的 hypervisor 服务。
但目前我们的 hypervisor 不对 guest 提供任何 hypercall 结构,就无需处理。不过需要让 guest RIP
加上 VMCALL
指令的长度,使得之后通过 VMRESUME
再次进入 guest 时,能够跳过 VMCALL
指令,从下一条指令开始执行。
-
去掉
test_guest()
函数之前的#[naked]
,- 再运行会发生什么?
- 为什么会这样?(提示:通过
make disasm
查看反汇编后的代码) - *适当修改代码的其他部分,使得去掉
#[naked]
后仍可以正常运行。
-
*我们的 hypervisor 在处理 VM exit 时,复用了第一次进入 guest 前的那个栈。请修改代码,使用一个专门的栈来处理 VM exit。
-
**在目前的实现中,
VmxVcpu::run()
的返回值是!
,即永不返回。请修改run()
函数的实现,使其在发生 VM exit 时能够返回,返回 VM exit 的相关信息 (类型为VmxExitInfo
),并将 guest 的运行与 VM exit 的处理改成以下形式:loop { let exit_info = vcpu.run(); vmexit_handler(exit_info); }