diff --git a/loader/src/memory_mapper.rs b/loader/src/memory_mapper.rs index 55bdc46fe..af98fedf7 100644 --- a/loader/src/memory_mapper.rs +++ b/loader/src/memory_mapper.rs @@ -17,6 +17,8 @@ use core::alloc::Layout; pub type Result = core::result::Result; +pub type MemoryWriter = fn(&mut MemoryMapper, usize, &[u8]) -> Result<()>; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct MemoryPermissions(u8); @@ -79,11 +81,12 @@ pub struct MemoryMapper { virtual_end: usize, mem: Storage, mode: MappingMode, + writer: Option, } impl MemoryMapper { #[inline] - pub fn new(regions: Option<&'static [MemoryRegion]>) -> Self { + pub fn new(regions: Option<&'static [MemoryRegion]>, writer: Option) -> Self { Self { virtual_entry: 0, virtual_start: usize::MAX, @@ -93,6 +96,7 @@ impl MemoryMapper { Some(regions) => MappingMode::Fixed(regions), None => MappingMode::Allocated, }, + writer, } } @@ -250,6 +254,12 @@ impl MemoryMapper { pub fn write_slice_at(&mut self, vaddr: usize, data: &[u8]) -> Result { let size = data.len(); + if let Some(writer) = self.writer.take() { + let result = writer(self, vaddr, data); + self.writer = Some(writer); + result?; + return Ok(size); + } if size == 0 { return Ok(size); } diff --git a/loader/tests/inputs/BUILD.gn b/loader/tests/inputs/BUILD.gn index 98d4f125c..b0213be3c 100644 --- a/loader/tests/inputs/BUILD.gn +++ b/loader/tests/inputs/BUILD.gn @@ -15,7 +15,7 @@ import("//build/boards/${board}.gni") import("//build/toolchain/blueos.gni") -if (defined(loader_test_relocation)) { +if (defined(ram_loader_test_relocation)) { _loader_test_app = "exec_app:loader_test_app" } else { _loader_test_app = "no_std_app:loader_test_app" @@ -26,6 +26,10 @@ group("loader_test_app") { # Postlink actions need visibility to the target that generates the binary. public_deps = [ _loader_test_app ] + if (defined(ram_loader_test_relocation) && + defined(flash_loader_test_relocation)) { + public_deps += [ "exec_app:loader_test_uselibrs" ] + } } postlink_action("gen_loader_test_elf_path_symbol") { @@ -46,10 +50,35 @@ static_library("loader_test_elf_path") { sources = get_target_outputs(":gen_loader_test_elf_path_symbol") } +if (defined(ram_loader_test_relocation) && + defined(flash_loader_test_relocation)) { + postlink_action("gen_loader_test_uselibrs_elf_path_symbol") { + testonly = true + exe = "exec_app:loader_test_uselibrs" + script = "//kernel/loader/tests/scripts/gen_path_symbol.py" + outfile = "${target_gen_dir}/${target_name}.c" + args = [ + "LOADER_TEST_USELIBRS_ELF_PATH", + rebase_path(outfile), + ] + outputs = [ outfile ] + } + + static_library("loader_test_uselibrs_elf_path") { + testonly = true + deps = [ ":gen_loader_test_uselibrs_elf_path_symbol" ] + sources = get_target_outputs(":gen_loader_test_uselibrs_elf_path_symbol") + } +} + group("inputs") { testonly = true deps = [ ":loader_test_elf_path", "malware:bomb_path", ] + if (defined(ram_loader_test_relocation) && + defined(flash_loader_test_relocation)) { + deps += [ ":loader_test_uselibrs_elf_path" ] + } } diff --git a/loader/tests/inputs/exec_app/BUILD.gn b/loader/tests/inputs/exec_app/BUILD.gn index d863bfe4d..6116b9901 100644 --- a/loader/tests/inputs/exec_app/BUILD.gn +++ b/loader/tests/inputs/exec_app/BUILD.gn @@ -15,18 +15,19 @@ import("//build/boards/${board}.gni") import("//build/templates/rust.gni") -assert(defined(loader_test_relocation), - "loader_test_relocation must be defined for the EXEC loader test app") -assert(defined(loader_test_relocation.region), - "loader_test_relocation.region must be defined") -assert(defined(loader_test_relocation.origin), - "loader_test_relocation.origin must be defined") -assert(defined(loader_test_relocation.length), - "loader_test_relocation.length must be defined") -assert(defined(loader_test_relocation.permissions), - "loader_test_relocation.permissions must be defined") +assert( + defined(ram_loader_test_relocation), + "ram_loader_test_relocation must be defined for the EXEC loader test app") +assert(defined(ram_loader_test_relocation.region), + "ram_loader_test_relocation.region must be defined") +assert(defined(ram_loader_test_relocation.origin), + "ram_loader_test_relocation.origin must be defined") +assert(defined(ram_loader_test_relocation.length), + "ram_loader_test_relocation.length must be defined") +assert(defined(ram_loader_test_relocation.permissions), + "ram_loader_test_relocation.permissions must be defined") -_loader_test_linker_script_template = "link.x.template" +_loader_test_linker_script_template = "ram_link.x.template" _loader_test_linker_script = "${target_gen_dir}/loader_test_relocation_link.x" action("gen_loader_test_relocation") { @@ -35,13 +36,13 @@ action("gen_loader_test_relocation") { inputs = [ _loader_test_linker_script_template ] args = [ "--region", - loader_test_relocation.region, + ram_loader_test_relocation.region, "--origin", - loader_test_relocation.origin, + ram_loader_test_relocation.origin, "--length", - loader_test_relocation.length, + ram_loader_test_relocation.length, "--permissions", - loader_test_relocation.permissions, + ram_loader_test_relocation.permissions, "--linker-script-template", rebase_path(_loader_test_linker_script_template), "--linker-script", @@ -53,15 +54,81 @@ action("gen_loader_test_relocation") { build_rust("loader_test_app") { testonly = true crate_type = "bin" - sources = [ "src/main.rs" ] + sources = [ "src/xor.rs" ] build_deps = [ ":gen_loader_test_relocation" ] configs += [ "//build/boards/${board}:app_config" ] linker_script = _loader_test_linker_script rustflags = [ "-Cpanic=abort", "-Crelocation-model=static", + "-Cstrip=symbols", + "-Clink-arg=-Wl,-s", "-Clink-arg=-Wl,-static", "-Clink-arg=-Wl,--no-pie", "-Clink-arg=-nostartfiles", ] } + +if (defined(flash_loader_test_relocation)) { + _irom = flash_loader_test_relocation.irom + _rodata = flash_loader_test_relocation.rodata + _rwdata = flash_loader_test_relocation.rwdata + _loader_test_xip_linker_script_template = "flash_link.x.template" + _loader_test_xip_linker_script = "${target_gen_dir}/loader_test_xip_link.x" + + action("gen_loader_test_xip_relocation") { + testonly = true + script = "//kernel/loader/tests/scripts/gen_loader_test_relocation.py" + inputs = [ _loader_test_xip_linker_script_template ] + args = [ + "--irom-origin", + _irom.origin, + "--irom-length", + _irom.length, + "--irom-permissions", + _irom.permissions, + "--rodata-origin", + _rodata.origin, + "--rodata-length", + _rodata.length, + "--rodata-permissions", + _rodata.permissions, + "--rwdata-origin", + _rwdata.origin, + "--rwdata-length", + _rwdata.length, + "--rwdata-permissions", + _rwdata.permissions, + "--flash-mmu-page-size", + flash_loader_test_relocation.page_size, + "--linker-script-template", + rebase_path(_loader_test_xip_linker_script_template), + "--linker-script", + rebase_path(_loader_test_xip_linker_script), + ] + outputs = [ _loader_test_xip_linker_script ] + } + + build_rust("loader_test_uselibrs") { + testonly = true + crate_type = "bin" + sources = [ "src/uselibrs.rs" ] + build_deps = [ ":gen_loader_test_xip_relocation" ] + configs += [ "//build/boards/${board}:app_config" ] + linker_script = _loader_test_xip_linker_script + rustflags = [ + "-Cpanic=abort", + "-Crelocation-model=static", + "-Cstrip=symbols", + "-Clink-arg=-Wl,-s", + "-Clink-arg=-Wl,-static", + "-Clink-arg=-Wl,--no-pie", + "-Clink-arg=-nostartfiles", + ] + deps = [ + "//kernel/kernel:atomic", + "//libc", + "//librs:librs_swi", + ] + } +} diff --git a/loader/tests/inputs/exec_app/flash_link.x.template b/loader/tests/inputs/exec_app/flash_link.x.template new file mode 100644 index 000000000..8f8df9f91 --- /dev/null +++ b/loader/tests/inputs/exec_app/flash_link.x.template @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 vivo Mobile Communication Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +ENTRY(_start) + +MEMORY +{ + IROM (@IROM_PERMISSIONS@) : ORIGIN = @IROM_ORIGIN@, LENGTH = @IROM_LENGTH@ + RODATA (@RODATA_PERMISSIONS@) : ORIGIN = @RODATA_ORIGIN@, LENGTH = @RODATA_LENGTH@ + RWDATA (@RWDATA_PERMISSIONS@) : ORIGIN = @RWDATA_ORIGIN@, LENGTH = @RWDATA_LENGTH@ +} + +PHDRS +{ + text PT_LOAD FLAGS(5); + rodata PT_LOAD FLAGS(4); + data PT_LOAD FLAGS(6); +} + +SECTIONS +{ + .text : ALIGN(4) + { + KEEP(*(.text._start)) + *(.text .text.*) + } > IROM :text + + .rodata : ALIGN(4) + { + *(.srodata .srodata.*) + *(.rodata .rodata.*) + } > RODATA :rodata + + .data : ALIGN(4) + { + PROVIDE(__global_pointer$ = . + 0x800); + *(.sdata .sdata.*) + *(.data .data.*) + *(.tdata .tdata.*) + } > RWDATA :data + + .bss (NOLOAD) : ALIGN(4) + { + *(.sbss .sbss.*) + *(.bss .bss.*) + *(.tbss .tbss.*) + *(COMMON) + } > RWDATA :data + + /DISCARD/ : + { + *(.eh_frame*) + *(.comment*) + } + + ASSERT(SIZEOF(.text) > 0, "EXEC payload has no text") + ASSERT(SIZEOF(.text) <= @FLASH_MMU_PAGE_SIZE@, + "EXEC payload text exceeds its flash MMU page") + ASSERT(SIZEOF(.rodata) <= @FLASH_MMU_PAGE_SIZE@, + "EXEC payload rodata exceeds its flash MMU page") +} diff --git a/loader/tests/inputs/exec_app/link.x.template b/loader/tests/inputs/exec_app/ram_link.x.template similarity index 100% rename from loader/tests/inputs/exec_app/link.x.template rename to loader/tests/inputs/exec_app/ram_link.x.template diff --git a/loader/tests/inputs/exec_app/src/uselibrs.rs b/loader/tests/inputs/exec_app/src/uselibrs.rs new file mode 100644 index 000000000..d6e548bee --- /dev/null +++ b/loader/tests/inputs/exec_app/src/uselibrs.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2026 vivo Mobile Communication Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_main] +#![no_std] + +use core::alloc::{GlobalAlloc, Layout}; + +// librs links liballoc even though this test does not allocate. +struct UnusedAllocator; + +unsafe impl GlobalAlloc for UnusedAllocator { + unsafe fn alloc(&self, _layout: Layout) -> *mut u8 { + core::ptr::null_mut() + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} +} + +#[global_allocator] +static ALLOCATOR: UnusedAllocator = UnusedAllocator; + +// RV32IMC has no A extension. libatomic uses these hooks for its fallback. +#[no_mangle] +pub extern "C" fn disable_local_irq_save() -> usize { + let old: usize; + unsafe { + core::arch::asm!( + "csrrci {old}, mstatus, 8", + old = out(reg) old, + options(nostack), + ); + } + old +} + +#[no_mangle] +pub extern "C" fn enable_local_irq_restore(old: usize) { + unsafe { + core::arch::asm!("csrw mstatus, {old}", old = in(reg) old, options(nostack)); + } +} + +#[no_mangle] +pub extern "C" fn _start() -> i32 { + librs::time::msleep(1) +} + +#[panic_handler] +fn panic(_: &core::panic::PanicInfo<'_>) -> ! { + loop { + core::hint::spin_loop(); + } +} diff --git a/loader/tests/inputs/exec_app/src/main.rs b/loader/tests/inputs/exec_app/src/xor.rs similarity index 100% rename from loader/tests/inputs/exec_app/src/main.rs rename to loader/tests/inputs/exec_app/src/xor.rs diff --git a/loader/tests/integration_test/BUILD.gn b/loader/tests/integration_test/BUILD.gn index bd2e845f8..fb348faad 100644 --- a/loader/tests/integration_test/BUILD.gn +++ b/loader/tests/integration_test/BUILD.gn @@ -40,13 +40,38 @@ build_rust("loader_integration_test") { proc_macro_deps = [ "//kernel/test_harness:blueos_test_macro" ] rustflags = [ "--test" ] - if (defined(loader_test_relocation)) { + if (defined(ram_loader_test_relocation)) { rustenv = [ - "LOADER_TEST_RELOCATION_ORIGIN=${loader_test_relocation.origin}", - "LOADER_TEST_RELOCATION_LENGTH=${loader_test_relocation.length}", - "LOADER_TEST_RELOCATION_PERMISSIONS=${loader_test_relocation.permissions}", + "LOADER_TEST_RAM_ORIGIN=" + ram_loader_test_relocation.origin, + "LOADER_TEST_RAM_LENGTH=" + ram_loader_test_relocation.length, + "LOADER_TEST_RAM_PERMISSIONS=" + ram_loader_test_relocation.permissions, ] cfgs = [ "loader_test_exec" ] + + if (defined(flash_loader_test_relocation)) { + deps += [ "//external/vendor/esp-rom-sys-0.1.4:esp_rom_sys" ] + _irom = flash_loader_test_relocation.irom + _rodata = flash_loader_test_relocation.rodata + _rwdata = flash_loader_test_relocation.rwdata + rustenv += [ + "LOADER_TEST_FLASH_CAPACITY=" + flash_loader_test_relocation.capacity, + "LOADER_TEST_FLASH_PAGE_SIZE=" + flash_loader_test_relocation.page_size, + "LOADER_TEST_IROM_ORIGIN=" + _irom.origin, + "LOADER_TEST_IROM_LENGTH=" + _irom.length, + "LOADER_TEST_IROM_PERMISSIONS=" + _irom.permissions, + "LOADER_TEST_IROM_FLASH_OFFSET=" + _irom.flash_offset, + "LOADER_TEST_RODATA_ORIGIN=" + _rodata.origin, + "LOADER_TEST_RODATA_LENGTH=" + _rodata.length, + "LOADER_TEST_RODATA_PERMISSIONS=" + _rodata.permissions, + "LOADER_TEST_RODATA_FLASH_OFFSET=" + _rodata.flash_offset, + "LOADER_TEST_RWDATA_ORIGIN=" + _rwdata.origin, + "LOADER_TEST_RWDATA_LENGTH=" + _rwdata.length, + "LOADER_TEST_RWDATA_PERMISSIONS=" + _rwdata.permissions, + ] + cfgs += [ "loader_test_flash" ] + rustflags += [ "-Clink-arg=-Wl,-T" + rebase_path( + "//external/vendor/esp-rom-sys-0.1.4/ld/esp32c3/rom-functions.x") ] + } } } diff --git a/loader/tests/integration_test/src/test.rs b/loader/tests/integration_test/src/test.rs index c6f7381a8..b165ac59a 100644 --- a/loader/tests/integration_test/src/test.rs +++ b/loader/tests/integration_test/src/test.rs @@ -22,6 +22,8 @@ use semihosting::{io::Read, println}; extern "C" { static LOADER_TEST_ELF_PATH: *const c_char; + #[cfg(loader_test_flash)] + static LOADER_TEST_USELIBRS_ELF_PATH: *const c_char; static INVALID_MAGIC_ELF_PATH: *const c_char; static INVALID_ENTRY_ELF_PATH: *const c_char; static INVALID_SEGMENT_SIZE_ELF_PATH: *const c_char; @@ -30,8 +32,13 @@ extern "C" { #[cfg(loader_test_exec)] mod loader_test_config { use blueos_loader as loader; + #[cfg(loader_test_flash)] + use esp_rom_sys::rom::spiflash::{ + esp_rom_spiflash_erase_sector, esp_rom_spiflash_unlock, esp_rom_spiflash_write, + ESP_ROM_SPIFLASH_RESULT_OK, ESP_ROM_SPIFLASH_RESULT_TIMEOUT, + }; - const fn parse_hex(value: &str) -> usize { + pub(super) const fn parse_hex(value: &str) -> usize { let bytes = value.as_bytes(); if bytes.len() <= 2 || bytes[0] != b'0' || (bytes[1] != b'x' && bytes[1] != b'X') { panic!("loader test relocation value must be hexadecimal"); @@ -52,7 +59,7 @@ mod loader_test_config { result } - const fn parse_permissions(value: &str) -> loader::MemoryPermissions { + pub(super) const fn parse_permissions(value: &str) -> loader::MemoryPermissions { let bytes = value.as_bytes(); let mut index = 0; let mut permissions = loader::MemoryPermissions::NONE; @@ -69,15 +76,275 @@ mod loader_test_config { permissions } - pub const TEST_REGION_START: usize = parse_hex(env!("LOADER_TEST_RELOCATION_ORIGIN")); - pub const TEST_REGION_END: usize = - TEST_REGION_START + parse_hex(env!("LOADER_TEST_RELOCATION_LENGTH")); - pub const TEST_REGION_PERMISSIONS: loader::MemoryPermissions = - parse_permissions(env!("LOADER_TEST_RELOCATION_PERMISSIONS")); + #[cfg(loader_test_flash)] + use core::sync::atomic::{AtomicUsize, Ordering}; + + #[cfg(loader_test_flash)] + pub const FLASH_CAPACITY: usize = parse_hex(env!("LOADER_TEST_FLASH_CAPACITY")); + #[cfg(loader_test_flash)] + pub const FLASH_PAGE_SIZE: usize = parse_hex(env!("LOADER_TEST_FLASH_PAGE_SIZE")); + + pub const RAM_START: usize = parse_hex(env!("LOADER_TEST_RAM_ORIGIN")); + pub const RAM_END: usize = RAM_START + parse_hex(env!("LOADER_TEST_RAM_LENGTH")); + + #[cfg(loader_test_flash)] + pub const IROM_START: usize = parse_hex(env!("LOADER_TEST_IROM_ORIGIN")); + #[cfg(loader_test_flash)] + pub const IROM_END: usize = IROM_START + parse_hex(env!("LOADER_TEST_IROM_LENGTH")); + #[cfg(loader_test_flash)] + pub const IROM_FLASH_OFFSET: usize = parse_hex(env!("LOADER_TEST_IROM_FLASH_OFFSET")); + + #[cfg(loader_test_flash)] + pub const RODATA_START: usize = parse_hex(env!("LOADER_TEST_RODATA_ORIGIN")); + #[cfg(loader_test_flash)] + pub const RODATA_END: usize = RODATA_START + parse_hex(env!("LOADER_TEST_RODATA_LENGTH")); + #[cfg(loader_test_flash)] + pub const RODATA_FLASH_OFFSET: usize = parse_hex(env!("LOADER_TEST_RODATA_FLASH_OFFSET")); + + #[cfg(loader_test_flash)] + pub const RWDATA_START: usize = parse_hex(env!("LOADER_TEST_RWDATA_ORIGIN")); + #[cfg(loader_test_flash)] + pub const RWDATA_END: usize = RWDATA_START + parse_hex(env!("LOADER_TEST_RWDATA_LENGTH")); + + #[cfg(loader_test_flash)] + static IROM_WRITTEN: AtomicUsize = AtomicUsize::new(0); + #[cfg(loader_test_flash)] + static RODATA_WRITTEN: AtomicUsize = AtomicUsize::new(0); + /// Writes a sector-aligned image to the ESP32-C3 boot flash through its ROM driver. + /// + /// ESP32 QEMU exposes its boot NOR flash through the dedicated SPI1 controller, + /// which is serviced by these ROM routines. The destination range is erased first. + #[cfg(loader_test_flash)] + fn write_boot_flash(offset: u32, data: &[u8]) -> Result<(), &'static str> { + const SECTOR_ERASE_SIZE: usize = 4096; + const PROGRAM_CHUNK_SIZE: usize = 256; + + if data.is_empty() { + return Ok(()); + } + if offset as usize % SECTOR_ERASE_SIZE != 0 { + return Err("Boot flash destination must be sector aligned"); + } + let end = (offset as usize) + .checked_add(data.len()) + .ok_or("Boot flash destination overflow")?; + if end > FLASH_CAPACITY { + return Err("Boot flash destination exceeds capacity"); + } + + let check_result = |result| match result { + ESP_ROM_SPIFLASH_RESULT_OK => Ok(()), + ESP_ROM_SPIFLASH_RESULT_TIMEOUT => Err("Boot flash operation timed out"), + _ => Err("Boot flash is not ready"), + }; + + check_result(unsafe { esp_rom_spiflash_unlock() })?; + let first_sector = offset as usize / SECTOR_ERASE_SIZE; + let sector_count = data.len().div_ceil(SECTOR_ERASE_SIZE); + for sector in first_sector..first_sector + sector_count { + check_result(unsafe { esp_rom_spiflash_erase_sector(sector as u32) })?; + } + + const WORDS_PER_CHUNK: usize = PROGRAM_CHUNK_SIZE / core::mem::size_of::(); + let mut words = [u32::MAX; WORDS_PER_CHUNK]; + let mut written = 0usize; + while written < data.len() { + let len = core::cmp::min(PROGRAM_CHUNK_SIZE, data.len() - written); + words.fill(u32::MAX); + let bytes = unsafe { + core::slice::from_raw_parts_mut(words.as_mut_ptr().cast::(), PROGRAM_CHUNK_SIZE) + }; + bytes[..len].copy_from_slice(&data[written..written + len]); + let aligned_len = len.next_multiple_of(core::mem::size_of::()); + check_result(unsafe { + esp_rom_spiflash_write(offset + written as u32, words.as_ptr(), aligned_len as u32) + })?; + written += len; + } + Ok(()) + } + + #[cfg(loader_test_flash)] + pub fn write_load_data( + mapper: &mut loader::MemoryMapper, + vaddr: usize, + data: &[u8], + ) -> loader::Result { + let (region_start, flash_offset, written) = if (IROM_START..IROM_END).contains(&vaddr) { + (IROM_START, IROM_FLASH_OFFSET, &IROM_WRITTEN) + } else if (RODATA_START..RODATA_END).contains(&vaddr) { + (RODATA_START, RODATA_FLASH_OFFSET, &RODATA_WRITTEN) + } else { + mapper.write_slice_at(vaddr, data)?; + return Ok(()); + }; + let offset = vaddr + .checked_sub(region_start) + .and_then(|value| flash_offset.checked_add(value)) + .ok_or("Flash destination overflow")?; + let end = offset + .checked_add(data.len()) + .ok_or("Flash destination overflow")?; + if end > FLASH_CAPACITY { + return Err("Flash destination exceeds capacity"); + } + write_boot_flash(offset as u32, data)?; + written.fetch_max(vaddr - region_start + data.len(), Ordering::Relaxed); + Ok(()) + } + + #[cfg(not(loader_test_flash))] pub static TEST_REGIONS: [loader::MemoryRegion; 1] = [unsafe { - loader::MemoryRegion::new(TEST_REGION_START, TEST_REGION_END, TEST_REGION_PERMISSIONS) + loader::MemoryRegion::new( + RAM_START, + RAM_END, + parse_permissions(env!("LOADER_TEST_RAM_PERMISSIONS")), + ) + }]; + + #[cfg(loader_test_flash)] + pub static TEST_REGIONS: [loader::MemoryRegion; 4] = [ + unsafe { + loader::MemoryRegion::new( + RAM_START, + RAM_END, + parse_permissions(env!("LOADER_TEST_RAM_PERMISSIONS")), + ) + }, + unsafe { + loader::MemoryRegion::new( + IROM_START, + IROM_END, + parse_permissions(env!("LOADER_TEST_IROM_PERMISSIONS")), + ) + }, + unsafe { + loader::MemoryRegion::new( + RODATA_START, + RODATA_END, + parse_permissions(env!("LOADER_TEST_RODATA_PERMISSIONS")), + ) + }, + unsafe { + loader::MemoryRegion::new( + RWDATA_START, + RWDATA_END, + parse_permissions(env!("LOADER_TEST_RWDATA_PERMISSIONS")), + ) + }, + ]; + + #[cfg(not(loader_test_flash))] + pub static NON_EXEC_REGIONS: [loader::MemoryRegion; 1] = [unsafe { + loader::MemoryRegion::new( + RAM_START, + RAM_END, + loader::MemoryPermissions::READ.bitor(loader::MemoryPermissions::WRITE), + ) }]; + + #[cfg(loader_test_flash)] + pub static NON_EXEC_REGIONS: [loader::MemoryRegion; 4] = [ + unsafe { + loader::MemoryRegion::new( + RAM_START, + RAM_END, + loader::MemoryPermissions::READ.bitor(loader::MemoryPermissions::WRITE), + ) + }, + unsafe { loader::MemoryRegion::new(IROM_START, IROM_END, loader::MemoryPermissions::READ) }, + unsafe { + loader::MemoryRegion::new( + RODATA_START, + RODATA_END, + parse_permissions(env!("LOADER_TEST_RODATA_PERMISSIONS")), + ) + }, + unsafe { + loader::MemoryRegion::new( + RWDATA_START, + RWDATA_END, + parse_permissions(env!("LOADER_TEST_RWDATA_PERMISSIONS")), + ) + }, + ]; + + #[cfg(loader_test_flash)] + unsafe extern "C" { + fn Cache_Ibus_MMU_Set( + ext_ram: u32, + vaddr: u32, + paddr: u32, + page_size_kb: u32, + page_count: u32, + fixed: u32, + ) -> i32; + fn Cache_Dbus_MMU_Set( + ext_ram: u32, + vaddr: u32, + paddr: u32, + page_size_kb: u32, + page_count: u32, + fixed: u32, + ) -> i32; + } + + #[cfg(loader_test_flash)] + pub fn map_written_flash(require_rodata: bool) -> Result<(), &'static str> { + let irom_pages = IROM_WRITTEN + .load(Ordering::Relaxed) + .div_ceil(FLASH_PAGE_SIZE); + let rodata_pages = RODATA_WRITTEN + .load(Ordering::Relaxed) + .div_ceil(FLASH_PAGE_SIZE); + if irom_pages == 0 || (require_rodata && rodata_pages == 0) { + return Err("Expected flash-backed ELF segments were not written"); + } + + const IROM_BUS_START: usize = 0x4200_0000; + const DROM_BUS_START: usize = 0x3c00_0000; + let irom_first_page = (IROM_START - IROM_BUS_START) / FLASH_PAGE_SIZE; + if rodata_pages > 0 { + let rodata_first_page = (RODATA_START - DROM_BUS_START) / FLASH_PAGE_SIZE; + if irom_first_page < rodata_first_page + rodata_pages + && rodata_first_page < irom_first_page + irom_pages + { + return Err("IROM and DROM segments overlap in the shared flash MMU"); + } + } + + let page_size_kb = (FLASH_PAGE_SIZE / 1024) as u32; + let irom_result = unsafe { + Cache_Ibus_MMU_Set( + 0, + IROM_START as u32, + IROM_FLASH_OFFSET as u32, + page_size_kb, + irom_pages as u32, + 0, + ) + }; + if irom_result != 0 { + return Err("Failed to map IROM flash pages"); + } + if rodata_pages > 0 { + let rodata_result = unsafe { + Cache_Dbus_MMU_Set( + 0, + RODATA_START as u32, + RODATA_FLASH_OFFSET as u32, + page_size_kb, + rodata_pages as u32, + 0, + ) + }; + if rodata_result != 0 { + return Err("Failed to map DROM flash pages"); + } + } + Ok(()) + } } fn read_all(ptr: *const core::ffi::c_char) -> semihosting::io::Result> { @@ -96,10 +363,10 @@ fn read_all(ptr: *const core::ffi::c_char) -> semihosting::io::Result> { } mod test_elf_loader { + #[cfg(loader_test_flash)] + use super::loader_test_config::{map_written_flash, write_load_data}; #[cfg(loader_test_exec)] - use super::loader_test_config::{ - TEST_REGIONS, TEST_REGION_END, TEST_REGION_PERMISSIONS, TEST_REGION_START, - }; + use super::loader_test_config::{NON_EXEC_REGIONS, RAM_START, TEST_REGIONS}; use super::*; use blueos_test_macro::test; @@ -110,30 +377,22 @@ mod test_elf_loader { static SHORT_REGIONS: [loader::MemoryRegion; 1] = [unsafe { // SAFETY: This is a valid subset of the configured loader test range. loader::MemoryRegion::new( - TEST_REGION_START, - TEST_REGION_START + 16, - TEST_REGION_PERMISSIONS, - ) - }]; - - #[cfg(loader_test_exec)] - static NON_EXEC_REGIONS: [loader::MemoryRegion; 1] = [unsafe { - // SAFETY: The configured region supports read and write accesses. - loader::MemoryRegion::new( - TEST_REGION_START, - TEST_REGION_END, - loader::MemoryPermissions::READ.bitor(loader::MemoryPermissions::WRITE), + RAM_START, + RAM_START + 16, + loader::MemoryPermissions::READ + .bitor(loader::MemoryPermissions::WRITE) + .bitor(loader::MemoryPermissions::EXECUTE), ) }]; fn new_mapper() -> loader::MemoryMapper { #[cfg(loader_test_exec)] { - loader::MemoryMapper::new(Some(&TEST_REGIONS)) + loader::MemoryMapper::new(Some(&TEST_REGIONS), None) } #[cfg(not(loader_test_exec))] { - loader::MemoryMapper::new(None) + loader::MemoryMapper::new(None, None) } } @@ -159,6 +418,18 @@ mod test_elf_loader { } } + #[cfg(all(loader_test_flash, not(debug_assertions)))] + #[test] + fn test_load_uselibrs_and_run_from_flash() { + let buf = read_all(unsafe { LOADER_TEST_USELIBRS_ELF_PATH }).unwrap(); + let mut mapper = loader::MemoryMapper::new(Some(&TEST_REGIONS), Some(write_load_data)); + assert!(loader::load_elf(&buf, &mut mapper).is_ok()); + map_written_flash(true).unwrap(); + let entry = mapper.real_entry().unwrap(); + let run = unsafe { core::mem::transmute:: i32>(entry) }; + assert_eq!(run(), 0); + } + // FIXME: We should use FS's lseek API to get lower footprint. // TODO: Use semihosting's seek API to parse the ELF file. #[cfg(not(loader_test_exec))] @@ -202,7 +473,7 @@ mod test_elf_loader { #[test] fn test_exec_rejects_allocated_mapper() { let buf = read_all(unsafe { LOADER_TEST_ELF_PATH }).unwrap(); - let mut mapper = loader::MemoryMapper::new(None); + let mut mapper = loader::MemoryMapper::new(None, None); assert!(loader::load_elf(&buf, &mut mapper).is_err()); } @@ -210,10 +481,10 @@ mod test_elf_loader { #[test] fn test_exec_rejects_out_of_range_without_writing() { let buf = read_all(unsafe { LOADER_TEST_ELF_PATH }).unwrap(); - let before = unsafe { (TEST_REGION_START as *const u32).read_volatile() }; - let mut mapper = loader::MemoryMapper::new(Some(&SHORT_REGIONS)); + let before = unsafe { (RAM_START as *const u32).read_volatile() }; + let mut mapper = loader::MemoryMapper::new(Some(&SHORT_REGIONS), None); assert!(loader::load_elf(&buf, &mut mapper).is_err()); - let after = unsafe { (TEST_REGION_START as *const u32).read_volatile() }; + let after = unsafe { (RAM_START as *const u32).read_volatile() }; assert_eq!(after, before); } @@ -221,7 +492,7 @@ mod test_elf_loader { #[test] fn test_exec_rejects_non_executable_region() { let buf = read_all(unsafe { LOADER_TEST_ELF_PATH }).unwrap(); - let mut mapper = loader::MemoryMapper::new(Some(&NON_EXEC_REGIONS)); + let mut mapper = loader::MemoryMapper::new(Some(&NON_EXEC_REGIONS), None); assert!(loader::load_elf(&buf, &mut mapper).is_err()); } } diff --git a/loader/tests/scripts/gen_loader_test_relocation.py b/loader/tests/scripts/gen_loader_test_relocation.py index 7b72d5ec6..8fc71a27e 100644 --- a/loader/tests/scripts/gen_loader_test_relocation.py +++ b/loader/tests/scripts/gen_loader_test_relocation.py @@ -23,10 +23,20 @@ def parse_args(): parser = argparse.ArgumentParser() - parser.add_argument("--region", required=True) - parser.add_argument("--origin", required=True) - parser.add_argument("--length", required=True) - parser.add_argument("--permissions", required=True) + parser.add_argument("--region") + parser.add_argument("--origin") + parser.add_argument("--length") + parser.add_argument("--permissions") + parser.add_argument("--irom-origin") + parser.add_argument("--irom-length") + parser.add_argument("--irom-permissions") + parser.add_argument("--rodata-origin") + parser.add_argument("--rodata-length") + parser.add_argument("--rodata-permissions") + parser.add_argument("--rwdata-origin") + parser.add_argument("--rwdata-length") + parser.add_argument("--rwdata-permissions") + parser.add_argument("--flash-mmu-page-size") parser.add_argument("--linker-script-template", required=True) parser.add_argument("--linker-script", required=True) return parser.parse_args() @@ -35,6 +45,7 @@ def parse_args(): def validate_identifier(value, description): if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", value): raise ValueError(f"invalid {description}: {value}") + return value def parse_number(value, description): @@ -47,14 +58,15 @@ def parse_number(value, description): return result -def write_linker_script(template_path, path, region, origin, length, - permissions): - replacements = { - "@REGION@": region, - "@ORIGIN@": f"0x{origin:x}", - "@LENGTH@": f"0x{length:x}", - "@PERMISSIONS@": permissions, - } +def validate_permissions(value, description): + if (not value or any(permission not in "rwx" for permission in value) + or len(set(value)) != len(value)): + raise ValueError( + f"{description} must contain unique r, w, or x permissions") + return value + + +def write_linker_script(template_path, path, replacements): template = pathlib.Path(template_path).read_text(encoding="utf-8") placeholders = set(LINKER_SCRIPT_PLACEHOLDER_PATTERN.findall(template)) expected_placeholders = set(replacements) @@ -84,26 +96,76 @@ def write_linker_script(template_path, path, region, origin, length, def main(): args = parse_args() - validate_identifier(args.region, "region name") - origin = parse_number(args.origin, "origin") - length = parse_number(args.length, "length") - if length <= 16: - raise ValueError("length must be greater than 16 bytes") - if origin + length <= origin: - raise ValueError("region address overflow") - permissions = args.permissions - if (not permissions or any(value not in "rwx" for value in permissions) - or len(set(permissions)) != len(permissions)): - raise ValueError( - "permissions must be r, w, x, or a combination of them") + values = { + "origin": args.origin, + "length": args.length, + "irom_origin": args.irom_origin, + "irom_length": args.irom_length, + "rodata_origin": args.rodata_origin, + "rodata_length": args.rodata_length, + "rwdata_origin": args.rwdata_origin, + "rwdata_length": args.rwdata_length, + "flash_mmu_page_size": args.flash_mmu_page_size, + } + values = { + name: value + for name, value in values.items() if value is not None + } + parsed = { + name: parse_number(value, name.replace("_", " ")) + for name, value in values.items() + } + generic_region = [args.region, args.origin, args.length, args.permissions] + if any(value is not None + for value in generic_region) and any(value is None + for value in generic_region): + raise ValueError("incomplete generic region") + if args.region is not None: + origin = parsed["origin"] + length = parsed["length"] + if origin + length <= origin: + raise ValueError("generic region address overflow") + + for region in ("irom", "rodata", "rwdata"): + provided = [ + getattr(args, f"{region}_origin"), + getattr(args, f"{region}_length"), + getattr(args, f"{region}_permissions"), + ] + if any(value is not None + for value in provided) and any(value is None + for value in provided): + raise ValueError(f"incomplete {region.replace('_', ' ')} region") + if provided[0] is None: + continue + origin = parsed[f"{region}_origin"] + length = parsed[f"{region}_length"] + if origin + length <= origin: + raise ValueError(f"{region} address overflow") + + replacements = { + f"@{name.upper()}@": f"0x{value:x}" + for name, value in parsed.items() + } + if args.region is not None: + replacements["@REGION@"] = validate_identifier(args.region, + "region name") + replacements["@PERMISSIONS@"] = validate_permissions( + args.permissions, "generic region permissions") + + for region in ("irom", "rodata", "rwdata"): + if getattr(args, f"{region}_permissions") is None: + continue + permissions = validate_permissions( + getattr(args, f"{region}_permissions"), + f"{region.replace('_', ' ')} permissions", + ) + replacements[f"@{region.upper()}_PERMISSIONS@"] = permissions write_linker_script( args.linker_script_template, args.linker_script, - args.region, - origin, - length, - args.permissions, + replacements, )