Skip to content

Commit

Permalink
feat: UEFI MVP
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Simon <[email protected]>
Signed-off-by: Martin Kröning <[email protected]>
  • Loading branch information
mkroening and sarahspberrypi committed Apr 10, 2024
1 parent 59d2d42 commit b3a072e
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 17 deletions.
5 changes: 1 addition & 4 deletions Cargo.lock

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

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ edition = "2021"
[dependencies]
align-address = "0.1"
cfg-if = "1"
hermit-entry = { version = "0.9", features = ["loader"] }
hermit-entry = { version = "0.10", features = ["loader"] }
log = "0.4"
one-shot-mutex = "0.1"
sptr = "0.3"
Expand Down 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: 3 additions & 1 deletion src/arch/x86_64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ cfg_if::cfg_if! {
} else if #[cfg(target_os = "none")] {
mod multiboot;
pub use self::multiboot::*;
} else if #[cfg(target_os = "uefi")] {
mod uefi;
pub use self::uefi::*;
}
}

Expand All @@ -18,7 +21,6 @@ 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;

#[cfg(target_os = "none")]
Expand Down
75 changes: 75 additions & 0 deletions src/arch/x86_64/uefi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use core::arch::asm;
use core::ops::Range;

use align_address::Align;
use alloc::vec::Vec;
use hermit_entry::boot_info::{BootInfo, DeviceTreeAddress, HardwareInfo, PlatformInfo, RawBootInfo, SerialPortBase};
use hermit_entry::elf::LoadedKernel;
use hermit_entry::Entry;
use log::info;
use sptr::Strict;
use uefi::table::boot::PAGE_SIZE;

use crate::arch::x86_64::SERIAL_IO_PORT;

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 stack_address = usize::try_from(load_info.kernel_image_addr_range.end)
.unwrap()
.align_down(PAGE_SIZE);

take_static::take_static! {
static RAW_BOOT_INFO: Option<RawBootInfo> = None;
}

let raw_boot_info = RAW_BOOT_INFO.take().unwrap();

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

info!("boot_info = {boot_info:#?}");
let boot_info_ptr = raw_boot_info.insert(RawBootInfo::from(boot_info));
info!("boot_info at {boot_info_ptr:p}");

info!("Jumping to Hermit Application Entry Point at {entry_point:#x}");

#[allow(dead_code)]
const ENTRY_TYPE_CHECK: Entry = {
unsafe extern "C" fn entry_signature(
_raw_boot_info: &'static RawBootInfo,
_cpu_id: u32,
) -> ! {
unimplemented!()
}
entry_signature
};

unsafe {
asm!(
"mov rsp, {stack_address}",
"jmp {entry}",
stack_address = in(reg) stack_address,
entry = in(reg) entry_point,
in("rdi") boot_info_ptr,
in("rsi") 0,
options(noreturn)
)
}
}
35 changes: 35 additions & 0 deletions src/os/uefi/fdt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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)
}
}
133 changes: 125 additions & 8 deletions src/os/uefi/mod.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,59 @@
mod console;
mod fdt;

use alloc::string::String;
use alloc::vec::Vec;
use core::ffi::c_void;
use core::mem::MaybeUninit;
use core::{fmt, slice};
use sptr::Strict;

use align_address::Align;
use hermit_entry::elf::KernelObject;
use log::info;
use qemu_exit::QEMUExit;
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;

// 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 { arch::boot_kernel(kernel_info, fdt, phys_addr_range) }
}

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

data
}

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

info!("Memory map:");
for desc in memory_map.entries() {
info!("{}", desc.display());
}

let desc = memory_map
.entries()
.filter(|desc| desc.ty == MemoryType::CONVENTIONAL)
.max_by_key(|desc| desc.page_count)
.unwrap();
info!("Chosen memory region:");
info!("{}", desc.display());
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
)
}
}
Loading

0 comments on commit b3a072e

Please sign in to comment.