Skip to content

Commit

Permalink
add inital UEFI boot support
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahspberrypi committed Jan 19, 2024
1 parent 3230e2d commit c27ada5
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 14 deletions.
57 changes: 55 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,52 @@ This project is a loader to run the [Hermit kernel](https://github.com/hermitcor
* [`rustup`](https://www.rust-lang.org/tools/install)
* [NASM](https://nasm.us/) (only for x86_64)

## Building
### UEFI Boot (x86_64 only)

QEMU does not support UEFI as firmware interface per se. You'll need to install [OVMF](https://github.com/tianocore/tianocore.github.io/wiki/OVMF-FAQ#what-is-open-virtual-machine-firmware-ovmf) (an open source implementation of UEFI for virtual machines).
You can do so, e.g., via terminal:
```bash
$ sudo apt install ovmf
```
Then, you can modify your QEMU command via the `-bios` flag accordingly later on.

## Building (BIOS Boot)

```bash
$ cargo xtask build --target <TARGET> --release
```

With `<TARGET>` being either `x86_64`, `x86_64-uefi`, or `aarch64`.
With `<TARGET>` being either `x86_64`, or `aarch64`.

Afterward, the loader is located at `target/<TARGET>/release/hermit-loader`.

## Building (UEFI Boot, x86_64 only)

Currently, the loader requires a compiled binary of your Hermit application in the directory `src/arch/x86_64/`.
Then, the name of your application has to be specified in the `find_kernel` function (in `src/arch/x86_64/mod.rs`) like so:
```
#[cfg(target_os = "uefi")]
pub unsafe fn find_kernel() -> &'static [u8] {
include_bytes!("APPLICATION_NAME")
}
```
whereas `APPLICATION_NAME` needs to be replaced with the actual name of your application.

Afterwards, you can build the loader:
```bash
$ cargo xtask build --target <TARGET> --release
```

With `<TARGET>` being `x86_64-uefi`.

Finally, the loader is located at `target/<TARGET>/release/BootX64.efi`.

## Running

### x86-64

#### BIOS Boot

On x86-64 Linux with KVM, you can boot Hermit like this:

```
Expand All @@ -35,6 +67,27 @@ $ qemu-system-x86_64 \
-initrd <APP>
```

#### UEFI Boot

The UEFI Specification requires the loader to be located in a directory structure like so: `/bootloader/efi/boot/`.
A quick way to do this is via the following terminal commands:
```bash
$ mkdir -p bootloader/efi/boot

$ cp -f target/x86_64-uefi/debug/BootX64.efi bootloader/efi/boot/
```

Then, you can boot Hermit like this:
```
qemu-system-x86_64 -nographic -cpu qemu64,apic,fsgsbase,fxsr,rdrand,rdtscp,xsave,xsaveopt \
-smp <NUMBER OF CORES> \
-m 512M \
-device isa-debug-exit,iobase=0xf4,iosize=0x04 \
--bios OVMF.fd \
-drive format=raw,file=fat:rw:bootloader,media=disk \
-d cpu_reset
```

#### 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
77 changes: 70 additions & 7 deletions src/arch/x86_64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod physicalmem;
use core::arch::asm;
#[cfg(all(target_os = "none", not(feature = "fc")))]
use core::mem;
#[cfg(target_os = "none")]
#[cfg(any(target_os = "none", target_os = "uefi"))]
use core::ptr::write_bytes;
#[cfg(target_os = "none")]
use core::slice;
Expand All @@ -25,6 +25,12 @@ use multiboot::information::MemoryManagement;
#[cfg(all(target_os = "none", not(feature = "fc")))]
use multiboot::information::{Multiboot, PAddr};
use uart_16550::SerialPort;
#[cfg(target_os = "uefi")]
use uefi::prelude::*;
#[cfg(target_os = "uefi")]
use uefi::table::{boot::*, cfg};
#[cfg(target_os = "uefi")]
use x86_64::align_up;
use x86_64::structures::paging::{PageSize, PageTableFlags, Size2MiB, Size4KiB};

use self::physicalmem::PhysAlloc;
Expand Down Expand Up @@ -80,19 +86,76 @@ pub fn output_message_byte(byte: u8) {
unsafe { COM1.send(byte) };
}

// Right now, the kernel binary has to be hardcoded into the loader.
// The binary has to be in the same directory (or its whereabouts have to specified in the "include_bytes!" statement).
#[cfg(target_os = "uefi")]
pub unsafe fn find_kernel() -> &'static [u8] {
&[1, 2, 3]
include_bytes!("hermit-rs-template")
}

/// This is the actual boot function.
/// The bootstack is cleared and provided/calculated BOOT_INFO is written into before the actual call to the assembly code to jump into the kernel.
#[cfg(target_os = "uefi")]
pub unsafe fn boot_kernel(
_elf_address: Option<u64>,
_virtual_address: u64,
_mem_size: u64,
_entry_point: u64,
rsdp_addr: u64,
kernel_addr: u64,
filesize: usize,
kernel_info: LoadedKernel,
runtime_system_table: uefi::table::SystemTable<uefi::table::Runtime>,
start_address: usize,
end_address: usize,
) -> ! {
loop {}
let LoadedKernel {
load_info,
entry_point,
} = kernel_info;

let kernel_end = kernel_addr + filesize as u64;
info!("kernel_end at: {:#x}", kernel_end);
// determine boot stack address
let mut new_stack = align_up(kernel_end, Size4KiB::SIZE);

// clear stack
write_bytes(
new_stack as *mut u8,
0,
KERNEL_STACK_SIZE.try_into().unwrap(),
);

static mut BOOT_INFO: Option<RawBootInfo> = None;

// Write previously gathered information relevant for booting into the BOOT_INFO struct
BOOT_INFO = {
let boot_info = BootInfo {
hardware_info: HardwareInfo {
phys_addr_range: start_address as u64..end_address as u64,
serial_port_base: SerialPortBase::new(SERIAL_IO_PORT),
device_tree: None,
},
load_info,
platform_info: PlatformInfo::Uefi { rsdp_addr },
};
info!("Boot Info (Loader): {:#x?}", boot_info);
Some(RawBootInfo::from(boot_info))
};

info!("BootInfo located at {:#x}", &BOOT_INFO as *const _ as u64);

// Jump to the kernel entry point and provide the BOOT_INFO as well.
info!(
"Jumping to HermitCore Application Entry Point at {:#x}",
entry_point
);

asm!(
"mov rsp, {stack_address}",
"jmp {entry}",
stack_address = in(reg) new_stack as u64,
entry = in(reg) entry_point,
in("rdi") BOOT_INFO.as_ref().unwrap(),
in("rsi") 0,
options(noreturn)
)
}

#[cfg(all(target_os = "none", feature = "fc"))]
Expand Down
101 changes: 96 additions & 5 deletions src/uefi.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,100 @@
use uefi::prelude::*;
use crate::arch;
use crate::console;
use core::{cmp, fmt::Write, mem, slice};
#[allow(unused_imports)]
use hermit_entry::elf::KernelObject;
use log::info;
use uefi::{
prelude::*,
table::{boot::*, cfg},
};

// Entry Point of the Uefi Loader
/// Entry Point of the UEFI Loader
/// This function gets a so-called "EFI System Table" (see UEFI Specification, Section 4: EFI System Table) from the Firmware Interface.
/// Here, the RSDP (for BOOT_INFO) and the kernel are located and the kernel is parsed and loaded into memory.
/// After that, free physical memory for the kernel to use is looked for and saved for BOOT_INFO as well.
/// After that, the architecture specific boot function is called.
#[entry]
fn loader_main(_handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
uefi_services::init(&mut system_table).unwrap();
unsafe fn loader_main(_handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
// initialize Hermits Log functionality
arch::message_output_init();
crate::log::init();

Status::SUCCESS
info!("Hello World from UEFI boot!");
// look for the rsdp in the EFI system table before calling exit boot services (see UEFI specification for more)
let rsdp_addr = {
// returns an iterator to the config table entries which in turn point to other system-specific tables
let mut cfg_entries = system_table.config_table().iter();
// look for ACPI2 RSDP first
let acpi2_addr = cfg_entries.find(|entry| entry.guid == cfg::ACPI2_GUID);
// takes the value of either acpi2_addr or (if it does not exist) ACPI1 address
let rsdp = acpi2_addr.or(cfg_entries.find(|entry| entry.guid == cfg::ACPI_GUID));
rsdp.map(|entry| entry.address as u64)
.expect("no RSDP address found")
};
info!("RSDP found at {rsdp_addr:#x}");
info!("Locating kernel");

let kernel = KernelObject::parse(arch::find_kernel()).unwrap();
info!("Kernel parsed!");
let filesize = kernel.mem_size();
info!("Kernelsize: {:#?}", filesize);
let kernel_addr = system_table
.boot_services()
.allocate_pages(
AllocateType::AnyPages,
MemoryType::LOADER_DATA,
(filesize / 4096) + 1,
)
.unwrap();
let kernel_addr = kernel.start_addr().unwrap_or(kernel_addr);
info!("Kernel located at {:#x}", kernel_addr);
let memory = slice::from_raw_parts_mut(kernel_addr as *mut mem::MaybeUninit<u8>, filesize);

let kernel_info = kernel.load_kernel(memory, memory.as_ptr() as u64);
info!("Kernel loaded into memory");

// exit boot services for getting a runtime view of the system table and an iterator to the UEFI memory map
let (runtime_system_table, mut memory_map) = system_table.exit_boot_services();

memory_map.sort();
let mut entries = memory_map.entries();
let mut clone = entries.clone();
let mut size = 0;
let mut max_size = 0;
for index in entries {
if index.ty.eq(&uefi::table::boot::MemoryType(7)) {
size = index.page_count;
if size > max_size {
max_size = size;
}
}
}

let start_address = clone
.find(|&&x| x.page_count == max_size)
.unwrap()
.phys_start as usize;
let end_address = (start_address + (max_size as usize * 0x1000 as usize) - 1) as usize;

info!("Kernelmemory: start: {start_address:#x?}, end: {end_address:#x?}");

// Jump into actual booting routine
arch::boot_kernel(
rsdp_addr,
kernel_addr,
filesize,
kernel_info,
runtime_system_table,
start_address,
end_address,
)
}

#[panic_handler]
fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
// We can't use `println!` or related macros, because `_print` unwraps a result and might panic again
writeln!(unsafe { &mut console::CONSOLE }, "[LOADER] {info}").ok();

loop {}
}

0 comments on commit c27ada5

Please sign in to comment.