Stop RAM Offsets in Cortex‑M7 Linker Scripts with Correct Ordering

Summary

A developer attempted to insert a new memory section, .new_section, at the start of the internal RAM in a Cortex-M7 linker script. The attempt failed because unmapped library sections (specifically libname) were being implicitly pulled into the RAM region, causing an unexpected offset. The developer was also confused by the behavior of the Location Counter (.) when jumping between different memory regions (ROM to RAM).

Root Cause

The failure stems from two fundamental misunderstanding of how the GNU Linker (ld) operates:

  • Implicit Section Inclusion: When a section is not explicitly defined in the linker script, the linker doesn’t discard it. Instead, it places these “orphan” sections into the default memory region or the next available space in the current memory assignment. Because the library section libname was assigned to RAM (implicitly or via a default rule), it occupied the space before the developer’s manual .new_section definition.
  • The Location Counter vs. Memory Regions: The rule “you cannot move the location counter backwards” applies to the current section’s address space. However, the linker script is a sequence of commands that can switch between different MEMORY regions. When the script moves from > rom to > ram, the linker is not “moving the counter backwards”; it is re-initializing the location counter to the start address of the new memory region.

Why This Happens in Real Systems

  • Modular Dependencies: In complex embedded systems, drivers and libraries are often decoupled. A driver might be used in ten different projects, but only two might include a specific third-party library.
  • Linker Default Behavior: The linker is designed to be “helpful.” If it encounters a section like .libname that isn’t explicitly handled, it won’t throw an error; it will simply attempt to place it in the first available memory segment that fits, often leading to silent memory layout shifts.
  • Implicit RAM mapping: Many toolchains default unassigned data-like sections to RAM, which can cause “phantom” data to occupy the very memory addresses intended for critical structures like stacks or custom buffers.

Real-World Impact

  • Memory Corruption: If a developer assumes a section starts at 0x20000000 but a library section actually pushed it to 0x20000040, any hardcoded pointer arithmetic will cause silent data corruption.
  • Non-Deterministic Builds: Adding or removing a library dependency can change the entire memory map, making bugs impossible to reproduce across different hardware configurations.
  • Boot Failure: If the .new_section was intended for a critical structure (like a jump table or a configuration block) and its address shifts, the system may enter a HardFault loop immediately upon startup.

Example or Code

To fix this, we must use a Wildcard/Catch-all pattern or an explicitly ordered approach to ensure the “orphan” sections do not intercept our intended memory layout.

/* The WRONG way: Allows orphans to steal the start of RAM */
SECTIONS {
    .text : { *(.text*) } > rom
    .relocate : { *(.data*) } > ram
    .new_section : { *(.new_section) } > ram 
}

/* The RIGHT way: Force the new section to the absolute start, 
   then capture all other RAM content afterwards */
SECTIONS {
    .text : { *(.text*) } > rom

    /* 1. Force our specific section to the very beginning of RAM */
    .new_section : 
    {
        . = ALIGN(4);
        _snew_section = .;
        KEEP(*(.new_section))
        . = ALIGN(4);
        _enew_section = .;
    } > ram

    /* 2. Capture EVERYTHING else that belongs in RAM (including library orphans) */
    .data_and_orphans :
    {
        . = ALIGN(4);
        _sdata_all = .;
        *(.data .data.*)
        *(.ramfunc .ramfunc.*)
        *(libname) /* Explicitly handle the library if known */
        *(.*)      /* The "Catch-all" for any other undefined sections */
        . = ALIGN(4);
        _edata_all = .;
    } > ram
}

How Senior Engineers Fix It

  • The “Catch-all” Pattern: Instead of trying to predict every library section, senior engineers define a “container” section for the entire memory region. They place the high-priority sections at the top of that container and use a wildcard *(.*) at the bottom to collect all remaining orphans.
  • Explicit Memory Mapping: They avoid relying on default linker behavior. If a section is critical, its placement is strictly enforced via KEEP() and specific memory region assignments.
  • Symbol Validation: They use nm or objdump as part of the CI/CD pipeline to verify that the symbols _snew_section match the expected hardcoded address.
  • Defensive Linker Scripting: They treat the linker script as code that must be robust against changes in input files.

Why Juniors Miss It

  • The “Black Box” Fallacy: Juniors often treat the linker script as a static configuration file rather than a programmatic instruction set that reacts to the input object files.
  • Focus on Syntax over Logic: A junior sees that the script “compiles” without errors and assumes the logic is correct. They miss the semantic error of the memory layout shifting.
  • Lack of Toolchain Depth: Juniors rarely use map files or objdump to inspect the final output. They debug the code, assuming the memory map is a constant, rather than debugging the linker’s decisions.

Leave a Comment