HomeAbout UsContact Us

Linker Script Mastery: Memory Layout for Embedded Firmware

By Jithin Tom
Published in Embedded C/C++
October 24, 2026
5 min read
Linker Script Mastery: Memory Layout for Embedded Firmware

Table Of Contents

01
Problem Statement
02
Memory Layout Fundamentals
03
Section Mapping Patterns
04
Advanced Patterns
05
Debugging Linker Script Issues
06
Toolchain-Specific Notes
07
Verification Steps
08
Summary
09
Related Reading
10
References
11
Frequently Asked Questions

Problem Statement

The linker script is the single source of truth for your firmware’s memory layout — yet it’s the most misunderstood file in embedded projects. Engineers searching for “linker script .data not copied to RAM”, “firmware crashes after adding global variable”, or “how to place variable in specific memory region” hit a wall: the linker accepts invalid layouts silently, startup code assumes correct LMA/VMA mapping, and debuggers show symbols at wrong addresses.

A single misplaced > or missing AT> can cause: variables in wrong memory regions, .data not initialized at startup, stack/heap collision, or ISR vectors at wrong address. This article provides a complete reference for writing, debugging, and verifying linker scripts — with patterns that work across Cortex-M, RISC-V, and multi-core SoCs.

Why Linker Scripts Matter

Every embedded engineer eventually faces a linker script issue. The symptoms are maddening: a global variable reads back garbage, an ISR never fires, the stack overflows into the heap, or the firmware simply won’t boot. The root cause is almost always a mismatch between what the linker thinks the memory layout is and what the hardware actually provides.

+===============================================================================+
| TYPICAL LINKER SCRIPT FAILURE CHAIN |
+===============================================================================+
| |
| WRONG LINKER SCRIPT |
| | |
| v |
| +-------------+ +-------------+ +-------------+ +-------------+ |
| | .data LMA |--->| Startup |--->| .data in |--->| Global vars | |
| | != Flash | | copies from | | RAM is | | contain | |
| | address | | wrong flash | | garbage | | garbage | |
| +-------------+ +-------------+ +-------------+ +-------------+ |
| |
| OR: |
| +-------------+ +-------------+ +-------------+ +-------------+ |
| | Stack/heap |--->| No ASSERT |--->| Stack grows |--->| Corrupt | |
| | overlap | | in script | | into heap | | heap vars | |
| +-------------+ +-------------+ +-------------+ +-------------+ |
| |
| OR: |
| +-------------+ +-------------+ +-------------+ +-------------+ |
| | .isr_vector |--->| Vector at |--->| CPU jumps |--->| Hard fault | |
| | not 1st | | wrong addr | | to garbage | | on reset | |
| +-------------+ +-------------+ +-------------+ +-------------+ |
| |
| ALL PREVENTABLE with proper linker script + link-time verification |
+===============================================================================+

Memory Layout Fundamentals

The Three Address Spaces

Every embedded firmware operates in three address spaces simultaneously:

+=======================================================================+
| THREE ADDRESS SPACES IN EMBEDDED |
+=======================================================================+
| |
| BUILD TIME (Linker View) RUNTIME (CPU View) |
| +---------------------+ +---------------------+ |
| | VMA (Virtual | | Physical Addresses | |
| | Memory Address) | --MAP--> | (Flash @ 0x08... | |
| | Where code THINKS | | RAM @ 0x20...) | |
| | it lives | | | |
| +---------------------+ +---------------------+ |
| ^ ^ |
| | LOAD TIME | |
| +--------------------------------------+ |
| LMA (Load Memory Address) |
| Where data is STORED in Flash |
| |
| KEY INSIGHT: .text/.rodata: LMA == VMA (Execute-in-Place from Flash) |
| .data: LMA != VMA (Copied Flash -> RAM at boot) |
| .bss: No LMA (Zero-initialized in RAM at boot) |
| |
+=======================================================================+

Critical distinction: The linker resolves symbols at VMA. The flash programmer writes at LMA. Startup code copies LMA -> VMA for .data. If these diverge -> silent corruption.

Memory Region Definition

/* STM32F407 example: 1 MB Flash, 192 KB RAM (+ 64 KB CCM RAM) */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
CCM_RAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
BKPSRAM (xrw) : ORIGIN = 0x40024000, LENGTH = 4K
}
/* Stack/heap symbols for startup code */
_stack_size = 0x2000;
_heap_size = 0x1000;

