Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  • GuestPhysMemSpace maps guest physical addresses to OSTD-managed frames using EPT.
  • GuestContext provides access to the guest CPU state.
  • GuestMode provides an interface to run the guest using GuestContext and GuestPhysMemSpace.

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");