From c7a080f78430b5a145194adbc31512a95ace14f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kr=C3=B6ning?= Date: Fri, 12 Apr 2024 17:58:18 +0200 Subject: [PATCH] feat: UEFI MVP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Martin Kröning --- Cargo.lock | 1 - Cargo.toml | 1 - README.md | 42 ++++++++++ data/x86_64/hello_world | 4 +- src/arch/x86_64/mod.rs | 6 +- src/os/uefi/fdt.rs | 36 +++++++++ src/os/uefi/mod.rs | 171 ++++++++++++++++++++++++++++++++++++++-- xtask/src/ci/qemu.rs | 7 +- 8 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 src/os/uefi/fdt.rs diff --git a/Cargo.lock b/Cargo.lock index 35567b8c..f6bb5509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,7 +303,6 @@ dependencies = [ "multiboot", "naked-function", "one-shot-mutex", - "qemu-exit", "riscv", "sbi-rt", "spinning_top", diff --git a/Cargo.toml b/Cargo.toml index e871dc7c..661e9eab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index e28172d7..f6156e92 100644 --- a/README.md +++ b/README.md @@ -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 @@ -34,6 +48,34 @@ qemu-system-x86_64 \ -initrd ``` +#### 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 esp/efi/boot/bootx64.efi +$ cp 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`. diff --git a/data/x86_64/hello_world b/data/x86_64/hello_world index 64b5720b..9b0ea08e 100755 --- a/data/x86_64/hello_world +++ b/data/x86_64/hello_world @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ec5d6747c4ac7e1899d145a18badde148da73a42b7f9e3bfa720ba7b814f4a4 -size 870520 +oid sha256:4ec289b96f7828a68308e63b35aeaa6ad493cf193110014c490dc162e13a15a6 +size 19791960 diff --git a/src/arch/x86_64/mod.rs b/src/arch/x86_64/mod.rs index 3fece3a1..90acc585 100644 --- a/src/arch/x86_64/mod.rs +++ b/src/arch/x86_64/mod.rs @@ -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 { @@ -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, diff --git a/src/os/uefi/fdt.rs b/src/os/uefi/fdt.rs new file mode 100644 index 00000000..cfc10049 --- /dev/null +++ b/src/os/uefi/fdt.rs @@ -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 { + 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> { + self.writer.end_node(self.root_node)?; + + self.writer.finish() + } + + pub fn rsdp(mut self, rsdp: u64) -> FdtWriterResult { + 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) + } +} diff --git a/src/os/uefi/mod.rs b/src/os/uefi/mod.rs index 19d7fba4..9291a769 100644 --- a/src/os/uefi/mod.rs +++ b/src/os/uefi/mod.rs @@ -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) -> 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 { @@ -42,3 +76,124 @@ fn read_app(bt: &BootServices) -> Vec { data } + +pub unsafe fn boot_kernel( + kernel_info: LoadedKernel, + fdt: Vec, + phys_addr_range: Range, +) -> ! { + 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]>; +} + +impl BootServicesExt for BootServices { + fn alloc_page_slice(&self, size: usize) -> uefi::Result<&'static mut [MaybeUninit]> { + 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 { + 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 + ) + } +} diff --git a/xtask/src/ci/qemu.rs b/xtask/src/ci/qemu.rs index dd68d92c..b8abbe3e 100644 --- a/xtask/src/ci/qemu.rs +++ b/xtask/src/ci/qemu.rs @@ -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(); @@ -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);