Example: Writing a Hypervisor in About 100 Lines of Safe Rust
This example demonstrates OSTD’s hypervisor support on Intel x86 by implementing a minimal hypervisor entirely in safe Rust.
This minimal hypervisor aims at running the following Hello World assembly program in a guest.
The guest loops over the "Hello World\n" string and writes each character to COM1 via the out instruction.
# SPDX-License-Identifier: MPL-2.0
.code16
.global _start
.section .text
_start:
xorw %ax, %ax
movw %ax, %ds
cld
movw $message, %si
movw $0x3f8, %dx
.Lprint:
lodsb
testb %al, %al
jz .Lhalt
outb %al, %dx
jmp .Lprint
.Lhalt:
hlt
message:
.asciz "Hello World\n"
RFC-0003 describes the OSTD support required by this minimal hypervisor. Three core OSTD abstractions provide the kernel with hypervisor capabilities:
GuestPhysMemSpacemaps guest physical addresses to OSTD-managed frames using EPT.GuestContextprovides access to the guest CPU state.GuestModeprovides an interface to run the guest usingGuestContextandGuestPhysMemSpace.
The code of this minimal hypervisor is shown below:
// SPDX-License-Identifier: MPL-2.0
#![no_std]
#![deny(unsafe_code)]
use ostd::{
Error,
arch::vm::{GuestContext, VcpuMsrs, VcpuRegs, VcpuSregs, VmxExitReason},
mm::{CachePolicy, FrameAllocOptions, PAGE_SIZE, PageFlags, PageProperty, VmIo},
power::{ExitCode, poweroff},
prelude::*,
task::{TaskOptions, disable_preempt},
vm::{DummyGuestHooks, GuestMode, GuestPhysMemSpace, GuestReturnReason},
};
#[ostd::main]
fn main() {
// VMX lifecycle operations need a task context with interrupts enabled.
TaskOptions::new(|| {
run_guest();
poweroff(ExitCode::Success);
})
.spawn()
.unwrap();
}
fn run_guest() {
let guest_mode = match GuestMode::new() {
Ok(guest_mode) => guest_mode,
Err(Error::NotEnoughResources | Error::AccessDenied) => {
println!("Error: VMX is not supported by the VM");
return;
}
Err(err) => panic!("Failed to initialize VMX: {err:?}"),
};
// Prepares the guest code by writing to the guest memory space.
let guest_mem = GuestPhysMemSpace::new().expect("EPT is required for guest memory");
let frame = FrameAllocOptions::new().alloc_frame().unwrap();
frame.write_bytes(0, GUEST_CODE).unwrap();
{
let guard = disable_preempt();
let mut cursor = guest_mem.cursor_mut(&guard, &(0..PAGE_SIZE)).unwrap();
cursor.map(
frame.into(),
PageProperty::new_guest(PageFlags::RWX, CachePolicy::Writeback),
);
}
// Creates the data structure that maintains the CPU state of the guest.
let mut context = GuestContext::new(
VcpuRegs::new(),
VcpuSregs::new_real_mode(),
VcpuMsrs::default(),
)
.unwrap();
loop {
// Runs the guest until an exit requires handling by the host.
let exit = match guest_mode
.execute(&mut context, &guest_mem, &DummyGuestHooks)
.unwrap()
{
GuestReturnReason::VmExit(exit) => exit,
GuestReturnReason::KernelEvent => continue,
};
// Decodes the VM exit reason to handle guest serial output or halt.
match exit.exit_reason {
reason
if reason == VmxExitReason::IO_INSTRUCTION as u32
&& exit.exit_qualification & 0x3f == 0
&& (exit.exit_qualification >> 16) as u16 == 0x3f8 =>
{
print!("{}", char::from(context.regs().rax as u8));
let regs = context.regs_mut();
regs.rip = regs.rip.checked_add(exit.instruction_len as usize).unwrap();
}
reason if reason == VmxExitReason::HLT as u32 => break,
reason => panic!("unsupported guest exit: {reason:#x}"),
}
}
println!("Guest halted; returned to host.");
}
// The Makefile assembles and links `guest_hello.S` as a flat binary at GPA 0.
const GUEST_CODE: &[u8] = include_bytes!("../guest_hello");