diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index b789438f..374716af 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -241,10 +241,10 @@ enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT driver/mb_flash.c ui/multiboot.c ) -enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM) +enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY) -if(ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT) - message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM requires ENABLE_FEAT_F4HWN_MULTIBOOT.") +if(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT) + message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY requires ENABLE_FEAT_F4HWN_MULTIBOOT.") endif() enable_feature(ENABLE_FEAT_F4HWN_QRCODE) diff --git a/App/driver/mb_flash.c b/App/driver/mb_flash.c index dcc1cc5d..2b226bee 100644 --- a/App/driver/mb_flash.c +++ b/App/driver/mb_flash.c @@ -31,7 +31,6 @@ /* External SPI flash chip-select is on PA3 (see driver/py25q16.c). */ #define MB_CS_PIN (1u << 3) -#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM /* LCD control pins used only for RAM-resident progress updates. */ #define MB_LCD_CS_PIN (1u << 2) /* PB2 */ #define MB_LCD_A0_PIN (1u << 6) /* PA6 */ @@ -40,7 +39,6 @@ #define MB_PROGRESS_COLS 118u #define MB_PROGRESS_FIRST_COL 5u #define MB_PROGRESS_FILLED 0x2Du -#endif /* Number of erase/program retries per page before giving up (and resetting * anyway - the region is already erased, so USB recovery is the only option). */ @@ -103,14 +101,21 @@ static void MB_PrepareInternalFlash(void) /* during which the flash bus is unavailable. It must therefore NOT fetch any */ /* code from flash nor read any flash data: it uses raw register access only */ /* (no external calls), reads the source from the external SPI flash in */ -/* polled mode, and resets the MCU when done. It is placed in .RamFunc, which */ -/* the linker stores in flash and the startup copies to RAM alongside .data. */ +/* polled mode, and resets the MCU when done. Normally it is placed in */ +/* .RamFunc, which startup copies to RAM alongside .data. With the overlay */ +/* enabled it is linked in .MBRamFunc and copied over the PY25Q16 sector cache */ +/* only immediately before use. */ /* -------------------------------------------------------------------------- */ /* These primitives are called repeatedly while application flash is offline. * Keep one copy of each in the copied RAM section: noinline/noclone prevents * GCC from silently duplicating one back into MB_RamReflash. */ -#define MB_RAM_HELPER __attribute__((section(".RamFunc"), noinline, noclone, used)) +#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY + #define MB_RAM_SECTION ".MBRamFunc" +#else + #define MB_RAM_SECTION ".RamFunc" +#endif +#define MB_RAM_HELPER __attribute__((section(MB_RAM_SECTION), noinline, noclone, used)) /* Polled single-byte SPI2 transfer. The result is returned through out so * timeout and received 0xFF remain distinguishable. */ @@ -150,7 +155,6 @@ MB_RAM_HELPER __attribute__((noreturn)) static void mb_ram_reset(void) for (;;) { } } -#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM /* Minimal SPI1 LCD writer. A display timeout merely disables progress updates: * it must never abort or delay the safety-critical flash copy. */ MB_RAM_HELPER static bool mb_ram_lcd_spi(uint8_t v) @@ -220,9 +224,7 @@ __attribute__((always_inline)) static inline void mb_ram_lcd_clear(void) GPIOB->BSRR = MB_LCD_CS_PIN; } } -#endif /* !ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM */ - -__attribute__((section(".RamFunc"), noinline, used)) +__attribute__((section(MB_RAM_SECTION), noinline, used)) static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize, uint8_t *progressLine) { @@ -231,14 +233,10 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize uint8_t buf[MB_FLASH_PAGE] __attribute__((aligned(4))); uint32_t remaining = imageSize; uint32_t regionRemaining = MB_INT_APP_SIZE; -#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM uint32_t pagesDone = 0; uint32_t progressAccumulator = 0; uint32_t progressFilled = 0; uint32_t lcdEnabled = progressLine != NULL; -#else - (void)progressLine; -#endif __disable_irq(); @@ -349,7 +347,6 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize if (!success) goto fatal_reset; -#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM /* Advance the gauge without division (which could call a helper * in erased flash). Refresh once per 8 KiB internal sector. */ if (lcdEnabled) @@ -368,7 +365,6 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize if ((pagesDone & 31u) == 0u || regionRemaining == MB_FLASH_PAGE) lcdEnabled = mb_ram_progress_blit(progressLine); } -#endif intAddr += MB_FLASH_PAGE; extAddr += readSize; @@ -377,10 +373,8 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize } FLASH->CR |= FLASH_CR_LOCK; -#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM if (lcdEnabled) mb_ram_lcd_clear(); -#endif mb_ram_reset(); fatal_reset: @@ -399,6 +393,39 @@ fatal_reset: /* Set when a polled SPI wait below times out (external flash unresponsive). */ static volatile int mb_spi_err; +#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY +/* Linker-provided load/execution bounds for the restore stub. Its execution + * address aliases the PY25Q16 sector cache; its load image remains in flash. */ +extern uint8_t __mb_ramfunc_load_start; +extern uint8_t __mb_ramfunc_start; +extern uint8_t __mb_ramfunc_end; + +static bool mb_load_ram_reflash_overlay(void) +{ + const volatile uint8_t *src = &__mb_ramfunc_load_start; + volatile uint8_t *dst = &__mb_ramfunc_start; + volatile uint8_t *end = &__mb_ramfunc_end; + + /* Volatile byte copies prevent a library memcpy call. The copy itself runs + * while internal flash is still fully available. */ + while (dst < end) + *dst++ = *src++; + + /* Cortex-M0+ has no instruction cache, but the barriers ensure that every + * store is visible before the following BLX starts fetching the stub. */ + __DSB(); + __ISB(); + + src = &__mb_ramfunc_load_start; + dst = &__mb_ramfunc_start; + while (dst < end) + if (*dst++ != *src++) + return false; + + return true; +} +#endif + /* Polled single-byte SPI2 transfer (flash-resident; runs in normal context). * Bounded so a wedged SPI can never freeze the firmware: on timeout it sets * mb_spi_err and returns 0xFF, letting the caller fail gracefully. */ @@ -668,8 +695,27 @@ uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line) const uint32_t slotBase = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE; - /* Valid: the RAM stub copies exactly image_size bytes, pads the partial - * page with 0xFF and erases the remainder of the application region. */ +#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY + /* No PY25Q16 driver access is allowed after this point: the sector cache is + * about to become executable storage and the RAM stub always ends in reset. + * Mask IRQs first, then explicitly stop SPI2 DMA before overwriting the + * cache. Keeping both operations local avoids relying on validation's + * current polled-SPI implementation and closes any IRQ re-arm window. */ + const uint32_t primask = __get_PRIMASK(); + __disable_irq(); + mb_spi_polled_mode(); + PY25Q16_InvalidateCache(); + if (!mb_load_ram_reflash_overlay()) + { + __set_PRIMASK(primask); + return MB_ERR_RAM_LOAD; + } +#endif + + /* Valid and, for the overlay path, safely loaded: the RAM stub copies + * exactly image_size bytes, pads the partial page with 0xFF and erases the + * remainder of the application region. Keep flash locked until this point + * so an overlay-copy failure can return without changing flash state. */ MB_PrepareInternalFlash(); /* Call through a volatile pointer so the compiler emits an absolute 'blx' diff --git a/App/driver/mb_flash.h b/App/driver/mb_flash.h index f07ba94d..14abc758 100644 --- a/App/driver/mb_flash.h +++ b/App/driver/mb_flash.h @@ -71,7 +71,7 @@ typedef struct __attribute__((packed)) { uint8_t reserved[16]; /* pad to 64 bytes, future use */ } mb_slot_header_t; -/* Restore validation result (MB_OK never returns - the radio resets). */ +/* Multiboot operation result (a successful restore resets before returning). */ enum { MB_OK = 0, MB_ERR_MAGIC, /* no/invalid slot header */ @@ -81,15 +81,15 @@ enum { MB_ERR_CRC, /* image CRC32 mismatch */ MB_ERR_SPI, /* external flash read/write timed out*/ MB_ERR_SLOT, /* slot index out of range */ - MB_ERR_AUTH /* write refused: timestamp mismatch */ + MB_ERR_AUTH, /* write refused: timestamp mismatch */ + MB_ERR_RAM_LOAD /* restore stub RAM copy mismatch */ }; /* Multi-slot API used by the boot selector. Validation always covers the full * image CRC before restore. progress_line may point to a 128-byte LCD page; the * RAM copier then fills it while reflashing. Pass NULL to disable LCD updates. - * The in-copier LCD progress code is compiled out when - * ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM is defined (to reclaim RAM-resident - * .RamFunc space); in that case progress_line is ignored. */ + * With ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY, the copier is loaded over the + * PY25Q16 sector cache only after validation and immediately before this call. */ uint8_t MB_ValidateSlot(uint8_t slot, mb_slot_header_t *out_header, uint32_t *out_crc); uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line); diff --git a/App/driver/py25q16.c b/App/driver/py25q16.c index 4eeb5ae2..dfde7ac2 100644 --- a/App/driver/py25q16.c +++ b/App/driver/py25q16.c @@ -43,7 +43,15 @@ #define PAGE_SIZE 0x100 static uint32_t SectorCacheAddr = 0x1000000; +#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY +/* The restore-only RAM stub is copied over this cache immediately before it + * erases internal flash. A reset always follows, so the cache is never needed + * again after the overlay becomes active. */ +static uint8_t SectorCache[SECTOR_SIZE] + __attribute__((section(".bss.mb_workspace"), aligned(4), used)); +#else static uint8_t SectorCache[SECTOR_SIZE]; +#endif static uint8_t BlackHole[4] __attribute__((aligned(4))); static volatile bool TC_Flag; diff --git a/App/driver/py25q16.h b/App/driver/py25q16.h index ab6ffd2d..5c93b7e3 100644 --- a/App/driver/py25q16.h +++ b/App/driver/py25q16.h @@ -28,7 +28,8 @@ void PY25Q16_SectorErase(uint32_t Address); /* Drop the internal single-sector write cache. Call after erasing/programming * flash behind the driver's back (e.g. the raw multiboot slot/profile ops) so a - * later write cannot skip or resurrect data based on a stale cached sector. */ + * later write cannot skip or resurrect data based on a stale cached sector. It + * is also called before multiboot reuses the cache storage as a RAM overlay. */ void PY25Q16_InvalidateCache(void); #ifdef ENABLE_FEAT_F4HWN_MULTIBOOT diff --git a/App/ui/multiboot.c b/App/ui/multiboot.c index bc915ec0..eedb38b2 100644 --- a/App/ui/multiboot.c +++ b/App/ui/multiboot.c @@ -39,6 +39,7 @@ static const char *mb_error_text(uint8_t err) case MB_ERR_SPI: return "SPI ERROR"; case MB_ERR_SLOT: return "bad slot"; case MB_ERR_AUTH: return "auth"; + case MB_ERR_RAM_LOAD: return "RAM LOAD ERROR"; default: return "error"; } } @@ -262,12 +263,8 @@ __attribute__((noinline)) static void mb_prepare_progress_screen(const char *tit UI_PrintStringSmallNormal(title, 2, 126, 1); UI_PrintStringSmallNormal("DO NOT POWER OFF", 2, 126, 3); UI_PrintStringSmallNormal(detail, 2, 126, 5); -#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM - /* Empty gauge that the RAM copier fills as it reflashes. Without the - * in-copier progress code the bar would never move, so it is left out and - * the static "DO NOT POWER OFF" screen stands on its own. */ + /* Empty gauge that the RAM copier fills as it reflashes. */ mb_draw_progress_outline(); -#endif ST7565_BlitStatusLine(); ST7565_BlitFullScreen(); } @@ -409,11 +406,7 @@ void UI_MultibootSelector(void) } mb_prepare_progress(selected); -#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM uint8_t err = MB_RestoreSlot(selected, gFrameBuffer[6]); -#else - uint8_t err = MB_RestoreSlot(selected, NULL); -#endif /* Only reached when the final pre-erase validation refused the slot. */ status[selected] = err; diff --git a/App/ui/welcome.c b/App/ui/welcome.c index 0547f468..659079e7 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -115,15 +115,10 @@ extern uint8_t _edata; // End of .data in RAM extern uint8_t _sbss; // Start of .bss in RAM extern uint8_t _ebss; // End of .bss in RAM -// _eflash_used must be defined in the linker script immediately after the last -// section with a FLASH load address (after .noncacheable). Example: -// -// .noncacheable : { -// ... -// } > RAM AT> FLASH -// _eflash_used = LOADADDR(.noncacheable) + SIZEOF(.noncacheable); -// -// This gives the exact byte count that the linker reports as FLASH used. +// _eflash_used is defined by the linker at the end of the final section with a +// FLASH load image. This is currently .mb_ramfunc (empty without the overlay), +// after the load images for .data and .noncacheable. It therefore gives the +// exact byte count that the linker reports as FLASH used. extern uint8_t _eflash_used; // Absolute symbols: their *address* IS the numeric size value (ARM/CMSIS convention). @@ -152,8 +147,8 @@ static void build_usage(uint32_t* ram_used, uint32_t* flash_used) const uint32_t stack_size = (uint32_t)(uintptr_t)&_Min_Stack_Size; *ram_used = span(&_sdata, &_ebss) + heap_size + stack_size; - // FLASH: _eflash_used is placed by the linker script right after the last - // section copied to FLASH (.data LMA + .noncacheable LMA). + // FLASH: _eflash_used follows the final FLASH load image (.mb_ramfunc, + // after the .data and .noncacheable load images). // Note: _etext is NOT usable here because this linker script places .rodata // sections AFTER _etext, making it an unreliable end-of-flash marker. *flash_used = span((void*)FLASH_BASE, &_eflash_used); diff --git a/CMakeLists.txt b/CMakeLists.txt index 4aa8726b..9e1fd523 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,25 @@ endif() # Post build processing # +# The restore stub executes while application flash is unavailable. Reject an +# overlay build if the linked stub branches outside its RAM section or if its +# input section refers to any external code/data. This is deliberately a hard +# build gate: either condition could make a restore unrecoverable without DFU. +if(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY) + add_custom_command( + TARGET ${EXE_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} + "-DOBJDUMP=${CMAKE_OBJDUMP}" + "-DELF=$" + "-DMAP=${CMAKE_CURRENT_BINARY_DIR}/${EXE_NAME}.map" + "-DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}" + -P "${CMAKE_SOURCE_DIR}/cmake/check_mb_ramfunc.cmake" + COMMENT "Checking multiboot RAM stub isolation" + VERBATIM + ) +endif() + # Generate .bin file add_custom_command( TARGET ${EXE_NAME} diff --git a/CMakePresets.json b/CMakePresets.json index 48083701..7424af34 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -93,7 +93,7 @@ "ENABLE_FEAT_F4HWN_LOGO_SAV": false, "ENABLE_FEAT_F4HWN_MENU_CAT": false, "ENABLE_FEAT_F4HWN_MULTIBOOT": false, - "ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM": false, + "ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY": false, "ENABLE_AGC_SHOW_DATA": false, "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, @@ -144,7 +144,7 @@ "ENABLE_FEAT_F4HWN_MENU_CAT": true, "ENABLE_FEAT_F4HWN_ACTION_PICKER": true, "ENABLE_FEAT_F4HWN_MULTIBOOT": true, - "ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM": false, + "ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY": true, "ENABLE_SWD": true, "EDITION_STRING": "Fusion", "TARGET": "f4hwn.fusion" diff --git a/Core/py32f071xb.ld b/Core/py32f071xb.ld index b35cac18..905d4e9b 100644 --- a/Core/py32f071xb.ld +++ b/Core/py32f071xb.ld @@ -159,7 +159,54 @@ SECTIONS *(.noncacheable*) . = ALIGN(4); } >RAM AT> FLASH - _eflash_used = LOADADDR(.noncacheable) + SIZEOF(.noncacheable); + + /* Optional multiboot overlay. The restore stub is linked at the same RAM + address as the 4 KiB PY25Q16 sector cache, but its load image remains in + FLASH and is copied into the cache only immediately before reflashing. */ + OVERLAY : NOCROSSREFS AT (LOADADDR(.noncacheable) + SIZEOF(.noncacheable)) + { + .mb_ramfunc + { + . = ALIGN(4); + __mb_ramfunc_start = .; + KEEP(*(.MBRamFunc)) + KEEP(*(.MBRamFunc*)) + . = ALIGN(4); + __mb_ramfunc_end = .; + } + + .mb_workspace + { + . = ALIGN(4); + __mb_workspace_start = .; + KEEP(*(.bss.mb_workspace)) + KEEP(*(.bss.mb_workspace*)) + . = ALIGN(4); + __mb_workspace_end = .; + } + } >RAM + + __mb_ramfunc_load_start = LOADADDR(.mb_ramfunc); + __mb_ramfunc_size = SIZEOF(.mb_ramfunc); + __mb_workspace_size = SIZEOF(.mb_workspace); + + ASSERT(__mb_ramfunc_size <= __mb_workspace_size, + "Multiboot RAM stub does not fit in PY25Q16 sector-cache overlay") + ASSERT((__mb_ramfunc_size == 0) || (__mb_workspace_size == 0x1000), + "Multiboot overlay workspace is not the 4 KiB sector cache") + ASSERT((__mb_ramfunc_size == 0) || + (ADDR(.mb_ramfunc) == ADDR(.mb_workspace)), + "Multiboot RAM stub and workspace do not share the same VMA") + ASSERT((__mb_ramfunc_size == 0) || + (LOADADDR(.mb_ramfunc) != ADDR(.mb_ramfunc)), + "Multiboot RAM stub load and execution addresses must differ") + ASSERT((LOADADDR(.mb_ramfunc) + SIZEOF(.mb_ramfunc)) <= + (ORIGIN(FLASH) + LENGTH(FLASH)), + "FLASH overflowed by multiboot RAM-stub load image") + + /* .mb_ramfunc is the final section with a FLASH load image. The workspace + member is NOLOAD/BSS storage and therefore contributes RAM only. */ + _eflash_used = LOADADDR(.mb_ramfunc) + SIZEOF(.mb_ramfunc); /* Uninitialized data section */ . = ALIGN(4); @@ -200,5 +247,3 @@ SECTIONS .ARM.attributes 0 : { *(.ARM.attributes) } } - - diff --git a/cmake/check_mb_ramfunc.cmake b/cmake/check_mb_ramfunc.cmake new file mode 100644 index 00000000..50757a6c --- /dev/null +++ b/cmake/check_mb_ramfunc.cmake @@ -0,0 +1,138 @@ +# Validate the multiboot restore stub after linking. The application flash is +# unavailable while this code runs, so every direct control-flow target and +# every symbolic code/data reference must remain inside .mb_ramfunc. + +foreach(required OBJDUMP ELF MAP BINARY_DIR) + if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "") + message(FATAL_ERROR "check_mb_ramfunc: missing -D${required}=...") + endif() +endforeach() + +if(NOT EXISTS "${ELF}") + message(FATAL_ERROR "check_mb_ramfunc: ELF not found: ${ELF}") +endif() +if(NOT EXISTS "${MAP}") + message(FATAL_ERROR "check_mb_ramfunc: map file not found: ${MAP}") +endif() + +execute_process( + COMMAND "${OBJDUMP}" -h "${ELF}" + RESULT_VARIABLE headers_result + OUTPUT_VARIABLE headers + ERROR_VARIABLE headers_error +) +if(NOT headers_result EQUAL 0) + message(FATAL_ERROR + "check_mb_ramfunc: objdump section scan failed (${headers_result}):\n${headers_error}") +endif() + +string(REGEX MATCH + "[ \t]\\.mb_ramfunc[ \t]+([0-9A-Fa-f]+)[ \t]+([0-9A-Fa-f]+)" + section_header "${headers}") +if(section_header STREQUAL "") + message(FATAL_ERROR "check_mb_ramfunc: .mb_ramfunc not found in ${ELF}") +endif() +set(section_size_hex "${CMAKE_MATCH_1}") +set(section_start_hex "${CMAKE_MATCH_2}") +math(EXPR section_size "0x${section_size_hex}") +math(EXPR section_start "0x${section_start_hex}") +math(EXPR section_end "${section_start} + ${section_size}") +math(EXPR section_end_hex "${section_end}" OUTPUT_FORMAT HEXADECIMAL) +if(section_size EQUAL 0) + message(FATAL_ERROR "check_mb_ramfunc: .mb_ramfunc is empty in an overlay build") +endif() + +execute_process( + COMMAND "${OBJDUMP}" --no-show-raw-insn -d -j .mb_ramfunc "${ELF}" + RESULT_VARIABLE disassembly_result + OUTPUT_VARIABLE disassembly + ERROR_VARIABLE disassembly_error +) +if(NOT disassembly_result EQUAL 0) + message(FATAL_ERROR + "check_mb_ramfunc: disassembly failed (${disassembly_result}):\n${disassembly_error}") +endif() + +# Check all Thumb branch forms. An indirect BLX/BX cannot be proven safe; only +# BX LR (a normal function return) is accepted. Direct branches must stay in the +# linked RAM section, including compiler-generated tail calls and veneers. +string(REPLACE "\n" ";" disassembly_lines "${disassembly}") +set(branch_count 0) +foreach(line IN LISTS disassembly_lines) + if(line MATCHES + "^[ \t]*[0-9A-Fa-f]+:[ \t]+([A-Za-z0-9.]+)([ \t]+(.*))?") + set(mnemonic "${CMAKE_MATCH_1}") + set(operands "${CMAKE_MATCH_3}") + + if(mnemonic MATCHES + "^(b|bl|blx|bx|beq|bne|bcs|bcc|bhs|blo|bmi|bpl|bvs|bvc|bhi|bls|bge|blt|bgt|ble)(\\.[nw])?$" + OR mnemonic MATCHES "^(cbz|cbnz)$") + math(EXPR branch_count "${branch_count} + 1") + + if(mnemonic MATCHES "^bx(\\.[nw])?$") + string(STRIP "${operands}" register_name) + if(NOT register_name STREQUAL "lr" AND NOT register_name STREQUAL "r14") + message(FATAL_ERROR + "check_mb_ramfunc: unsafe indirect branch in RAM stub:\n${line}") + endif() + continue() + endif() + + # BL/B operands contain the address directly; CBZ/CBNZ put it after + # the register and comma. In both forms it is the final numeric + # operand before objdump's optional annotation. + if(NOT operands MATCHES + "(^|[, \t])((0[xX])?[0-9A-Fa-f]+)([ \t]+<[^>]+>)?[ \t]*$") + message(FATAL_ERROR + "check_mb_ramfunc: unresolved/indirect branch in RAM stub:\n${line}") + endif() + set(target_hex "${CMAKE_MATCH_2}") + string(REGEX REPLACE "^0[xX]" "" target_hex "${target_hex}") + math(EXPR target "0x${target_hex}") + if(target LESS section_start OR NOT target LESS section_end) + message(FATAL_ERROR + "check_mb_ramfunc: branch leaves .mb_ramfunc:\n${line}\n" + "Allowed range: 0x${section_start_hex}..${section_end_hex}") + endif() + endif() + endif() +endforeach() + +# Locate the exact input object from the linker map rather than assuming a +# generator-specific CMakeFiles path. Any relocation in .MBRamFunc would be an +# external symbolic reference (code or data), because all approved helpers share +# this same input section and their local branches are assembler-resolved. +file(READ "${MAP}" map_contents) +string(REGEX MATCH + "[^\r\n]*\\.MBRamFunc[^\r\n]*mb_flash\\.c\\.(obj|o)" + object_line "${map_contents}") +if(object_line STREQUAL "") + message(FATAL_ERROR + "check_mb_ramfunc: mb_flash object not found in ${MAP} " + "(the overlay safety gate requires a non-LTO input object)") +endif() +string(REGEX MATCH "[^ \t]+mb_flash\\.c\\.(obj|o)" object_path "${object_line}") +if(NOT IS_ABSOLUTE "${object_path}") + set(object_path "${BINARY_DIR}/${object_path}") +endif() +if(NOT EXISTS "${object_path}") + message(FATAL_ERROR "check_mb_ramfunc: input object not found: ${object_path}") +endif() + +execute_process( + COMMAND "${OBJDUMP}" -r -j .MBRamFunc "${object_path}" + RESULT_VARIABLE relocations_result + OUTPUT_VARIABLE relocations + ERROR_VARIABLE relocations_error +) +if(NOT relocations_result EQUAL 0) + message(FATAL_ERROR + "check_mb_ramfunc: relocation scan failed (${relocations_result}):\n${relocations_error}") +endif() +if(relocations MATCHES "R_ARM_") + message(FATAL_ERROR + "check_mb_ramfunc: .MBRamFunc has an external code/data reference:\n${relocations}") +endif() + +message(STATUS + "Multiboot RAM stub isolation OK: ${section_size} bytes, ${branch_count} checked branches") diff --git a/cmake/gcc-arm-none-eabi.cmake b/cmake/gcc-arm-none-eabi.cmake index 10971efb..962505fe 100644 --- a/cmake/gcc-arm-none-eabi.cmake +++ b/cmake/gcc-arm-none-eabi.cmake @@ -13,6 +13,7 @@ set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++) set(CMAKE_LINKER ${TOOLCHAIN_PREFIX}g++) set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) +set(CMAKE_OBJDUMP ${TOOLCHAIN_PREFIX}objdump) set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size) set(CMAKE_EXECUTABLE_SUFFIX_ASM ".elf")