Skip to content

Commit

Permalink
Merge pull request #325 from hermit-os/uefi
Browse files Browse the repository at this point in the history
feat: UEFI MVP
  • Loading branch information
mkroening authored Oct 30, 2024
2 parents 1420a95 + 093974c commit 6c45371
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 30 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 @@ -36,7 +36,6 @@ goblin = { version = "0.9", default-features = false, features = ["elf64"] }

[target.'cfg(target_os = "uefi")'.dependencies]
uefi = { version = "0.33", features = ["alloc", "panic_handler", "qemu"] }
qemu-exit = "3"

[target.'cfg(target_arch = "riscv64")'.dependencies]
fdt = "0.1"
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ 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, you have to download the Open Virtual Machine Firmware (OVMF) UEFI implementation separately.
You can download prebuilt OVMF images from [rust-osdev/ovmf-prebuilt](https://github.com/rust-osdev/ovmf-prebuilt).

## Building

```bash
Expand Down Expand Up @@ -34,6 +39,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
4 changes: 0 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
#![allow(unstable_name_collisions)]
#![allow(clippy::missing_safety_doc)]

#[cfg(not(target_os = "uefi"))]
use ::log::info;
#[cfg(not(target_os = "uefi"))]
use hermit_entry::boot_info::{BootInfo, RawBootInfo};

#[macro_use]
Expand All @@ -24,12 +22,10 @@ mod os;
))]
extern crate alloc;

#[cfg(not(target_os = "uefi"))]
trait BootInfoExt {
fn write(self) -> &'static RawBootInfo;
}

#[cfg(not(target_os = "uefi"))]
impl BootInfoExt for BootInfo {
fn write(self) -> &'static RawBootInfo {
info!("boot_info = {self:#x?}");
Expand Down
121 changes: 121 additions & 0 deletions src/os/uefi/fdt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use alloc::format;
use alloc::vec::Vec;
use core::fmt::{self, Write};

use log::info;
use uefi::boot::{MemoryDescriptor, MemoryType, PAGE_SIZE};
use uefi::mem::memory_map::{MemoryMap, MemoryMapMut};
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)?;

let chosen_node = writer.begin_node("chosen")?;
writer.end_node(chosen_node)?;

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)
}

pub fn memory_map(mut self, memory_map: &mut impl MemoryMapMut) -> FdtWriterResult<Self> {
memory_map.sort();
info!("Memory map:\n{}", memory_map.display());

let entries = memory_map
.entries()
.filter(|entry| entry.ty == MemoryType::CONVENTIONAL);

for entry in entries {
let memory_node = self
.writer
.begin_node(format!("memory@{:x}", entry.phys_start).as_str())?;
self.writer.property_string("device_type", "memory")?;
self.writer.property_array_u64(
"reg",
&[entry.phys_start, entry.page_count * PAGE_SIZE as u64],
)?;
self.writer.end_node(memory_node)?;
}

Ok(self)
}
}

trait MemoryMapExt: MemoryMap {
fn display(&self) -> MemoryMapDisplay<'_, Self> {
MemoryMapDisplay { inner: self }
}
}

impl<T> MemoryMapExt for T where T: MemoryMap {}

struct MemoryMapDisplay<'a, T: ?Sized> {
inner: &'a T,
}

impl<'a, T> fmt::Display for MemoryMapDisplay<'a, T>
where
T: MemoryMap,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut has_fields = false;

for desc in self.inner.entries() {
if has_fields {
f.write_char('\n')?;
}
write!(f, "{}", desc.display())?;

has_fields = true;
}
Ok(())
}
}

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
)
}
}
105 changes: 93 additions & 12 deletions src/os/uefi/mod.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,57 @@
mod allocator;
mod console;
mod fdt;

use alloc::string::String;
use alloc::vec::Vec;
use core::ffi::c_void;
use core::mem::MaybeUninit;
use core::slice;

use align_address::Align;
use hermit_entry::boot_info::{
BootInfo, DeviceTreeAddress, HardwareInfo, PlatformInfo, SerialPortBase,
};
use hermit_entry::elf::{KernelObject, LoadedKernel};
use log::info;
use qemu_exit::QEMUExit;
use uefi::boot::MemoryType;
use sptr::Strict;
use uefi::boot::{AllocateType, MemoryType, PAGE_SIZE};
use uefi::fs::{FileSystem, Path};
use uefi::prelude::*;
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 main() -> Status {
uefi::helpers::init().unwrap();
crate::log::init();

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

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

let kernel_info = kernel.load_kernel(kernel_memory, kernel_memory.as_ptr() as u64);

let rsdp = rsdp();

drop(kernel_image);

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

allocator::exit_boot_services();
let _memory_map = unsafe { boot::exit_boot_services(MemoryType::LOADER_DATA) };
let mut memory_map = unsafe { boot::exit_boot_services(MemoryType::LOADER_DATA) };

println!("Exited boot services!");
println!("Allocations still {}!", String::from("work"));
let fdt = fdt.memory_map(&mut memory_map).unwrap().finish().unwrap();

let custom_exit_success = 3;
let qemu_exit_handle = qemu_exit::X86::new(0xf4, custom_exit_success);
qemu_exit_handle.exit_success()
unsafe { boot_kernel(kernel_info, fdt) }
}

fn read_app() -> Vec<u8> {
Expand All @@ -49,3 +69,64 @@ fn read_app() -> Vec<u8> {

data
}

pub unsafe fn boot_kernel(kernel_info: LoadedKernel, fdt: Vec<u8>) -> ! {
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: 0..0,
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) }
}

fn alloc_page_slice(size: usize) -> uefi::Result<&'static mut [MaybeUninit<u8>]> {
let size = size.align_up(PAGE_SIZE);
let ptr = boot::allocate_pages(
AllocateType::AnyPages,
MemoryType::LOADER_DATA,
size / PAGE_SIZE,
)?;
Ok(unsafe { slice::from_raw_parts_mut(ptr.cast().as_ptr(), size) })
}

/// 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() -> *const c_void {
system::with_config_table(|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
})
}
12 changes: 6 additions & 6 deletions xtask/src/ci/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ impl Qemu {
if self.build.target() == Target::X86_64Uefi {
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 @@ -198,13 +201,10 @@ impl Qemu {
}

fn memory(&self) -> usize {
let mut memory = 32usize;
if self.image == "hello_c" {
memory = memory.max(64);
}
let mut memory = 64usize;
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 6c45371

Please sign in to comment.