Rule: Define ALL memory regions upfront. Every output section must map to a region with >REGION. The linker will error if sections overflow.

Region Attributes Explained

The attribute string in MEMORY definitions controls section permissions in the generated ELF:

AttributeMeaningTypical Use
rReadableFlash, ROM, .text, .rodata
wWritableRAM, .data, .bss
xExecutableFlash, .text
aAllocatableAll loadable sections
i / lInitialized.data (has LMA content)
!Invert!w = read-only

For Cortex-M, the standard combination is FLASH (rx) and RAM (xrw). The x on RAM enables execute-from-RAM (needed for bootloaders, OTA staging).

Section Mapping Patterns

Memory Map Architecture

The interaction between Flash (non-volatile storage) and RAM (runtime execution space) in an ARM Cortex-M system is visualized below:

+===============================================================================+
| FLASH MEMORY (ROM) RAM (SRAM) |
| Non-Volatile Storage (LMA) Runtime Execution (VMA) |
+===============================================================================+
| |
| 0x08000000 -> +---------------------+ |
| | .isr_vector (VTOR) | |
| +---------------------+ |
| | .text (Executable) | |
| +---------------------+ |
| | .rodata (Constants) | |
| +---------------------+ 0x20000000 -> +---------------------+ |
| _sidata ----> | .data image (LMA) | --- Copy ---> | .data (Read/Write) | |
| | (Initial values) | at startup | (_sdata to _edata) | |
| +---------------------+ +---------------------+ |
| | | | .bss (Zero-filled) | |
| | (Unused Flash) | | (_sbss to _ebss) | |
| | | +---------------------+ |
| | | | .heap (malloc) | |
| | | | (_heap_start -> up) | |
| | | +---------------------+ |
| | | | | |
| | | | ^ Stack/Heap Gap | |
| | | | v (Safety Margin) | |
| | | | | |
| | | +---------------------+ |
| | | | .stack (MSP limit) | |
| | | | (_stack_bottom) | |
| | | | | | |
| | | | v | |
| 0x08100000 -> +---------------------+ 0x20030000 -> +---------------------+ |
| (End of 1MB Flash) | _stack_top (End RAM)| |
| +---------------------+ |
+===============================================================================+

Standard Section Layout (Cortex-M)

SECTIONS
{
/* 1. Vector table -- MUST BE FIRST, at FLASH origin with power-of-two alignment */
.isr_vector :
{
. = ALIGN(128); /* Cortex-M VTOR alignment requirement */
KEEP(*(.isr_vector)) /* Prevent linker GC (--gc-sections) removal */
. = ALIGN(4);
} >FLASH
/* 2. Code and read-only data (execute-in-place) */
.text :
{
. = ALIGN(4);
*(.text) /* Standard .text input sections */
*(.text*) /* Subsections generated by -ffunction-sections */
*(.rodata) /* Read-only constants */
*(.rodata*) /* Subsections generated by -fdata-sections */
*(.glue_7) *(.glue_7t) /* ARM/Thumb interworking veneers (if applicable) */
*(.eh_frame*) /* Exception unwinding frames */
KEEP(*(.init))
KEEP(*(.fini))
. = ALIGN(4);
_etext = .; /* End of code symbol */
} >FLASH
/* 3. Initialized data -- LMA in Flash, VMA in RAM */
_sidata = LOADADDR(.data); /* Export absolute Flash address for C startup copy */
.data :
{
. = ALIGN(4);
_sdata = .; /* VMA start in RAM */
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .; /* VMA end in RAM */
} >RAM AT>FLASH /* VMA=RAM, LMA=FLASH */
/* 4. Zero-initialized data -- VMA only in RAM */
.bss :
{
. = ALIGN(4);
_sbss = .; /* VMA start in RAM */
*(.bss)
*(.bss*)
*(COMMON) /* Uninitialized global variables */
. = ALIGN(4);
_ebss = .; /* VMA end in RAM */
} >RAM
/* 5. Dynamic heap -- allocated after .bss, grows upwards (newlib _sbrk) */
.heap (NOLOAD) :
{
. = ALIGN(8);
PROVIDE(_heap_start = .);
PROVIDE(end = .); /* Required by standard C libraries (newlib) */
PROVIDE(_end = .);
. = . + _heap_size;
. = ALIGN(8);
PROVIDE(_heap_end = .);
} >RAM
/* 6. User Stack reservation -- allocated at the top of RAM (grows downwards) */
.stack (NOLOAD) :
{
. = ALIGN(8);
_stack_bottom = .; /* Lowest valid stack address (stack guard) */
. = . + _stack_size;
. = ALIGN(8);
_stack_top = .; /* Top of stack (initial MSP loaded on reset) */
} >RAM
/* 7. Discard debugging and ABI sections from release image */
/DISCARD/ :
{
*(.note.gnu.build-id)
*(.note.ABI-tag)
*(.comment)
}
}

