-
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 (guest 调用 hypervisor 的服务),并每次把 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
指令,从下一条指令处开始执行。
本阶段主要工作是实现了 VmxVcpu::run()
函数:
#[repr(C)]
pub struct VmxVcpu<H: RvmHal> {
guest_regs: GeneralRegisters,
host_stack_top: u64,
vmcs: VmxRegion<H>,
}
impl<H: RvmHal> VmxVcpu<H> {
pub fn run(&mut self) -> ! {
VmcsHostNW::RSP
.write(&self.host_stack_top as *const _ as usize)
.unwrap();
unsafe { self.vmx_launch() }
}
}
该函数首先将 VMCS host RSP
设为 &self.host_stack_top
,即 VmxVcpu::guest_regs
结构的末尾,使得发生 VM exit 时可以直接用 PUSH
指令保存 guest 通用寄存器到 VmxVcpu::guest_regs
。
之后进入 VmxVcpu::vmx_launch()
:
#[naked]
unsafe extern "C" fn vmx_launch(&mut self) -> ! {
asm!(
"mov [rdi + {host_stack_top}], rsp", // save current RSP to Vcpu::host_stack_top
"mov rsp, rdi", // set RSP to guest regs area
restore_regs_from_stack!(),
"vmlaunch",
"jmp {failed}",
host_stack_top = const size_of::<GeneralRegisters>(),
failed = sym Self::vmx_entry_failed,
options(noreturn),
)
}
这段代码是用纯汇编写成的 (Rust #[naked]
属性),流程如下:
- 将当前的 host
RSP
保存进self.host_stack_top
(RDI
是该函数的第一个参数,即self
)。 - 切换 host
RSP
到RDI
,即self.guest_regs
结构的开头。 - 通过
restore_regs_from_stack!()
宏,将VmxVcpu::guest_regs
结构中存储的 guest 通用寄存器依次 pop 出来。 - 执行
VMLAUNCH
,进入 guest。 - 如果
VMLAUNCH
执行失败,将不会进入 guest,而是执行之后的指令,这里就直通跳转到了错误处理函数VmxVcpu::vmx_entry_failed()
(目前处理方式是直接 panic)。
VMCS 中设置了 host RIP
为 VmxVcpu::vmx_exit()
函数的地址,因此 VM exit 时会直接跳转到该函数:
#[naked]
unsafe extern "C" fn vmx_exit(&mut self) -> ! {
asm!(
save_regs_to_stack!(),
"mov r15, rsp", // save temporary RSP to r15
"mov rdi, rsp", // set the first arg to &Vcpu
"mov rsp, [rsp + {host_stack_top}]", // set RSP to Vcpu::host_stack_top
"call {vmexit_handler}", // call vmexit_handler
"mov rsp, r15", // load temporary RSP from r15
restore_regs_from_stack!(),
"vmresume",
"jmp {failed}",
host_stack_top = const size_of::<GeneralRegisters>(),
vmexit_handler = sym Self::vmexit_handler,
failed = sym Self::vmx_entry_failed,
options(noreturn),
);
}
其流程如下:
- 通过
save_regs_to_stack!()
宏,向VmxVcpu::guest_regs
依次 push guest 通用寄存器 (之前已设置 VMCS hostRSP
)。 - 保存此时的 host
RSP
到临时寄存器R15
,便于之后再次 VM entry 时恢复 guest 通用寄存器。 - 将
RDI
设为RSP
,即接下来的函数调用的第一个参数为VmxVcpu
结构。 - 从
VmxVcpu::host_stack_top
恢复 hostRSP
,即切换到执行VmxVcpu::vmx_launch()
前的栈。 - 调用 VM exit 处理函数
VmxVcpu::vmexit_handler()
。之后会通过RvmHal
trait 进一步调用 hypervisor/src/hv/vmexit.rs 中的vmexit_handler()
。 - VM exit 处理完毕,从
R15
恢复栈,并恢复 guest 通用寄存器,准备再次进入 guest。 - 执行
VMRESUME
,再次进入 guest。 - 如果
VMRESUME
执行失败,直通跳转到VmxVcpu::vmx_entry_failed()
。
-
去掉
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); }