Support PE/COFF entry point

This commit is contained in:
Ruihan Li 2025-03-12 22:47:29 +08:00 committed by Tate, Hongliang Tian
parent 2a7cdb0802
commit 78a9ec9e96
6 changed files with 267 additions and 29 deletions

View File

@ -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

View File

@ -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"

View File

@ -22,6 +22,7 @@
#![no_std]
#![no_main]
#![feature(maybe_uninit_fill)]
#![feature(maybe_uninit_slice)]
#![feature(maybe_uninit_write_slice)]
mod console;

View File

@ -2,33 +2,44 @@
use core::mem::MaybeUninit;
use uefi::boot::AllocateType;
pub fn alloc_at(addr: usize, size: usize) -> &'static mut [MaybeUninit<u8>] {
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<u8>] {
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<u8>, size) }
unsafe { core::slice::from_raw_parts_mut(allocated.as_ptr().cast(), size) }
}

View File

@ -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::<BootParams>());
MaybeUninit::fill(bytes, 0);
// SAFETY: Zero initialization gives a valid representation for `BootParams`.
unsafe { &mut *bytes.as_mut_ptr().cast::<BootParams>() }
};
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::proto::loaded_image::LoadedImage>(
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::<LoadFile2>(&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::<LoadFile2>(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::<GraphicsOutput>() else {
uefi::println!("[EFI stub] Warning: Failed to locate the graphics handle!");
return;
};
let Ok(mut protocol) = open_protocol_exclusive::<GraphicsOutput>(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.

View File

@ -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