Startup Code Copy Routine (Required for .data)

In C, symbols defined in a linker script do not allocate storage variables; their addresses represent the location counter values established by the linker. Always use the address-of operator (&_sdata) or declare them as array types:

/* startup.c -- called during Reset_Handler before main() */
#include <stdint.h>
void CopyDataInit(void)
{
/* Linker script symbols */
extern uint32_t _sidata; /* LMA of .data in Flash */
extern uint32_t _sdata; /* VMA start of .data in RAM */
extern uint32_t _edata; /* VMA end of .data in RAM */
uint32_t *p_src = &_sidata;
uint32_t *p_dst = &_sdata;
while (p_dst < &_edata) {
*p_dst++ = *p_src++;
}
}
void ZeroBSS(void)
{
extern uint32_t _sbss; /* VMA start of .bss in RAM */
extern uint32_t _ebss; /* VMA end of .bss in RAM */
uint32_t *p_dst = &_sbss;
while (p_dst < &_ebss) {
*p_dst++ = 0U;
}
}

[!IMPORTANT] GNU ld Does Not Automatically Generate _sidata A common misconception is that the GNU linker automatically synthesizes _sidata. It does not. The symbol MUST be explicitly exported in your linker script using _sidata = LOADADDR(.data);. Without this definition, the linker emits an undefined reference to '_sidata' error when compiling startup code.

Advanced Patterns

Custom Section Placement (CCM RAM, Backup SRAM, etc.)

Specialized embedded microcontrollers feature distinct memory regions with unique physical interfaces, such as Core-Coupled Memory (CCM) or battery-backed Backup SRAM:

/* STM32F407: Flash, main SRAM, CCM RAM, and Backup SRAM */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
CCM_RAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K /* Core-Coupled Memory */
BKPSRAM (xrw) : ORIGIN = 0x40024000, LENGTH = 4K /* Backup SRAM (VBAT) */
}
SECTIONS
{
/* ... standard .text, .rodata, .data, .bss ... */
/* Initialized CCM data (copied from Flash at boot) */
_siccm_data = LOADADDR(.ccm_data);
.ccm_data :
{
. = ALIGN(4);
_sccm_data = .;
*(.ccm_data*)
. = ALIGN(4);
_eccm_data = .;
} >CCM_RAM AT>FLASH
/* Uninitialized CCM BSS (zero-filled at boot) */
.ccm_bss (NOLOAD) :
{
. = ALIGN(4);
_sccm_bss = .;
*(.ccm_bss*)
. = ALIGN(4);
_eccm_bss = .;
} >CCM_RAM
/* Initialized Backup SRAM data (retained during VBAT mode) */
_sibkpsram_data = LOADADDR(.bkpsram_data);
.bkpsram_data :
{
. = ALIGN(4);
_sbkpsram_data = .;
*(.bkpsram_data*)
. = ALIGN(4);
_ebkpsram_data = .;
} >BKPSRAM AT>FLASH
.bkpsram_bss (NOLOAD) :
{
. = ALIGN(4);
_sbkpsram_bss = .;
*(.bkpsram_bss*)
. = ALIGN(4);
_ebkpsram_bss = .;
} >BKPSRAM
}

[!WARNING] Hardware Architecture Pitfall: CCM RAM and DMA on STM32F4 On STM32F4 devices, CCM RAM (0x10000000) is wired exclusively to the Cortex-M4 D-Code and I-Code buses. It is completely isolated from the multi-AHB bus matrix. Because DMA controllers reside on the AHB matrix, DMA controllers cannot access CCM RAM. Attempting to execute DMA transfers to or from CCM RAM will trigger a bus transfer error (TEIF flag) and fail silently. Reserve CCM RAM strictly for CPU-intensive DSP math, FIR/IIR filters, fast control loops, or dedicated ISR stacks. Allocate all DMA buffers in general SRAM (0x20000000).

