diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..1afd7066 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "limine-bootloader"] + path = integration-test/limine-bootloader + url = https://github.com/limine-bootloader/limine.git + branch = v7.x-binary diff --git a/Cargo.lock b/Cargo.lock index 406ee5b1..1e0fb965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", "unicode-xid", ] @@ -97,9 +97,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.74" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index 1ce60e83..7b24a5da 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,9 @@ at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +## Developer Guide + +This is a pretty normal Cargo workspace with two crates. For the integration +test, please read the +corresponding [instructions](./integration-test/README.md). diff --git a/integration-test/.gitignore b/integration-test/.gitignore new file mode 100644 index 00000000..862abcfb --- /dev/null +++ b/integration-test/.gitignore @@ -0,0 +1 @@ +serial.txt diff --git a/integration-test/README.md b/integration-test/README.md index 6639368d..2dd8b6ee 100644 --- a/integration-test/README.md +++ b/integration-test/README.md @@ -3,21 +3,29 @@ This directory contains integration tests for the `multiboot2` and the `multiboot2-header` crate. The integration tests start a QEMU VM and do certain checks at runtime. If something fails, they instruct QEMU to exit with an error -code. All output of the VM is printed to the screen. If +code. All output of the VM is printed to the screen. -The `bins` directory contains binaries that **are** the tests. The `tests` +The `bins` directory contains Rust binaries that **are** the tests. The `tests` directory contains test definitions, run scripts, and other relevant files. The main entry to run all tests is `./run.sh` in this directory. -## TL;DR: -- `$ nix-shell --run ./run.sh` to execute the integration tests with Nix (recommended) -- `$ ./run.sh` to execute the integration tests (you have to install dependencies manually) +## TL;DR + +- `$ nix-shell --run ./run.sh` to execute the integration tests with Nix + (recommended) + - `$ nix-shell --run "integration-test/run.sh"` to run the test from the + project root +- `$ ./run.sh` to execute the integration tests (you have to get the + dependencies manually) ## Prerequisites + The tests are executed best when using [`nix`](https://nixos.org/)/`nix-shell` to get the relevant tools. Otherwise, please make sure the following packages are available: -- grub helper tools + +- grub helper tools (grub-file) - rustup +- OVMF - QEMU - xorriso diff --git a/integration-test/bins/Cargo.lock b/integration-test/bins/Cargo.lock index c888c2ec..d2a939db 100644 --- a/integration-test/bins/Cargo.lock +++ b/integration-test/bins/Cargo.lock @@ -95,15 +95,6 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "multiboot" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87ad3b7b7bcf5da525c22221e3eb3a020cd68b2d55ae62f629c15e8bc3bd56e" -dependencies = [ - "paste", -] - [[package]] name = "multiboot2" version = "0.22.2" @@ -143,7 +134,6 @@ dependencies = [ "elf_rs", "good_memory_allocator", "log", - "multiboot", "multiboot2", "multiboot2-header", "util", @@ -170,12 +160,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "proc-macro2" version = "1.0.86" diff --git a/integration-test/bins/multiboot2_chainloader/Cargo.toml b/integration-test/bins/multiboot2_chainloader/Cargo.toml index e3089f53..19e08e7f 100644 --- a/integration-test/bins/multiboot2_chainloader/Cargo.toml +++ b/integration-test/bins/multiboot2_chainloader/Cargo.toml @@ -1,6 +1,9 @@ [package] name = "multiboot2_chainloader" -description = "Integrationtest: Multiboot2 chainloader" +description = """ +Integrationtest: Multiboot2 chainloader. The loader itself loads via Multiboot1, +but the payload is loaded via Multiboot2 by the loader. +""" version = "0.1.0" edition = "2021" publish = false @@ -9,7 +12,6 @@ publish = false anyhow.workspace = true log.workspace = true good_memory_allocator.workspace = true -multiboot = "0.8" multiboot2.workspace = true multiboot2-header.workspace = true util.workspace = true diff --git a/integration-test/bins/multiboot2_chainloader/build.rs b/integration-test/bins/multiboot2_chainloader/build.rs index cdf8dfbf..cd422df7 100644 --- a/integration-test/bins/multiboot2_chainloader/build.rs +++ b/integration-test/bins/multiboot2_chainloader/build.rs @@ -1,5 +1,5 @@ fn main() { - let linker_script = "multiboot2_chainloader/link.ld"; + let linker_script = "link.ld"; println!("cargo:rerun-if-changed={linker_script}"); - println!("cargo:rustc-link-arg=-T{linker_script}"); + println!("cargo:rustc-link-arg=-Tmultiboot2_chainloader/{linker_script}"); } diff --git a/integration-test/bins/multiboot2_chainloader/link.ld b/integration-test/bins/multiboot2_chainloader/link.ld index 754a44f9..c45c6c74 100644 --- a/integration-test/bins/multiboot2_chainloader/link.ld +++ b/integration-test/bins/multiboot2_chainloader/link.ld @@ -11,9 +11,9 @@ PHDRS SECTIONS { /* Chainloader linked at 8M, payload at 16M */ - .text 8M : AT(8M) ALIGN(4K) + .text 12M : AT(12M) ALIGN(4K) { - KEEP(*(.multiboot_header)); + KEEP(*(.multiboot2_header)); *(.text .text.*) } : kernel_rx diff --git a/integration-test/bins/multiboot2_chainloader/src/loader.rs b/integration-test/bins/multiboot2_chainloader/src/loader.rs index e543867c..80b4bb10 100644 --- a/integration-test/bins/multiboot2_chainloader/src/loader.rs +++ b/integration-test/bins/multiboot2_chainloader/src/loader.rs @@ -1,19 +1,58 @@ use alloc::boxed::Box; +use alloc::vec::Vec; use elf_rs::{ElfFile, ProgramHeaderEntry, ProgramType}; +use log::{debug, info}; use multiboot2::{ - BootLoaderNameTag, CommandLineTag, MaybeDynSized, MemoryArea, MemoryAreaType, MemoryMapTag, - ModuleTag, SmbiosTag, + BootLoaderNameTag, CommandLineTag, EFIMemoryAreaType, MaybeDynSized, MemoryArea, + MemoryAreaType, MemoryMapTag, ModuleTag, SmbiosTag, }; +fn get_free_mmap_areas( + mbi: &multiboot2::BootInformation, +) -> Vec<(u64 /* start */, u64 /* size */)> { + match (mbi.memory_map_tag(), mbi.efi_memory_map_tag()) { + (Some(mmt), None) => mmt + .memory_areas() + .iter() + .filter(|ma| ma.typ() == MemoryAreaType::Available) + .map(|ma| (ma.start_address(), ma.size())) + .collect::>(), + (_, Some(mmt)) => mmt + .memory_areas() + .filter(|ma| ma.ty == EFIMemoryAreaType::CONVENTIONAL) + .map(|ma| (ma.phys_start, ma.page_count * 4096)) + .collect::>(), + _ => panic!("No usable memory map"), + } +} + +fn assert_load_segment_fits_into_memory( + start: u64, + size: u64, + free_areas: &[(u64 /* start */, u64 /* size */)], +) { + let end = start + size; + let range = free_areas + .iter() + .find(|(a_start, a_size)| start >= *a_start && end <= a_start + a_size); + if let Some(range) = range { + debug!("Can load load segment (0x{start:x?}, {size:x?}) into free memory area {range:#x?}"); + } else { + panic!("Can't load load segment (0x{start:x?}, {size:x?}) into any area!"); + } +} + /// Loads the first module into memory. Assumes that the module is a ELF file. /// The handoff is performed according to the Multiboot2 spec. -pub fn load_module(mut modules: multiboot::information::ModuleIter) -> ! { +pub fn load_module(mbi: &multiboot2::BootInformation) -> ! { + let mut modules = mbi.module_tags(); + // Load the ELF from the Multiboot1 boot module. let elf_mod = modules.next().expect("Should have payload"); let elf_bytes = unsafe { core::slice::from_raw_parts( - elf_mod.start as *const u64 as *const u8, - (elf_mod.end - elf_mod.start) as usize, + elf_mod.start_address() as *const u64 as *const u8, + elf_mod.module_size() as usize, ) }; let elf = elf_rs::Elf32::from_bytes(elf_bytes).expect("Should be valid ELF"); @@ -28,10 +67,11 @@ pub fn load_module(mut modules: multiboot::information::ModuleIter) -> ! { log::info!("Multiboot2 header:\n{hdr:#?}"); } - // Map the load segments into memory (at their corresponding link). + // Load the load segments into memory (at their corresponding link address). { - let elf = elf_rs::Elf32::from_bytes(elf_bytes).expect("Should be valid ELF"); + let free_areas = get_free_mmap_areas(mbi); elf.program_header_iter() + .inspect(|ph| assert_load_segment_fits_into_memory(ph.vaddr(), ph.memsz(), &free_areas)) .filter(|ph| ph.ph_type() == ProgramType::LOAD) .for_each(|ph| { map_memory(ph); @@ -53,9 +93,9 @@ pub fn load_module(mut modules: multiboot::information::ModuleIter) -> ! { MemoryAreaType::Reserved, )])) .add_module(ModuleTag::new( - elf_mod.start as u32, - elf_mod.end as u32, - elf_mod.string.unwrap(), + elf_mod.start_address(), + elf_mod.end_address(), + elf_mod.cmdline().unwrap(), )) // Test that we can add SmbiosTag multiple times. .add_smbios(SmbiosTag::new(1, 1, &[1, 2, 3])) @@ -66,7 +106,7 @@ pub fn load_module(mut modules: multiboot::information::ModuleIter) -> ! { log::info!( "Handing over to ELF: {}", - elf_mod.string.unwrap_or("") + elf_mod.cmdline().unwrap_or("") ); // handoff @@ -84,7 +124,7 @@ pub fn load_module(mut modules: multiboot::information::ModuleIter) -> ! { /// address space. The loader assumes that the addresses to not clash with the /// loader (or anything else). fn map_memory(ph: ProgramHeaderEntry) { - log::debug!("Mapping LOAD segment {ph:#?}"); + debug!("Mapping LOAD segment {ph:#?}"); let dest_ptr = ph.vaddr() as *mut u8; let content = ph.content().expect("Should have content"); unsafe { core::ptr::copy(content.as_ptr(), dest_ptr, content.len()) }; diff --git a/integration-test/bins/multiboot2_chainloader/src/main.rs b/integration-test/bins/multiboot2_chainloader/src/main.rs index 59c2940c..15139752 100644 --- a/integration-test/bins/multiboot2_chainloader/src/main.rs +++ b/integration-test/bins/multiboot2_chainloader/src/main.rs @@ -3,7 +3,6 @@ #![feature(error_in_core)] mod loader; -mod multiboot; extern crate alloc; @@ -12,6 +11,7 @@ extern crate util; use util::init_environment; +core::arch::global_asm!(include_str!("multiboot2_header.S"), options(att_syntax)); core::arch::global_asm!(include_str!("start.S"), options(att_syntax)); /// Entry into the Rust code from assembly using the x86 SystemV calling @@ -19,10 +19,22 @@ core::arch::global_asm!(include_str!("start.S"), options(att_syntax)); #[no_mangle] fn rust_entry(multiboot_magic: u32, multiboot_hdr: *const u32) -> ! { init_environment(); - let x = 0.12 + 0.56; - log::debug!("{x}"); log::debug!("multiboot_hdr={multiboot_hdr:x?}, multiboot_magic=0x{multiboot_magic:x?}"); - let mbi = multiboot::get_mbi(multiboot_magic, multiboot_hdr as u32).unwrap(); - let module_iter = mbi.modules().expect("Should provide modules"); - loader::load_module(module_iter); + assert_eq!(multiboot_magic, multiboot2::MAGIC); + let mbi = unsafe { multiboot2::BootInformation::load(multiboot_hdr.cast()) }.unwrap(); + + if let Some(mmap) = mbi.efi_memory_map_tag() { + log::debug!("efi memory map:",); + for desc in mmap.memory_areas() { + log::warn!( + " start=0x{:016x?} size={:016x?} type={:?}, attr={:?}", + desc.phys_start, + desc.page_count * 4096, + desc.ty, + desc.att + ); + } + } + + loader::load_module(&mbi); } diff --git a/integration-test/bins/multiboot2_chainloader/src/multiboot.rs b/integration-test/bins/multiboot2_chainloader/src/multiboot.rs deleted file mode 100644 index 0a0bd5f6..00000000 --- a/integration-test/bins/multiboot2_chainloader/src/multiboot.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Parsing the Multiboot information. Glue code for the [`multiboot`] code. - -use anyhow::anyhow; -use core::ptr::addr_of_mut; -use core::slice; -use multiboot::information::{MemoryManagement, Multiboot, PAddr, SIGNATURE_EAX}; - -static mut MEMORY_MANAGEMENT: Mem = Mem; - -/// Returns an object to access the fields of the Multiboot information -/// structure. -pub fn get_mbi<'a>(magic: u32, ptr: u32) -> anyhow::Result> { - if magic != SIGNATURE_EAX { - return Err(anyhow!("Unknown Multiboot signature {magic:x}")); - } - let mmgmt: &mut dyn MemoryManagement = unsafe { &mut *addr_of_mut!(MEMORY_MANAGEMENT) }; - unsafe { Multiboot::from_ptr(ptr as u64, mmgmt) }.ok_or(anyhow!( - "Can't read Multiboot boot information from pointer" - )) -} - -/// Glue object between the global allocator and the multiboot crate. -struct Mem; - -impl MemoryManagement for Mem { - unsafe fn paddr_to_slice(&self, addr: PAddr, size: usize) -> Option<&'static [u8]> { - let ptr = addr as *const u64 as *const u8; - Some(slice::from_raw_parts(ptr, size)) - } - - // If you only want to read fields, you can simply return `None`. - unsafe fn allocate(&mut self, _length: usize) -> Option<(PAddr, &mut [u8])> { - None - } - - unsafe fn deallocate(&mut self, addr: PAddr) { - if addr != 0 { - unimplemented!() - } - } -} diff --git a/integration-test/bins/multiboot2_chainloader/src/multiboot2_header.S b/integration-test/bins/multiboot2_chainloader/src/multiboot2_header.S new file mode 100644 index 00000000..e2913ece --- /dev/null +++ b/integration-test/bins/multiboot2_chainloader/src/multiboot2_header.S @@ -0,0 +1,25 @@ +# The assembly code uses the GNU Assembly (GAS) flavor with Intel noprefix +# syntax. + +# Symbol from main.rs +.EXTERN start + +.code32 +.align 8 +.section .multiboot2_header + + mb2_header_start: + .long 0xe85250d6 # magic number + .long 0 # architecture 0 (protected mode i386) + .long mb2_header_end - mb2_header_start # header length + # checksum + .long 0x100000000 - (0xe85250d6 + 0 + (mb2_header_end - mb2_header_start)) + + # REQUIRED END TAG + .align 8 + .Lmb2_header_tag_end_start: + .word 0 # type (16bit) + .word 0 # flags (16bit) + .long .Lmb2_header_tag_end_end - .Lmb2_header_tag_end_start # size (32bit) + .Lmb2_header_tag_end_end: + mb2_header_end: diff --git a/integration-test/bins/multiboot2_chainloader/src/start.S b/integration-test/bins/multiboot2_chainloader/src/start.S index 4ce965cc..494cb669 100644 --- a/integration-test/bins/multiboot2_chainloader/src/start.S +++ b/integration-test/bins/multiboot2_chainloader/src/start.S @@ -3,19 +3,6 @@ .code32 -.section .multiboot_header, "a", @progbits - -/* - * Multiboot v1 Header. - * Required so that we can be booted by QEMU via the "-kernel" parameter. - */ -.align 8 -.global multiboot_header -multiboot_header: - .long 0x1badb002 - .long 0x0 - .long -0x1badb002 - .section .text .global start diff --git a/integration-test/bins/multiboot2_payload/build.rs b/integration-test/bins/multiboot2_payload/build.rs index 13b5d578..3ab67218 100644 --- a/integration-test/bins/multiboot2_payload/build.rs +++ b/integration-test/bins/multiboot2_payload/build.rs @@ -1,5 +1,5 @@ fn main() { - let linker_script = "multiboot2_payload/link.ld"; + let linker_script = "link.ld"; println!("cargo:rerun-if-changed={linker_script}"); - println!("cargo:rustc-link-arg=-T{linker_script}"); + println!("cargo:rustc-link-arg=-Tmultiboot2_payload/{linker_script}"); } diff --git a/integration-test/bins/multiboot2_payload/link.ld b/integration-test/bins/multiboot2_payload/link.ld index 4891d4e6..5508227d 100644 --- a/integration-test/bins/multiboot2_payload/link.ld +++ b/integration-test/bins/multiboot2_payload/link.ld @@ -11,9 +11,9 @@ PHDRS SECTIONS { /* Chainloader linked at 8M, payload at 16M */ - .text 16M : AT(16M) ALIGN(4K) + .text 24M : AT(24M) ALIGN(4K) { - *(.multiboot2_header) + KEEP(*(.multiboot2_header)); *(.text .text.*) } : kernel_rx diff --git a/integration-test/bins/multiboot2_payload/src/main.rs b/integration-test/bins/multiboot2_payload/src/main.rs index 4599bc35..4abc2e8d 100644 --- a/integration-test/bins/multiboot2_payload/src/main.rs +++ b/integration-test/bins/multiboot2_payload/src/main.rs @@ -29,7 +29,9 @@ fn main(multiboot2_magic: u32, multiboot2_hdr: u32) -> anyhow::Result<()> { if multiboot2_magic != multiboot2::MAGIC { Err(anyhow::Error::msg("Invalid bootloader magic"))? } - log::debug!("multiboot2_hdr={multiboot2_hdr:x?}, multiboot2_magic=0x{multiboot2_magic:x?}"); + log::debug!( + "multiboot2_hdr=0x{multiboot2_hdr:08x?}, multiboot2_magic=0x{multiboot2_magic:08x?}" + ); let mbi_ptr = (multiboot2_hdr as *const u8).cast(); let mbi = unsafe { BootInformation::load(mbi_ptr) }.map_err(anyhow::Error::msg)?; diff --git a/integration-test/bins/multiboot2_payload/src/multiboot2_header.S b/integration-test/bins/multiboot2_payload/src/multiboot2_header.S index 84ac5773..efd90649 100644 --- a/integration-test/bins/multiboot2_payload/src/multiboot2_header.S +++ b/integration-test/bins/multiboot2_payload/src/multiboot2_header.S @@ -21,7 +21,7 @@ .align 8 .Lmb2_header_tag_information_request_start: .word 1 # type (16bit) - .word 0 # flags (16bit) + .word 1 # flags (16bit) .long .Lmb2_header_tag_information_request_end - .Lmb2_header_tag_information_request_start # size (32bit) .long 1 .long 2 diff --git a/integration-test/bins/multiboot2_payload/src/verify/grub.rs b/integration-test/bins/multiboot2_payload/src/verify/bootloader.rs similarity index 84% rename from integration-test/bins/multiboot2_payload/src/verify/grub.rs rename to integration-test/bins/multiboot2_payload/src/verify/bootloader.rs index 9954e835..32400679 100644 --- a/integration-test/bins/multiboot2_payload/src/verify/grub.rs +++ b/integration-test/bins/multiboot2_payload/src/verify/bootloader.rs @@ -23,8 +23,8 @@ fn basic_sanity_checks(mbi: &BootInformation) -> anyhow::Result<()> { .map_err(anyhow::Error::msg)? .cmdline() .map_err(anyhow::Error::msg)?; - assert!(bootloader_name.starts_with("GRUB 2.")); - assert_eq!(cmdline, "some commandline arguments"); + assert!(bootloader_name.starts_with("GRUB 2.") || bootloader_name.starts_with("Limine")); + assert_eq!(cmdline, "some kernel cmdline"); Ok(()) } diff --git a/integration-test/bins/multiboot2_payload/src/verify/mod.rs b/integration-test/bins/multiboot2_payload/src/verify/mod.rs index aa196487..9baffad6 100644 --- a/integration-test/bins/multiboot2_payload/src/verify/mod.rs +++ b/integration-test/bins/multiboot2_payload/src/verify/mod.rs @@ -1,5 +1,5 @@ +mod bootloader; mod chainloader; -mod grub; use alloc::format; use alloc::vec::Vec; @@ -17,8 +17,11 @@ pub fn run(mbi: &BootInformation) -> anyhow::Result<()> { .map_err(anyhow::Error::msg)?; if bootloader.to_lowercase().contains("grub") { - log::info!("loaded by grub"); - grub::run(mbi)?; + log::info!("loaded by GRUB"); + bootloader::run(mbi)?; + } else if bootloader.to_lowercase().contains("limine") { + log::info!("loaded by Limine"); + bootloader::run(mbi)?; } else { log::info!("loaded by chainloader"); chainloader::run(mbi)?; @@ -78,6 +81,15 @@ pub(self) fn print_module_info(mbi: &BootInformation) -> anyhow::Result<()> { } let module = modules.first().unwrap(); let module_cmdline = module.cmdline().map_err(anyhow::Error::msg)?; + + let allowed_module_cmdlines = ["Limine bootloader config", "multiboot2_payload"]; + assert!( + allowed_module_cmdlines + .iter() + .any(|&str| module_cmdline == str), + "The module cmdline must be one of {allowed_module_cmdlines:?} but is {module_cmdline}" + ); + println!("Modules:"); println!( " 0x{:010x} - 0x{:010x} ({} B, cmdline='{}')", @@ -86,13 +98,13 @@ pub(self) fn print_module_info(mbi: &BootInformation) -> anyhow::Result<()> { module.module_size(), module_cmdline ); - println!(" grub cfg passed as boot module:"); + println!(" bootloader cfg passed as boot module:"); let grup_cfg_ptr = module.start_address() as *const u32 as *const u8; let grub_cfg = unsafe { core::slice::from_raw_parts(grup_cfg_ptr, module.module_size() as usize) }; - // In the GRUB bootflow case, we pass the config as module with it. This is - // not done for the chainloaded case. + // In the Limine bootflow case, we pass the config as module with it. This + // is not done for the chainloaded case. if let Ok(str) = core::str::from_utf8(grub_cfg) { println!("=== file begin ==="); for line in str.lines() { diff --git a/integration-test/bins/util/src/allocator.rs b/integration-test/bins/util/src/allocator.rs index 017f3c2e..4caea3eb 100644 --- a/integration-test/bins/util/src/allocator.rs +++ b/integration-test/bins/util/src/allocator.rs @@ -1,10 +1,10 @@ use good_memory_allocator::SpinLockedAllocator; -#[repr(align(0x4000))] -struct Align16K(T); +#[repr(align(0x1000))] +struct PageAlign(T); -/// 16 KiB naturally aligned backing storage for heap. -static mut HEAP: Align16K<[u8; 0x4000]> = Align16K([0; 0x4000]); +/// 16 KiB page-aligned backing storage for heap. +static mut HEAP: PageAlign<[u8; 0x4000]> = PageAlign([0; 0x4000]); #[global_allocator] static ALLOCATOR: SpinLockedAllocator = SpinLockedAllocator::empty(); diff --git a/integration-test/limine-bootloader b/integration-test/limine-bootloader new file mode 160000 index 00000000..24336d8f --- /dev/null +++ b/integration-test/limine-bootloader @@ -0,0 +1 @@ +Subproject commit 24336d8f369f6d3127fb77007c078bea4527f177 diff --git a/integration-test/run.sh b/integration-test/run.sh index 9f4fd36c..522a060f 100755 --- a/integration-test/run.sh +++ b/integration-test/run.sh @@ -7,30 +7,156 @@ IFS=$'\n\t' DIR=$(dirname "$(realpath "$0")") cd "$DIR" || exit +BINS_DIR=bins/target/x86-unknown-none/release +ANSI_HIGHLIGHT="\e[1;32m" # green + bold +ANSI_HIGHLIGHT_ERROR="\e[1;31m" # red + bold +ANSI_RESET="\e[0m" +QEMU_ARGS_BASE=( + -machine q35,accel=kvm + -m 128m # for OVMF, we need more than just 24 + -debugcon stdio + -serial file:serial.txt + -no-reboot + -device isa-debug-exit,iobase=0xf4,iosize=0x04 + -display none `# relevant for the CI` +) + function fn_main() { + git submodule update --init + fn_build_limine_hosttool fn_build_rust_bins - fn_multiboot2_integrationtest - fn_multiboot2_header_integrationtest + + fn_test_payload + fn_test_loader +} + +function fn_build_limine_hosttool() { + cd limine-bootloader + make + test -f ./limine + file --brief ./limine | grep -q "ELF 64-bit LSB executable" + cd - } function fn_build_rust_bins() { cd "bins" cargo --version cargo build --release --verbose - cd "$DIR" + cd - + + echo "Verifying multiboot2_chainloader ..." + test -f $BINS_DIR/multiboot2_chainloader + file --brief $BINS_DIR/multiboot2_chainloader | grep -q "ELF 32-bit LSB executable" + grub-file --is-x86-multiboot2 $BINS_DIR/multiboot2_chainloader + + echo "Verifying multiboot2_payload ..." + test -f $BINS_DIR/multiboot2_payload + file --brief $BINS_DIR/multiboot2_payload | grep -q "ELF 32-bit LSB executable" + grub-file --is-x86-multiboot2 $BINS_DIR/multiboot2_payload +} + +function fn_prepare_test_vol() { + TEST_VOL="$TEST_DIR/.vol" + rm -rf $TEST_VOL + mkdir -p $TEST_VOL + cp $TEST_DIR/limine.cfg $TEST_VOL + + # copy limine artifacts + mkdir -p $TEST_VOL/limine + cp limine-bootloader/limine-bios-cd.bin $TEST_VOL/limine + cp limine-bootloader/limine-bios.sys $TEST_VOL/limine + cp limine-bootloader/limine-uefi-cd.bin $TEST_VOL/limine + + mkdir -p $TEST_VOL/EFI/BOOT + cp limine-bootloader/BOOTX64.EFI $TEST_VOL/EFI_BOOT } -function fn_multiboot2_integrationtest() { - cd tests/multiboot2 - ./build_img.sh - ./run_qemu.sh - cd "$DIR" + + +# Builds a hybrid-bootable image using Limine as bootloader. Expects that +# all relevant files are in the directory describing the root volume. +function fn_build_limine_iso() { + xorriso -as mkisofs -b limine/limine-bios-cd.bin \ + -no-emul-boot -boot-load-size 4 -boot-info-table \ + --efi-boot limine/limine-uefi-cd.bin \ + -efi-boot-part --efi-boot-image --protective-msdos-label \ + $TEST_VOL -o $TEST_DIR/image.iso 2>/dev/null + + ./limine-bootloader/limine bios-install $TEST_DIR/image.iso 2>/dev/null } -function fn_multiboot2_header_integrationtest() { - cd tests/multiboot2-header - ./run_qemu.sh - cd "$DIR" +function fn_run_qemu() { + set +e + + # As QEMU can't print serial and debugcon to stdout simultaneously, I + # add a background task watching serial.txt + rm serial.txt + touch serial.txt + tail -f serial.txt & + + qemu-system-x86_64 "${QEMU_ARGS[@]}" + EXIT_CODE=$? + # Custom exit code used by the integration test to report success. + QEMU_EXIT_SUCCESS=73 + + set -e + + echo "#######################################" + if [[ $EXIT_CODE -eq $QEMU_EXIT_SUCCESS ]]; then + echo -e "${ANSI_HIGHLIGHT}SUCCESS${ANSI_RESET}" + echo # newline + else + echo -e "${ANSI_HIGHLIGHT_ERROR}FAILED - Integration Test 'multiboot2-header'${ANSI_RESET}" + exit "$EXIT_CODE" + fi +} + +function fn_run_test_bios() { + local ISO=$1 + local QEMU_ARGS=("${QEMU_ARGS_BASE[@]}") # copy array + local QEMU_ARGS+=( + -cdrom "$ISO" + ) + echo -e "Running '${ANSI_HIGHLIGHT}$ISO${ANSI_RESET}' in QEMU (with legacy BIOS firmware)" + fn_run_qemu +} + +function fn_run_test_uefi() { + local ISO=$1 + local QEMU_ARGS=("${QEMU_ARGS_BASE[@]}") # copy array + local QEMU_ARGS+=( + # Usually, this comes from the Nix shell. + -bios $OVMF + -cdrom "$ISO" + ) + echo -e "Running '${ANSI_HIGHLIGHT}$ISO${ANSI_RESET}' in QEMU (with UEFI/OVMF firmware)" + fn_run_qemu $QEMU_ARGS +} + +function fn_test_payload() { + local TEST_DIR=tests/01-boot-payload + fn_prepare_test_vol + + cp $BINS_DIR/multiboot2_payload $TEST_VOL/kernel + + fn_build_limine_iso + + fn_run_test_bios $TEST_DIR/image.iso + fn_run_test_uefi $TEST_DIR/image.iso +} + +# Tests the loader by chainloading the Multiboot2 payload. +function fn_test_loader() { + local TEST_DIR=tests/02-boot-loader-and-chainload + fn_prepare_test_vol + + cp $BINS_DIR/multiboot2_chainloader $TEST_VOL/kernel + cp $BINS_DIR/multiboot2_payload $TEST_VOL/payload + + fn_build_limine_iso + + fn_run_test_bios $TEST_DIR/image.iso + fn_run_test_uefi $TEST_DIR/image.iso } fn_main diff --git a/integration-test/tests/.gitignore b/integration-test/tests/.gitignore new file mode 100644 index 00000000..f4f0ceed --- /dev/null +++ b/integration-test/tests/.gitignore @@ -0,0 +1,2 @@ +.vol +image.iso diff --git a/integration-test/tests/01-boot-payload/limine.cfg b/integration-test/tests/01-boot-payload/limine.cfg new file mode 100644 index 00000000..5603f190 --- /dev/null +++ b/integration-test/tests/01-boot-payload/limine.cfg @@ -0,0 +1,11 @@ +TIMEOUT=0 +SERIAL=yes +VERBOSE=yes +INTERFACE_BRANDING=integration-test + +:integration-test +PROTOCOL=multiboot2 +KERNEL_PATH=boot:///kernel +KERNEL_CMDLINE=some kernel cmdline +MODULE_PATH=boot:///limine.cfg +MODULE_STRING=Limine bootloader config diff --git a/integration-test/tests/02-boot-loader-and-chainload/limine.cfg b/integration-test/tests/02-boot-loader-and-chainload/limine.cfg new file mode 100644 index 00000000..b2e90d94 --- /dev/null +++ b/integration-test/tests/02-boot-loader-and-chainload/limine.cfg @@ -0,0 +1,11 @@ +TIMEOUT=0 +SERIAL=yes +VERBOSE=yes +INTERFACE_BRANDING=integration-test + +:integration-test +PROTOCOL=multiboot2 +KERNEL_PATH=boot:///kernel +KERNEL_CMDLINE=some kernel cmdline +MODULE_PATH=boot:///payload +MODULE_STRING=multiboot2_payload diff --git a/integration-test/tests/multiboot2-header/README.md b/integration-test/tests/multiboot2-header/README.md deleted file mode 100644 index 60d63f47..00000000 --- a/integration-test/tests/multiboot2-header/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# multiboot2-header - Integration Test - -This integration test loads the `multiboot2_chainloader` binary as Multiboot1 -payload using QEMU. The `multiboot2_payload` binary is passed as boot module. -The `multiboot2_chainloader` behaves as bootloader and eventually loads -`multiboot2_payload` into the memory. `multiboot2_payload` figures out during -runtime whether it was loaded by GRUB or by the chainloader. diff --git a/integration-test/tests/multiboot2-header/run_qemu.sh b/integration-test/tests/multiboot2-header/run_qemu.sh deleted file mode 100755 index d7bbfbc3..00000000 --- a/integration-test/tests/multiboot2-header/run_qemu.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -# This script starts a bootable image in QEMU using legacy BIOS boot. - -# http://redsymbol.net/articles/unofficial-bash-strict-mode/ -set -euo pipefail -IFS=$'\n\t' - -DIR=$(dirname "$(realpath "$0")") -cd "$DIR" || exit - -BINS_DIR="../../bins/target/x86-unknown-none/release" -CHAINLOADER="$BINS_DIR/multiboot2_chainloader" -PAYLOAD="$BINS_DIR/multiboot2_payload" -# add "-d int \" to debug CPU exceptions -# "-display none" is necessary for the CI but locally the display and the -# combat monitor are really helpful - -set +e -qemu-system-x86_64 \ - -kernel "$CHAINLOADER" \ - -append "chainloader" \ - -initrd "$PAYLOAD multiboot2 payload" \ - -m 24m \ - -debugcon stdio \ - -no-reboot \ - -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ - -display none `# relevant for the CI` - -EXIT_CODE=$? -# Custom exit code used by the integration test to report success. -QEMU_EXIT_SUCCESS=73 - -echo "#######################################" -if [[ $EXIT_CODE -eq $QEMU_EXIT_SUCCESS ]]; then - echo "SUCCESS - Integration Test 'multiboot2-header'" - exit 0 -else - echo "FAILED - Integration Test 'multiboot2-header'" - exit "$EXIT_CODE" -fi diff --git a/integration-test/tests/multiboot2/.gitignore b/integration-test/tests/multiboot2/.gitignore deleted file mode 100644 index 526859d0..00000000 --- a/integration-test/tests/multiboot2/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.vol -grub_boot.img diff --git a/integration-test/tests/multiboot2/README.md b/integration-test/tests/multiboot2/README.md deleted file mode 100644 index 9ebf2a7e..00000000 --- a/integration-test/tests/multiboot2/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# multiboot2 - Integration Test - -This integration test uses GRUB as Multiboot2 bootloader and loads the -`multiboot2_payload` binary into the memory. The MBI is read at runtime and -certain checks are performed. diff --git a/integration-test/tests/multiboot2/build_img.sh b/integration-test/tests/multiboot2/build_img.sh deleted file mode 100755 index 026e80cf..00000000 --- a/integration-test/tests/multiboot2/build_img.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -# This script builds a bootable image. It bundles the test binary into a GRUB -# installation. The GRUB installation is configured to chainload the binary -# via Multiboot2. - -# http://redsymbol.net/articles/unofficial-bash-strict-mode/ -set -euo pipefail -IFS=$'\n\t' - -DIR=$(dirname "$(realpath "$0")") -cd "$DIR" || exit - -MULTIBOOT2_PAYLOAD_DIR="../../bins" -MULTIBOOT2_PAYLOAD_PATH="$MULTIBOOT2_PAYLOAD_DIR/target/x86-unknown-none/release/multiboot2_payload" - -echo "Verifying that the binary is a multiboot2 binary..." -grub-file --is-x86-multiboot2 "$MULTIBOOT2_PAYLOAD_PATH" - -# Delete previous state. -rm -rf .vol - -mkdir -p .vol/boot/grub -cp grub.cfg .vol/boot/grub -cp "$MULTIBOOT2_PAYLOAD_PATH" .vol - -# Create a GRUB image with the files in ".vol" being embedded. -echo "Creating bootable image..." -grub-mkrescue -o "grub_boot.img" ".vol" diff --git a/integration-test/tests/multiboot2/grub.cfg b/integration-test/tests/multiboot2/grub.cfg deleted file mode 100644 index ab810423..00000000 --- a/integration-test/tests/multiboot2/grub.cfg +++ /dev/null @@ -1,13 +0,0 @@ -# GRUB 2 configuration that boots the integration test binary via Multiboot2. - -set timeout=0 -set default=0 -# set debug=all - -menuentry "Integration Test" { - # The leading slash is very important. - multiboot2 /multiboot2_payload some commandline arguments - # Pass some module + command line. - module2 /boot/grub/grub.cfg grub-config - boot -} diff --git a/integration-test/tests/multiboot2/run_qemu.sh b/integration-test/tests/multiboot2/run_qemu.sh deleted file mode 100755 index 099308b1..00000000 --- a/integration-test/tests/multiboot2/run_qemu.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -# This script starts a bootable image in QEMU using legacy BIOS boot. - -# http://redsymbol.net/articles/unofficial-bash-strict-mode/ -set -euo pipefail -IFS=$'\n\t' - -DIR=$(dirname "$(realpath "$0")") -cd "$DIR" || exit - -BOOT_IMAGE="grub_boot.img" - -# add "-d int \" to debug CPU exceptions -# "-display none" is necessary for the CI but locally the display and the -# combat monitor are really helpful - -set +e -qemu-system-x86_64 \ - -boot d \ - -cdrom "$BOOT_IMAGE" \ - -m 24m \ - -debugcon stdio \ - -no-reboot \ - -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ - -display none `# relevant for the CI` - -EXIT_CODE=$? -# Custom exit code used by the integration test to report success. -QEMU_EXIT_SUCCESS=73 - - -echo "#######################################" -if [[ $EXIT_CODE -eq $QEMU_EXIT_SUCCESS ]]; then - echo "SUCCESS - Integration Test 'multiboot2'" - exit 0 -else - echo "FAILED - Integration Test 'multiboot2'" - exit "$EXIT_CODE" -fi diff --git a/shell.nix b/shell.nix index 178c58af..e08f4e09 100644 --- a/shell.nix +++ b/shell.nix @@ -10,7 +10,7 @@ pkgs.mkShell rec { niv # integration test - grub2 + grub2 # for grub-file qemu xorriso @@ -23,4 +23,6 @@ pkgs.mkShell rec { # For better reproducibility inside the Nix shell, we override this channel # with the pinned nixpkgs version. NIX_PATH = "nixpkgs=${sources.nixpkgs}"; + + OVMF = "${pkgs.OVMF.fd}/FV/OVMF.fd"; }