Skip to content

Commit

Permalink
feat: UEFI MVP
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Kröning <[email protected]>
  • Loading branch information
mkroening committed Apr 13, 2024
1 parent 774aa41 commit c7a080f
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 18 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ spinning_top = "0.3"
[target.'cfg(target_os = "uefi")'.dependencies]
uefi = { version = "0.27", features = ["alloc"] }
uefi-services = { version = "0.24", default-features = false, features = ["panic_handler", "qemu"] }
qemu-exit = "3"

[target.'cfg(target_arch = "riscv64")'.dependencies]
fdt = "0.1"
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ This project is a bootloader to run the [Hermit kernel](https://github.com/hermi

* [`rustup`](https://www.rust-lang.org/tools/install)

### UEFI Boot in x86-64 QEMU

As QEMU does not include a UEFI implementation, we have to get the Open Virtual Machine Firmware (OVMF) UEFI implementation separately.
On Linux, you can usually install it with your package manager.
Alternatively, you can run this command to get `OVMF_CODE.fd` and `OVMF_VARS.fd` from the Debian package on any system:

```bash
wget -O - http://security.debian.org/debian-security/pool/updates/main/e/edk2/ovmf_2022.11-6+deb12u1_all.deb \
| tar xvf - -O data.tar.xz \
| tar xvf - --strip-components=4 ./usr/share/OVMF/OVMF_CODE_4M.fd ./usr/share/OVMF/OVMF_VARS_4M.fd \
&& mv OVMF_CODE_4M.fd OVMF_CODE.fd \
&& mv OVMF_VARS_4M.fd OVMF_VARS.fd
```

## Building

```bash
Expand Down Expand Up @@ -34,6 +48,34 @@ qemu-system-x86_64 \
-initrd <APP>
```

#### UEFI Boot

For booting from UEFI, we have to set up an EFI system partition (ESP).
OVMF will automatically load and execute the loader if placed at `\efi\boot\bootx64.efi` in the ESP.
The Hermit application has to be next to the loader with the filename `hermit-app`.
You can set the ESP up with the following commands:

```bash
$ mkdir -p esp/efi/boot
$ cp <LOADER> esp/efi/boot/bootx64.efi
$ cp <APP> esp/efi/boot/hermit-app
```

Then, you can boot Hermit like this:

```bash
qemu-system-x86_64 \
-enable-kvm \
-cpu host \
-smp 1 \
-m 512M \
-device isa-debug-exit,iobase=0xf4,iosize=0x04 \
-display none -serial stdio \
-drive if=pflash,format=raw,readonly=on,file=OVMF_CODE.fd \
-drive if=pflash,format=raw,readonly=on,file=OVMF_VARS.fd \
-drive format=raw,file=fat:rw:esp
```

#### No KVM

If you want to emulate x86-64 instead of using KVM, omit `-enable-kvm` and set the CPU explicitly to a model of your choice, for example `-cpu Skylake-Client`.
Expand Down
4 changes: 2 additions & 2 deletions data/x86_64/hello_world
Git LFS file not shown
6 changes: 2 additions & 4 deletions src/arch/x86_64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ pub use console::Console;

#[cfg(target_os = "none")]
const KERNEL_STACK_SIZE: u64 = 32_768;
#[cfg(target_os = "none")]
const SERIAL_IO_PORT: u16 = 0x3F8;
pub const SERIAL_IO_PORT: u16 = 0x3F8;

#[cfg(target_os = "none")]
unsafe fn map_memory(address: usize, memory_size: usize) -> usize {
Expand All @@ -45,8 +44,7 @@ pub unsafe fn get_memory(memory_size: u64) -> u64 {
unsafe { map_memory(address, memory_size as usize) as u64 }
}

#[cfg(target_os = "none")]
unsafe fn enter_kernel(
pub unsafe fn enter_kernel(
stack: *mut u8,
entry: *const (),
raw_boot_info: &'static hermit_entry::boot_info::RawBootInfo,
Expand Down
36 changes: 36 additions & 0 deletions src/os/uefi/fdt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use alloc::format;
use alloc::vec::Vec;

use vm_fdt::{FdtWriter, FdtWriterNode, FdtWriterResult};

pub struct Fdt {
writer: FdtWriter,
root_node: FdtWriterNode,
}

impl Fdt {
pub fn new() -> FdtWriterResult<Self> {
let mut writer = FdtWriter::new()?;

let root_node = writer.begin_node("")?;
writer.property_string("compatible", "hermit,uefi")?;
writer.property_u32("#address-cells", 0x2)?;
writer.property_u32("#size-cells", 0x2)?;

Ok(Self { writer, root_node })
}

pub fn finish(mut self) -> FdtWriterResult<Vec<u8>> {
self.writer.end_node(self.root_node)?;

self.writer.finish()
}

pub fn rsdp(mut self, rsdp: u64) -> FdtWriterResult<Self> {
let rsdp_node = self.writer.begin_node(&format!("hermit,rsdp@{rsdp:x}"))?;
self.writer.property_array_u64("reg", &[rsdp, 1])?;
self.writer.end_node(rsdp_node)?;

Ok(self)
}
}
171 changes: 163 additions & 8 deletions src/os/uefi/mod.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,63 @@
mod console;
mod fdt;

use alloc::string::String;
use alloc::vec::Vec;
use core::ffi::c_void;
use core::mem::MaybeUninit;
use core::ops::Range;
use core::{fmt, slice};
use hermit_entry::boot_info::{
BootInfo, DeviceTreeAddress, HardwareInfo, PlatformInfo, SerialPortBase,
};

use align_address::Align;
use hermit_entry::elf::{KernelObject, LoadedKernel};
use log::info;
use qemu_exit::QEMUExit;
use sptr::Strict;
use uefi::fs::{FileSystem, Path};
use uefi::prelude::*;
use uefi::table::boot::{
AllocateType, BootServices, MemoryDescriptor, MemoryMap, MemoryType, PAGE_SIZE,
};
use uefi::table::cfg;

pub use self::console::CONSOLE;
use self::fdt::Fdt;
use crate::{arch, BootInfoExt};

// Entry Point of the Uefi Loader
#[entry]
fn loader_main(_handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
uefi_services::init(&mut system_table).unwrap();
crate::log::init();
let bs = system_table.boot_services();

let app = read_app(system_table.boot_services());
let kernel_image = read_app(bs);
let kernel = KernelObject::parse(&kernel_image).unwrap();

let string = String::from_utf8(app).unwrap();
println!("{string}");
let kernel_memory = bs.alloc_page_slice(kernel.mem_size()).unwrap();
let kernel_memory = &mut kernel_memory[..kernel.mem_size()];

let custom_exit_success = 3;
let qemu_exit_handle = qemu_exit::X86::new(0xf4, custom_exit_success);
qemu_exit_handle.exit_success()
let kernel_info = kernel.load_kernel(kernel_memory, kernel_memory.as_ptr() as u64);

let rsdp = system_table.rsdp();

drop(kernel_image);

let fdt = Fdt::new()
.unwrap()
.rsdp(u64::try_from(rsdp.expose_addr()).unwrap())
.unwrap()
.finish()
.unwrap();

let (_runtime_system_table, mut memory_map) =
system_table.exit_boot_services(MemoryType::LOADER_DATA);

let desc = largest_free_memory_region(&mut memory_map);
let phys_addr_range = desc.phys_start..desc.phys_start + (desc.page_count * PAGE_SIZE as u64);

unsafe { boot_kernel(kernel_info, fdt, phys_addr_range) }
}

fn read_app(bt: &BootServices) -> Vec<u8> {
Expand All @@ -42,3 +76,124 @@ fn read_app(bt: &BootServices) -> Vec<u8> {

data
}

pub unsafe fn boot_kernel(
kernel_info: LoadedKernel,
fdt: Vec<u8>,
phys_addr_range: Range<u64>,
) -> ! {
let LoadedKernel {
load_info,
entry_point,
} = kernel_info;

let device_tree = DeviceTreeAddress::new(u64::try_from(fdt.leak().as_ptr().addr()).unwrap());

let boot_info = BootInfo {
hardware_info: HardwareInfo {
phys_addr_range,
serial_port_base: SerialPortBase::new(arch::SERIAL_IO_PORT),
device_tree,
},
load_info,
platform_info: PlatformInfo::Fdt,
};

let stack = usize::try_from(boot_info.load_info.kernel_image_addr_range.end)
.unwrap()
.align_down(PAGE_SIZE);
let entry = sptr::from_exposed_addr(entry_point.try_into().unwrap());
let stack = sptr::from_exposed_addr_mut(stack);
let raw_boot_info = boot_info.write();

unsafe { arch::enter_kernel(stack, entry, raw_boot_info) }
}

trait BootServicesExt {
fn alloc_page_slice(&self, size: usize) -> uefi::Result<&'static mut [MaybeUninit<u8>]>;
}

impl BootServicesExt for BootServices {
fn alloc_page_slice(&self, size: usize) -> uefi::Result<&'static mut [MaybeUninit<u8>]> {
let size = size.align_up(PAGE_SIZE);
let phys_addr = self.allocate_pages(
AllocateType::AnyPages,
MemoryType::LOADER_DATA,
size / PAGE_SIZE,
)?;
let ptr = sptr::from_exposed_addr_mut(usize::try_from(phys_addr).unwrap());
Ok(unsafe { slice::from_raw_parts_mut(ptr, size) })
}
}

trait SystemTableBootExt {
/// Returns the RSDP.
///
/// This must be called before exiting boot services.
/// See [5.2.5.2. Finding the RSDP on UEFI Enabled Systems — ACPI Specification 6.5 documentation](https://uefi.org/specs/ACPI/6.5/05_ACPI_Software_Programming_Model.html#finding-the-rsdp-on-uefi-enabled-systems) for details.
fn rsdp(&self) -> *const c_void;
}

impl SystemTableBootExt for SystemTable<Boot> {
fn rsdp(&self) -> *const c_void {
let config_table = self.config_table();
let (rsdp, version) = if let Some(entry) = config_table
.iter()
.find(|entry| entry.guid == cfg::ACPI2_GUID)
{
(entry.address, 2)
} else {
let entry = config_table
.iter()
.find(|entry| entry.guid == cfg::ACPI_GUID)
.unwrap();
(entry.address, 1)
};
info!("Found ACPI {version} RSDP at {rsdp:p}");
rsdp
}
}

fn largest_free_memory_region<'a>(memory_map: &'a mut MemoryMap<'a>) -> &'a MemoryDescriptor {
memory_map.sort();

let max_desc = memory_map
.entries()
.filter(|desc| desc.ty == MemoryType::CONVENTIONAL)
.max_by_key(|desc| desc.page_count)
.unwrap();

info!("Memory map:");
for desc in memory_map.entries() {
if desc == max_desc {
info!("largest free memory region:");
}
info!("{}", desc.display());
}

max_desc
}

trait MemoryDescriptorExt {
fn display(&self) -> MemoryDescriptorDisplay<'_>;
}

impl MemoryDescriptorExt for MemoryDescriptor {
fn display(&self) -> MemoryDescriptorDisplay<'_> {
MemoryDescriptorDisplay { inner: self }
}
}

struct MemoryDescriptorDisplay<'a> {
inner: &'a MemoryDescriptor,
}

impl<'a> fmt::Display for MemoryDescriptorDisplay<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"start: {:#12x}, pages: {:#8x}, type: {:?}",
self.inner.phys_start, self.inner.page_count, self.inner.ty
)
}
}
7 changes: 5 additions & 2 deletions xtask/src/ci/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ impl Qemu {

sh.create_dir("target/esp/efi/boot")?;
sh.copy_file(self.build.dist_object(), "target/esp/efi/boot/bootx64.efi")?;
sh.write_file("target/esp/efi/boot/hermit-app", "Hello, UEFI!\n")?;
sh.copy_file(
self.build.ci_image(&self.image),
"target/esp/efi/boot/hermit-app",
)?;
}

let target = self.build.target();
Expand Down Expand Up @@ -208,7 +211,7 @@ impl Qemu {
}
match self.build.target() {
Target::X86_64Uefi => {
memory = memory.max(64);
memory = memory.max(512);
}
Target::Aarch64 => {
memory = memory.max(256);
Expand Down

0 comments on commit c7a080f

Please sign in to comment.