Usage in C:

/* High-throughput FFT workspace in CCM RAM (0-wait-state CPU access up to 168 MHz) */
__attribute__((section(".ccm_bss"), aligned(4)))
uint32_t fft_workspace[2048];
/* Time-critical PID control loop states in CCM RAM */
__attribute__((section(".ccm_data"), aligned(4)))
volatile struct pid_controller_t fast_loop = { .kp = 1.25f, .ki = 0.05f, .kd = 0.01f };
/* Persistent calibration parameters retained across system resets in Backup SRAM */
__attribute__((section(".bkpsram_data"), aligned(4)))
struct sensor_calibration_t saved_cal = { .magic = 0xDEADBEEF, .version = 1 };
/* Startup copy routine for custom initialized regions */
void CopyCustomSections(void)
{
extern uint32_t _siccm_data, _sccm_data, _eccm_data, _sccm_bss, _eccm_bss;
uint32_t *p_src = &_siccm_data;
uint32_t *p_dst = &_sccm_data;
while (p_dst < &_eccm_data) {
*p_dst++ = *p_src++;
}
p_dst = &_sccm_bss;
while (p_dst < &_eccm_bss) {
*p_dst++ = 0U;
}
}

Overlay Sections (Code Banking for Memory-Constrained Devices)

In applications where multiple large algorithmic routines exceed available RAM execution space, GNU ld’s OVERLAY mechanism allows multiple sections to share the same runtime VMA in RAM while being stored contiguously at different LMAs in Flash:

/* Overlay: multiple code banks share execution VMA in RAM, loaded sequentially in FLASH */
OVERLAY 0x20010000 : NOCROSSREFS AT (LOADADDR(.data) + SIZEOF(.data))
{
.ovly_bank0 { *(.ovly_bank0*) }
.ovly_bank1 { *(.ovly_bank1*) }
.ovly_bank2 { *(.ovly_bank2) }
} >RAM AT>FLASH

GNU ld automatically defines load symbols for each overlay bank:

  • __load_start_ovly_bank0 and __load_stop_ovly_bank0
  • __load_start_ovly_bank1 and __load_stop_ovly_bank1
#include <string.h>
extern uint8_t __load_start_ovly_bank0[], __load_stop_ovly_bank0[];
extern uint8_t __load_start_ovly_bank1[], __load_stop_ovly_bank1[];
#define OVERLAY_RAM_EXEC_ADDR ((void *)0x20010000)
void load_overlay(uint8_t bank_id)
{
uint8_t *load_start;
size_t size;
if (bank_id == 0) {
load_start = __load_start_ovly_bank0;
size = (size_t)(__load_stop_ovly_bank0 - __load_start_ovly_bank0);
} else {
load_start = __load_start_ovly_bank1;
size = (size_t)(__load_stop_ovly_bank1 - __load_start_ovly_bank1);
}
memcpy(OVERLAY_RAM_EXEC_ADDR, load_start, size);
__asm__ volatile ("dsb 0xF" ::: "memory");
__asm__ volatile ("isb 0xF" ::: "memory"); /* Pipeline flush for newly staged code */
}

Multi-Core Memory Partitioning (Asymmetric Multiprocessing - AMP)

In heterogeneous dual-core microcontrollers (e.g., STM32H7 with Cortex-M7 and Cortex-M4), shared RAM regions require strict cache coherency policies:

