diff --git a/.github/workflows/test_asterinas.yml b/.github/workflows/test_asterinas.yml index e9a34c4a..8d38ae48 100644 --- a/.github/workflows/test_asterinas.yml +++ b/.github/workflows/test_asterinas.yml @@ -99,10 +99,10 @@ jobs: if: ${{ matrix.test_id == 'boot_test_linux_legacy32' }} run: make run AUTO_TEST=boot ENABLE_KVM=1 BOOT_PROTOCOL=linux-legacy32 RELEASE=1 SMP=4 NETDEV=tap - - name: Syscall Test (Linux EFI Handover Boot Protocol) (Debug Build) + - name: Syscall Test (Linux EFI PE/COFF Boot Protocol) (Debug Build) id: syscall_test if: ${{ matrix.test_id == 'syscall_test' }} - run: make run AUTO_TEST=syscall ENABLE_KVM=1 BOOT_PROTOCOL=linux-efi-handover64 RELEASE=0 NETDEV=tap + run: make run AUTO_TEST=syscall ENABLE_KVM=1 BOOT_PROTOCOL=linux-efi-pe64 RELEASE=0 NETDEV=tap - name: Syscall Test at Ext2 (MicroVM) id: syscall_test_at_ext2_microvm diff --git a/Makefile b/Makefile index 4596d483..94b50d67 100644 --- a/Makefile +++ b/Makefile @@ -105,6 +105,9 @@ CARGO_OSDK_ARGS += --grub-mkrescue=/usr/bin/grub-mkrescue CARGO_OSDK_ARGS += --grub-boot-protocol="linux" # FIXME: GZIP self-decompression (--encoding gzip) triggers CPU faults CARGO_OSDK_ARGS += --encoding raw +else ifeq ($(BOOT_PROTOCOL), linux-efi-pe64) +CARGO_OSDK_ARGS += --grub-boot-protocol="linux" +CARGO_OSDK_ARGS += --encoding raw else ifeq ($(BOOT_PROTOCOL), linux-legacy32) CARGO_OSDK_ARGS += --linux-x86-legacy-boot CARGO_OSDK_ARGS += --grub-boot-protocol="linux" diff --git a/ostd/libs/linux-bzimage/setup/src/main.rs b/ostd/libs/linux-bzimage/setup/src/main.rs index fc41251d..03c646b1 100644 --- a/ostd/libs/linux-bzimage/setup/src/main.rs +++ b/ostd/libs/linux-bzimage/setup/src/main.rs @@ -22,6 +22,7 @@ #![no_std] #![no_main] #![feature(maybe_uninit_fill)] +#![feature(maybe_uninit_slice)] #![feature(maybe_uninit_write_slice)] mod console; diff --git a/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/alloc.rs b/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/alloc.rs index 7857919e..39ebac1c 100644 --- a/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/alloc.rs +++ b/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/alloc.rs @@ -2,33 +2,44 @@ use core::mem::MaybeUninit; +use uefi::boot::AllocateType; + pub fn alloc_at(addr: usize, size: usize) -> &'static mut [MaybeUninit] { assert_ne!(addr, 0, "the address to allocate is zero"); + assert!( + addr.checked_add(size).is_some(), + "the range to allocate overflows" + ); + + let allocated = alloc_pages(AllocateType::Address(addr as u64), size); + assert_eq!( + allocated.as_ptr().addr(), + addr, + "the allocated address is not the request address" + ); + + allocated +} + +pub(super) fn alloc_pages(ty: AllocateType, size: usize) -> &'static mut [MaybeUninit] { assert!( size <= isize::MAX as usize, "the size to allocate exceeds `isize::MAX`" ); - addr.checked_add(size) - .expect("the range to allocate overflows"); - let allocated = uefi::boot::allocate_pages( - uefi::boot::AllocateType::Address(addr as u64), + ty, uefi::boot::MemoryType::LOADER_DATA, size.div_ceil(super::efi::PAGE_SIZE as usize), ) .expect("the UEFI allocation fails"); - assert_eq!( - allocated.as_ptr() as usize, - addr, - "the allocated address is not the request address" - ); // SAFETY: // 1. The address is not zero and the size is reasonable (there are less the `isize::MAX` bytes - // and the range won't overflow the address space), as asserted above. + // and the range won't overflow the address space), as asserted above or guaranteed by the + // implementation of `allocate_pages`. // 2. The memory region is allocated via the UEFI firmware, so it is valid for reading and // writing. We will not deallocate it, so it live for `'static`. // 3. The type alignment is 1 and the type can contain uninitialized data. - unsafe { core::slice::from_raw_parts_mut(addr as *mut MaybeUninit, size) } + unsafe { core::slice::from_raw_parts_mut(allocated.as_ptr().cast(), size) } } diff --git a/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/efi.rs b/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/efi.rs index 1e016281..358835d8 100644 --- a/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/efi.rs +++ b/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/efi.rs @@ -1,15 +1,19 @@ // SPDX-License-Identifier: MPL-2.0 +use core::{ffi::CStr, mem::MaybeUninit}; + +use boot::{open_protocol_exclusive, AllocateType}; use linux_boot_params::BootParams; use uefi::{boot::exit_boot_services, mem::memory_map::MemoryMap, prelude::*}; use uefi_raw::table::system::SystemTable; use super::decoder::decode_payload; +use crate::x86::amd64_efi::alloc::alloc_pages; pub(super) const PAGE_SIZE: u64 = 4096; -#[export_name = "main_efi_handover64"] -extern "sysv64" fn main_efi_handover64( +#[export_name = "main_efi_common64"] +extern "sysv64" fn main_efi_common64( handle: Handle, system_table: *const SystemTable, boot_params_ptr: *mut BootParams, @@ -23,30 +27,71 @@ extern "sysv64" fn main_efi_handover64( uefi::helpers::init().unwrap(); - // SAFETY: We get boot parameters from the boot loader, so by contract the pointer is valid and - // the underlying memory is initialized. We are an exclusive owner of the memory region, so we - // can create a mutable reference of the plain-old-data type. - let boot_params = unsafe { &mut *boot_params_ptr }; + let boot_params = if boot_params_ptr.is_null() { + allocate_boot_params() + } else { + // SAFETY: We get boot parameters from the boot loader, so by contract the pointer is valid and + // the underlying memory is initialized. We are an exclusive owner of the memory region, so we + // can create a mutable reference of the plain-old-data type. + unsafe { &mut *boot_params_ptr } + }; efi_phase_boot(boot_params); - // SAFETY: We do not open boot service protocols or maintain references to boot service code - // and data. + // SAFETY: All previously opened boot service protocols have been closed. At this time, we have + // no references to the code and data of the boot services. unsafe { efi_phase_runtime(boot_params) }; } +fn allocate_boot_params() -> &'static mut BootParams { + let boot_params = { + let bytes = alloc_pages(AllocateType::AnyPages, core::mem::size_of::()); + MaybeUninit::fill(bytes, 0); + // SAFETY: Zero initialization gives a valid representation for `BootParams`. + unsafe { &mut *bytes.as_mut_ptr().cast::() } + }; + + boot_params.hdr.header = linux_boot_params::LINUX_BOOT_HEADER_MAGIC; + + boot_params +} + fn efi_phase_boot(boot_params: &mut BootParams) { uefi::println!( "[EFI stub] Loaded with offset {:#x}", crate::x86::image_load_offset(), ); + // Load the command line if it is not loaded. + if boot_params.hdr.cmd_line_ptr == 0 && boot_params.ext_cmd_line_ptr == 0 { + if let Some(cmdline) = load_cmdline() { + boot_params.hdr.cmd_line_ptr = cmdline.as_ptr().addr().try_into().unwrap(); + boot_params.ext_cmd_line_ptr = 0; + boot_params.hdr.cmdline_size = (cmdline.count_bytes() + 1).try_into().unwrap(); + } + } + + // Load the init ramdisk if it is not loaded. + if boot_params.hdr.ramdisk_image == 0 && boot_params.ext_ramdisk_image == 0 { + if let Some(initrd) = load_initrd() { + boot_params.hdr.ramdisk_image = initrd.as_ptr().addr().try_into().unwrap(); + boot_params.ext_ramdisk_image = 0; + boot_params.hdr.ramdisk_size = initrd.len().try_into().unwrap(); + boot_params.ext_ramdisk_size = 0; + } + } + // Fill the boot params with the RSDP address if it is not provided. if boot_params.acpi_rsdp_addr == 0 { boot_params.acpi_rsdp_addr = find_rsdp_addr().expect("ACPI RSDP address is not available") as usize as u64; } + // Fill the boot params with the screen info if it is not provided. + if boot_params.screen_info.lfb_base == 0 && boot_params.screen_info.ext_lfb_base == 0 { + fill_screen_info(&mut boot_params.screen_info); + } + // Decode the payload and load it as an ELF file. uefi::println!("[EFI stub] Decoding the kernel payload"); let kernel = decode_payload(crate::x86::payload()); @@ -54,6 +99,130 @@ fn efi_phase_boot(boot_params: &mut BootParams) { crate::loader::load_elf(&kernel); } +fn load_cmdline() -> Option<&'static CStr> { + uefi::println!("[EFI stub] Loading the cmdline"); + + let loaded_image = open_protocol_exclusive::( + uefi::boot::image_handle(), + ) + .unwrap(); + + let Some(load_options) = loaded_image.load_options_as_bytes() else { + uefi::println!("[EFI stub] Warning: No cmdline is available!"); + return None; + }; + + if load_options.len() % 2 != 0 || load_options.iter().skip(1).step_by(2).any(|c| *c != 0) { + uefi::println!("[EFI stub] Warning: The cmdline contains non-ASCII characters!"); + return None; + } + + // The load options are a `Char16` sequence. We should convert it to a `Char8` sequence. + let cmdline_bytes = alloc_pages( + AllocateType::MaxAddress(u32::MAX as u64), + load_options.len() / 2 + 1, + ); + for i in 0..load_options.len() / 2 { + cmdline_bytes[i].write(load_options[i * 2]); + } + cmdline_bytes[load_options.len() / 2].write(0); + + // SAFETY: We've initialized all the bytes above. + let cmdline_str = + CStr::from_bytes_until_nul(unsafe { cmdline_bytes.assume_init_ref() }).unwrap(); + + uefi::println!("[EFI stub] Loaded the cmdline: {:?}", cmdline_str); + + Some(cmdline_str) +} + +// Linux loads the initrd either using a special protocol `LINUX_EFI_INITRD_MEDIA_GUID` or using +// the file path specified on the command line (e.g., `initrd=/initrd.img`). We now only support +// the former approach, as it is more "modern" and easier to implement. Note that this approach +// requires the boot loader (e.g., GRUB, systemd-boot) to implement the protocol, while the latter +// approach does not. +fn load_initrd() -> Option<&'static [u8]> { + uefi::println!("[EFI stub] Loading the initrd"); + + // Note that we should switch to `uefi::proto::media::load_file::LoadFile2` once it provides a + // more ergonomic API. Its current API requires `alloc` and cannot load files on pages (i.e., + // ensure that the initrd is aligned to the page size). + #[repr(transparent)] + // SAFETY: The protocol GUID matches the protocol itself. + #[uefi::proto::unsafe_protocol(uefi_raw::protocol::media::LoadFile2Protocol::GUID)] + struct LoadFile2(uefi_raw::protocol::media::LoadFile2Protocol); + + let mut device_path_buf = [MaybeUninit::uninit(); 20 /* vendor */ + 4 /* end */]; + let mut device_path = { + use uefi::proto::device_path::build; + build::DevicePathBuilder::with_buf(&mut device_path_buf) + .push(&build::media::Vendor { + // LINUX_EFI_INITRD_MEDIA_GUID + vendor_guid: uefi::guid!("5568e427-68fc-4f3d-ac74-ca555231cc68"), + vendor_defined_data: &[], + }) + .unwrap() + .finalize() + .unwrap() + }; + + let Ok(handle) = uefi::boot::locate_device_path::(&mut device_path) else { + uefi::println!("[EFI stub] Warning: Failed to locate the initrd device!"); + return None; + }; + + let Ok(mut load_file2) = uefi::boot::open_protocol_exclusive::(handle) else { + uefi::println!("[EFI stub] Warning: Failed to open the initrd protocol!"); + return None; + }; + + let mut size = 0; + // SAFETY: The arguments are correctly specified according to the UEFI specification. + let status = unsafe { + (load_file2.0.load_file)( + &mut load_file2.0, + device_path.as_ffi_ptr().cast(), + false, /* boot_policy */ + &mut size, + core::ptr::null_mut(), + ) + }; + if status != uefi::Status::BUFFER_TOO_SMALL { + uefi::println!("[EFI stub] Warning: Failed to get the initrd size!"); + return None; + } + + let initrd = alloc_pages(AllocateType::MaxAddress(u32::MAX as u64), size); + // SAFETY: The arguments are correctly specified according to the UEFI specification. + let status = unsafe { + (load_file2.0.load_file)( + &mut load_file2.0, + device_path.as_ffi_ptr().cast(), + false, /* boot_policy */ + &mut size, + initrd.as_mut_ptr().cast(), + ) + }; + if status.is_error() { + uefi::println!("[EFI stub] Warning: Failed to load the initrd!"); + return None; + } + assert_eq!( + size, + initrd.len(), + "the initrd size has changed between two EFI calls" + ); + + uefi::println!( + "[EFI stub] Loaded the initrd: addr={:#x}, size={:#x}", + initrd.as_ptr().addr(), + initrd.len() + ); + + // SAFETY: We've initialized all the bytes in `load_file`. + Some(unsafe { initrd.assume_init_ref() }) +} + fn find_rsdp_addr() -> Option<*const ()> { use uefi::table::cfg::{ACPI2_GUID, ACPI_GUID}; @@ -65,13 +234,58 @@ fn find_rsdp_addr() -> Option<*const ()> { .find(|entry| entry.guid == acpi_guid) .map(|entry| entry.address.cast::<()>()) }) { + uefi::println!("[EFI stub] Found the ACPI RSDP at {:p}", rsdp_addr); return Some(rsdp_addr); } } + uefi::println!("[EFI stub] Warning: Failed to find the ACPI RSDP address!"); + None } +fn fill_screen_info(screen_info: &mut linux_boot_params::ScreenInfo) { + use uefi::proto::console::gop::{GraphicsOutput, PixelFormat}; + + let Ok(handle) = uefi::boot::get_handle_for_protocol::() else { + uefi::println!("[EFI stub] Warning: Failed to locate the graphics handle!"); + return; + }; + + let Ok(mut protocol) = open_protocol_exclusive::(handle) else { + uefi::println!("[EFI stub] Warning: Failed to open the graphics protocol!"); + return; + }; + + if !matches!( + protocol.current_mode_info().pixel_format(), + PixelFormat::Rgb | PixelFormat::Bgr + ) { + uefi::println!( + "[EFI stub] Warning: Ignored the framebuffer as the pixel format is not supported!" + ); + return; + } + + let addr = protocol.frame_buffer().as_mut_ptr().addr(); + let (width, height) = protocol.current_mode_info().resolution(); + + // TODO: We are only filling in fields that will be accessed later in the kernel. We should + // fill in other important information such as the pixel format. + screen_info.lfb_base = addr as u32; + screen_info.ext_lfb_base = (addr >> 32) as u32; + screen_info.lfb_width = width.try_into().unwrap(); + screen_info.lfb_height = height.try_into().unwrap(); + screen_info.lfb_depth = 32; // We've checked the pixel format above. + + uefi::println!( + "[EFI stub] Found the framebuffer at {:#x} with {}x{} pixels", + addr, + width, + height + ); +} + unsafe fn efi_phase_runtime(boot_params: &mut BootParams) -> ! { uefi::println!("[EFI stub] Exiting EFI boot services"); // SAFETY: The safety is upheld by the caller. diff --git a/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/setup.S b/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/setup.S index 77a5fa51..5add6c2b 100644 --- a/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/setup.S +++ b/ostd/libs/linux-bzimage/setup/src/x86/amd64_efi/setup.S @@ -50,6 +50,22 @@ entry_efi_handover64: // RSI: efi_system_table_t *table // RDX: struct boot_params *bp + jmp efi_common64 + +.global entry_efi_pe64 +entry_efi_pe64: + // This is the 64-bit EFI PE/COFF entry point. + // + // Arguments: + // RCX: void *handle + // RDX: efi_system_table_t *table + + mov rdi, rcx + mov rsi, rdx + xor rdx, rdx + jmp efi_common64 + +efi_common64: // We can reuse the stack provided by the UEFI firmware until a short time // after exiting the UEFI boot services. So we don't build our own stack. // @@ -83,19 +99,12 @@ reloc_iter: reloc_done: // Call the Rust main routine. - call main_efi_handover64 + call main_efi_common64 // The main routine should not return. If it does, there is nothing we can // do but stop the machine. jmp halt -.global entry_efi_pe64 -entry_efi_pe64: - // This is the 64-bit EFI PE/COFF entry point. - - // Not supported yet. Just stop the machine. - jmp halt - halt: hlt jmp halt