/* Dual-core STM32H7 (CM7 @ 480 MHz, CM4 @ 240 MHz) */
MEMORY
{
FLASH_CM7 (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
FLASH_CM4 (rx) : ORIGIN = 0x08100000, LENGTH = 1024K
RAM_CM7 (xrw) : ORIGIN = 0x20000000, LENGTH = 128K /* DTCM */
RAM_CM4 (xrw) : ORIGIN = 0x10000000, LENGTH = 128K /* SRAM1/2 */
SHARED_RAM (rw) : ORIGIN = 0x38000000, LENGTH = 64K /* SRAM4 (Non-cacheable) */
}
SECTIONS
{
.shared_data (NOLOAD) :
{
. = ALIGN(32);
*(.shared_data*)
. = ALIGN(32);
} >SHARED_RAM
}

[!NOTE] Cache Coherency Notice: Because the Cortex-M7 features L1 data caching, any access to SHARED_RAM must be marked as Non-Cacheable using the Cortex-M MPU (Memory Protection Unit) or explicitly maintained using CMSIS functions SCB_CleanDCache_by_Addr() and SCB_InvalidateDCache_by_Addr() before and after inter-core IPC transactions.

Bootloader-Specific Partitioning

When designing firmware with an in-field bootloader and an application payload, Flash must be partitioned into two distinct images:

/* Application Linker Script (Offset past 32KB Bootloader) */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 992K /* Application Flash */
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
}
SECTIONS
{
/* Application vector table placed at offset 0x08008000 */
.isr_vector :
{
. = ALIGN(128);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
.text :
{
*(.text*)
*(.rodata*)
. = ALIGN(4);
} >FLASH
/* Standard .data, .bss, .heap, and .stack follow */
}

Key Bootloader Runtime Rules:

  1. Vector Table Relocation: During application startup in SystemInit(), update the Vector Table Offset Register: SCB->VTOR = 0x08008000;.
  2. De-initialize Peripherals: Before branching to the application, the bootloader must disable all active interrupts (NVIC->ICER[x]), clear pending interrupts (NVIC->ICPR[x]), and reset the Main Stack Pointer (__set_MSP(*(uint32_t *)0x08008000)).
  3. RAM Segregation: If the bootloader passes execution parameters to the application in RAM, designate a dedicated (NOLOAD) communication mailbox section at a fixed RAM origin that neither runtime’s .bss clears.

Symbol Export and Weak Symbols

/* Provide default values if not defined in object files */
PROVIDE(g_heap_guard = 0xDEADBEEF);
PROVIDE_HIDDEN(_system_clock_hz = 168000000);

Debugging Linker Script Issues

Common Failure Modes

SymptomLikely CauseFix
Crash on startup.isr_vector not at ORIGIN(FLASH)Make .isr_vector first section, >FLASH
Globals uninitialized.data missing AT>FLASHAdd >RAM AT>FLASH
Variables at wrong addressLMA/VMA mismatchCheck objdump -h vs linker map
Stack overflow_stack_size too small / heap collisionIncrease stack, add ASSERT
Linker “region overflow”Section too large for MEMORYReduce section or increase region
ISR handler missingKEEP() missing on .isr_vectorAdd KEEP(*(.isr_vector))

Real-World Debugging Session: The Phantom Variable

+===============================================================================+
| DEBUGGING: GLOBAL VARIABLE AT WRONG ADDRESS |
+===============================================================================+
| |
| SYMPTOM: |
| struct config_t cfg = { .baud = 115200 }; // In .data |
| printf("%d", cfg.baud); // Prints 0 or garbage |
| |
| STEP 1: Check .data LMA vs VMA |
| $ arm-none-eabi-objdump -h firmware.elf | grep '\.data' |
| Idx Name Size VMA LMA File off Algn |
| 8 .data 0080 20000100 0800C000 0000D000 2**2 |
| [PASS] LMA (0x0800C000) != VMA (0x20000100) -- copy required |
| |
| STEP 2: Verify startup code uses _sidata |
| $ arm-none-eabi-nm firmware.elf | grep _sidata |
| 0800C000 D _sidata |
| [PASS] Symbol exists at correct Flash LMA |
| |
| STEP 3: Check CopyDataInit() implementation |
| Found: src = &_sidata; dst = &_sdata; // CORRECT |
| |
| STEP 4: Check .data section alignment |
| Linker script: .data : { . = ALIGN(4); ... } >RAM AT>FLASH |
| [PASS] 4-byte word aligned |
| |
| STEP 5: Check for overlapping sections |
| $ arm-none-eabi-objdump -h firmware.elf |
| .text 08000000 - 0800BFFF |
| .data 0800C000 - 0800C07F (LMA) |
| .rodata 0800C080 - 0800D120 |
| [PASS] No overlapping address space |
| |
| STEP 6: Hardware check -- is flash actually programmed? |
| $ openocd -c "program firmware.elf verify" |
| FAIL: verification mismatch at 0x0800C000 |
| |
| ROOT CAUSE: Flash sector not erased before write! |
| The .data LMA fell in a sector that was dirty. |
| |
| FIX: Ensure flash programmer performs sector or mass erase on upload |
| |
+===============================================================================+
# 1. Check section addresses and sizes
arm-none-eabi-objdump -h firmware.elf | grep -E '\.text|\.data|\.bss|\.isr_vector'
# 2. Verify LMA vs VMA for .data
arm-none-eabi-objdump -h firmware.elf | grep '\.data'
# Should show: VMA=0x200xxxxx LMA=0x080xxxxx (different!)
# 3. Full linker map (search for region overflows)
arm-none-eabi-ld -Map=firmware.map ...
grep -i overflow firmware.map
# 4. Check symbol placement for C runtime startup
arm-none-eabi-nm firmware.elf | grep -E '_sdata|_edata|_sbss|_ebss|_sidata'
# 5. Verify vector table at Flash origin (0x08000000)
arm-none-eabi-objdump -s -j .isr_vector firmware.elf
# 6. Check discarded unused functions and sections (dead code elimination)
# Pass -Wl,--print-gc-sections during build to inspect stripped sections:
arm-none-eabi-gcc ... -Wl,--gc-sections -Wl,--print-gc-sections 2>&1 | grep "removing unused section"
# 7. Check section alignments and sizes across all segments
arm-none-eabi-objdump -h firmware.elf
/* Prevent stack/heap collision in RAM */
ASSERT(_heap_end <= _stack_bottom,
"HEAP/STACK COLLISION: Heap overlaps with reserved user stack in RAM")
/* Ensure total static RAM allocations fit within physical RAM boundaries */
ASSERT(_stack_top <= ORIGIN(RAM) + LENGTH(RAM),
"RAM OVERFLOW: Combined .data + .bss + heap + stack exceed RAM length")
/* Ensure vector table resides precisely at Flash origin */
ASSERT(ADDR(.isr_vector) == ORIGIN(FLASH),
"VECTOR TABLE NOT AT FLASH ORIGIN: Cortex-M boot will fail")
/* Verify custom memory region capacity */
ASSERT(SIZEOF(.ccm_data) + SIZEOF(.ccm_bss) <= LENGTH(CCM_RAM),
"CCM RAM OVERFLOW: Custom data exceeds 64KB boundary")
/* Flash usage sanity check (warn if firmware exceeds 90% budget) */
ASSERT((_etext - ORIGIN(FLASH)) + SIZEOF(.data) <= (LENGTH(FLASH) * 9) / 10,
"FLASH USAGE WARNING: Firmware image exceeds 90% of total Flash")

Toolchain-Specific Notes

GCC/Clang (arm-none-eabi-ld, riscv64-unknown-elf-ld)

  • Use --gc-sections together with -ffunction-sections -fdata-sections for maximum dead code stripping.
  • ENTRY(Reset_Handler) must match your startup code entry symbol.
  • --print-memory-usage displays a console overview of region capacity and percentage utilized.
  • -Wl,--cref appends a cross-reference table to the generated map file.

IAR (ilinkarm)

/* IAR equivalent -- .icf file */
define memory mem with size = 1M;
define region FLASH = mem:[from 0x08000000 size 1M];
define region RAM = mem:[from 0x20000000 size 192K];
initialize by copy { section .data };
do not initialize { section .bss };
place in FLASH { readonly section .isr_vector, section .text, section .rodata };
place in RAM { section .data, section .bss };

LLVM LLD (ld.lld)

  • Invocation in embedded bare-metal toolchains: clang --target=arm-none-eabi -fuse-ld=lld -T script.ld.
  • LLD supports GNU ld script syntax including MEMORY, SECTIONS, AT>, LOADADDR(), and PROVIDE().
  • --icf=safe enables conservative Identical Code Folding, collapsing duplicated read-only code while guaranteeing distinct function pointer addresses.
  • LLD does not parse in-script VERSION {} blocks; symbol versioning must be passed via the --version-script=<file> command-line flag.
; Keil scatter file equivalent
LR_FLASH 0x08000000 0x00100000 {
ER_IROM 0x08000000 0x00100000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM 0x20000000 0x00030000 {
.ANY (+RW +ZI)
}
}

Migration: GNU ld -> LLD / LLD -> GNU ld

GNU ld FeatureLLD EquivalentNotes
ASSERT(expr, msg)SameFully compatible
PROVIDE(sym = val)SameFully compatible
KEEP(*(.sec))SameFully compatible
>REGION AT>REGIONSameFully compatible
OVERLAYPartialPrefer explicit section definitions in LLD
VERSION {}--version-scriptLLD requires dedicated version file
INPUT(file.a)SameFully compatible
GROUP(file1, file2)SameFully compatible

Verification Steps

  1. Link-time assertions pass — no ASSERT failures during compilation.
  2. Map file clean — zero region overflowed errors.
  3. Vector table at FLASH origin — objdump -j .isr_vector confirms base address 0x08000000.
  4. .data LMA != VMA — confirms Flash storage address differs from runtime RAM address.
  5. Stack/heap symbols reasonable — _stack_top at RAM top, _stack_bottom >= _heap_end.
  6. Custom sections in correct regions — verify addresses with arm-none-eabi-nm.
  7. Runtime validation — startup code successfully copies initialized data and clears .bss.

Summary

The linker script is the memory contract between your build system, startup code, and hardware. Master these patterns:

  • MEMORY regions first — declare every physical region with accurate base addresses and sizes.
  • LMA vs VMA always explicit — use >RAM AT>FLASH for .data and export _sidata = LOADADDR(.data);.
  • KEEP() on vectors — protect hardware exception tables from linker garbage collection.
  • Startup symbol export — export _sdata, _edata, _sbss, _ebss, _sidata, and _stack_top.
  • Assertions as guardrails — catch stack/heap collision and memory overflow at link time.
  • Respect bus architecture — remember that CCM RAM is isolated from DMA on STM32F4.
  • Verify with objdump/map — automate verification in continuous integration pipelines.

Quick Reference Card

TaskLinker Script Syntax
Define memory regionMEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1M }
Section in RAM, LMA in Flash.data : { *(.data*) } >RAM AT>FLASH
Export section Flash LMA_sidata = LOADADDR(.data);
Section in RAM only.bss : { *(.bss*) } >RAM
Keep section from GCKEEP(*(.isr_vector))
Provide symbolPROVIDE(_stack_top = ORIGIN(RAM) + LENGTH(RAM));
Assert conditionASSERT(_heap_end <= _stack_bottom, "Stack/heap collision");
Align address. = ALIGN(4);
Discard sections/DISCARD/ : { *(.note*) }

Linker Script Review Checklist (PR Template)

When reviewing linker script changes, verify:

  • All MEMORY regions match datasheet (ORIGIN, LENGTH, attributes)
  • .isr_vector is first section, mapped to ORIGIN(FLASH) with power-of-two alignment
  • .data has AT>FLASH and exports _sidata = LOADADDR(.data);
  • .bss has no LMA (NOLOAD implied)
  • KEEP() on .isr_vector and hardware-referenced tables
  • Stack/heap symbols defined and exported (PROVIDE or direct assignment)
  • ASSERT statements for stack/heap collision, RAM overflow, and Flash capacity
  • Custom sections (CCM, BKPSRAM) have correct attributes, export load addresses, and follow DMA bus rules
  • No hardcoded addresses — all derived from ORIGIN()/LENGTH()
  • Map file reviewed for section allocations and region utilization
  • objdump -h confirms LMA/VMA as expected

Common Anti-Patterns to Avoid

Anti-PatternWhy It’s WrongCorrect Approach
Hardcoded _sidata = 0x0800C000Breaks silently when code grows or sections shiftExport _sidata = LOADADDR(.data); in linker script
.data >RAM (no AT>)LMA = VMA — Flash image contains no initialization copy.data : { ... } >RAM AT>FLASH
.isr_vector not firstVector table misaligned; CPU jumps to arbitrary memory on resetPlace .isr_vector first in SECTIONS, mapped to >FLASH
No KEEP() on vectors--gc-sections strips interrupt vector table as unused codeDecorate section with KEEP(*(.isr_vector))
No stack/heap ASSERTSilent heap and stack memory corruption at runtimeAdd ASSERT(_heap_end <= _stack_bottom, ...)
Placing DMA buffers in CCM RAMCCM RAM does not connect to AHB bus matrix on STM32F4; DMA failsPlace DMA buffers in general SRAM (0x20000000)
Single MEMORY region for allPrevents selective placement and MPU permission enforcementDefine separate regions for Flash, SRAM, CCM, and backup RAM

References

  1. GNU Project. Using LD: The GNU Linker, Binutils 2.43. https://sourceware.org/binutils/docs/ld/
  2. ARM Limited. Arm Cortex-M Generic User Guide & Linker Script Reference, Arm Developer Docs. https://developer.arm.com/documentation/
  3. STMicroelectronics. STM32CubeIDE User Guide: Linker Scripts and Memory Mapping, UM2609, Rev 2. https://www.st.com/resource/en/user_manual/um2609-stm32cubeide-user-guide-stmicroelectronics.pdf
  4. STMicroelectronics. STM32F405/415, STM32F407/417 Reference Manual: System and Memory Architecture, RM0090, Rev 19. https://www.st.com/resource/en/reference_manual/rm0090-stm32f405415-stm32f407417-stm32f427437-and-stm32f429439-advanced-armbased-32bit-mcus-stmicroelectronics.pdf
  5. FreeRTOS. Memory Management and Linker Configuration, Kernel V11.1.0. https://www.freertos.org/a00111.html
  6. twilco. RISC-V from Scratch: Hardware Layouts, Linker Scripts, and C Runtimes, 2019. https://twilco.github.io/riscv-from-scratch/2019/04/27/riscv-from-scratch-2.html
  7. Zephyr Project. Build System (CMake) — Linker Scripts and Memory Management, v4.0.0. https://docs.zephyrproject.org/latest/build/cmake/linker_scripts.html

Frequently Asked Questions

Why does my firmware crash when I add a new global variable?

Adding a global or static variable expands either the `.data` section (if initialized) or `.bss` section (if uninitialized/zero). In typical bare-metal memory architectures where stack and heap share the unallocated RAM following `.bss`, an expanding data segment pushes heap allocation higher or consumes stack margin, triggering silent runtime stack-heap collisions. Use link-time `ASSERT` boundary checks to catch RAM exhaustion during compilation.

What's the difference between LMA and VMA, and why does .data need both?

LMA (Load Memory Address) is the address where data is physically stored in non-volatile Flash at burn time. VMA (Virtual/Execution Memory Address) is the runtime memory address where the CPU accesses variables in RAM. `.data` requires both: non-volatile retention in Flash (LMA) across resets, and writable read/write execution in RAM (VMA). The C runtime startup code copies this payload from LMA to VMA before `main()` executes.

How do I place a specific variable in a custom memory region (e.g., CCM RAM or backup SRAM)?

Declare a distinct `MEMORY` region in your linker script, construct a designated output section (e.g., `.ccm_data`), map it with `>CCM_RAM AT>FLASH` for initialized data, and export its load address using `_siccm_data = LOADADDR(.ccm_data);`. In C source code, decorate the variable with `__attribute__((section(".ccm_data")))`. Your reset handler must copy the data from Flash to CCM RAM during boot.

My ISR vector table isn't at address 0x08000000 -- what's wrong?

On ARM Cortex-M, the vector table must reside at the very base of Flash (mapped to `0x00000000` on reset, or `0x08000000` on STM32) and be aligned to a power-of-two boundary (at least 128 bytes). Ensure `.isr_vector` is the first output section in `SECTIONS`, mapped to `>FLASH`, and protected from dead-code stripping via `KEEP(*(.isr_vector))`.

How do I prevent the linker from optimizing away my interrupt handlers?

Protect the vector table using `KEEP(*(.isr_vector))` within the linker script, and ensure ISR functions are marked with `__attribute__((used))` in C source files. Because interrupt vectors are referenced by hardware exception architecture rather than software call graphs, linker garbage collection (`--gc-sections`) will silently discard them without explicit retention directives.

Tags

linker-scriptmemory-layoutembedded-cfirmwaregnu-ldmemory-map

Share


Previous Article
Debugging Bootloader Verify Failures in Field Deployments
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Fixing UART Overrun Errors on NXP i.MX RT with FlexIO
Fixing UART Overrun Errors on NXP i.MX RT with FlexIO
October 13, 2026
5 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media