Domain Deep-Dive
AdvancedLinker Scripts for Embedded C++
Writing and understanding linker scripts for bare-metal C++: memory regions, section placement, startup initialization, and common pitfalls.
Why linker scripts matter
On a hosted system (PC/server), the OS provides a flat virtual address space and the loader handles placement. On bare-metal, you must tell the linker exactly where every section goes. Flash, RAM, and I/O peripherals have fixed physical addresses burned into silicon.
Memory layout of a typical Cortex-M
cpp
Physical address map (STM32F4 as example):
0x0800_0000 - 0x080F_FFFF FLASH (1 MB) — read-only after power-on
0x2000_0000 - 0x2001_FFFF SRAM (128 KB) — read/write
Typical section placement:
FLASH: SRAM:
┌──────────────┐ 0x0800_0000 ┌──────────────┐ 0x2000_0000
│ .vectors │ │ .data │ (copied from FLASH)
│ .text │ │ .bss │ (zeroed by startup)
│ .rodata │ │ .heap │ ↑ grows up
│ .data (load) │ │ │
└──────────────┘ │ .stack │ ↓ grows down
└──────────────┘Minimal linker script
/* STM32F407 example */
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1M
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* Stack top = end of RAM */
_stack_top = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS {
/* Vector table MUST be at offset 0 in FLASH */
.vectors : {
KEEP(*(.vectors))
} > FLASH
/* Executable code and read-only data */
.text : {
*(.text*)
*(.rodata*)
} > FLASH
/* .data section: stored in FLASH, copied to RAM at startup */
.data : {
_data_start = .;
*(.data*)
_data_end = .;
} > RAM AT > FLASH /* VMA in RAM, LMA in FLASH */
/* Load address of .data in FLASH */
_data_load = LOADADDR(.data);
/* .bss section: zero-initialized; only in RAM */
.bss : {
_bss_start = .;
*(.bss*)
*(COMMON)
_bss_end = .;
} > RAM
/* Heap starts after .bss */
_heap_start = _bss_end;
_heap_end = _stack_top - 1024; /* leave 1KB for minimum stack */
}Startup: copying .data and zeroing .bss
The linker provides the _data_* / _bss_* symbols but your startup code must use them:
cpp
// startup.cpp — runs before main()
extern uint32_t _data_start;
extern uint32_t _data_end;
extern uint32_t _data_load; // source address in FLASH
extern uint32_t _bss_start;
extern uint32_t _bss_end;
extern uint32_t _stack_top;
extern "C" void Reset_Handler() {
// 1. Copy .data from FLASH to RAM
uint32_t* src = &_data_load;
uint32_t* dst = &_data_start;
while (dst < &_data_end) *dst++ = *src++;
// 2. Zero .bss
dst = &_bss_start;
while (dst < &_bss_end) *dst++ = 0;
// 3. Call static constructors (C++ global objects)
extern void (*__preinit_array_start[])();
extern void (*__preinit_array_end[])();
for (auto** fn = __preinit_array_start; fn < __preinit_array_end; ++fn)
(*fn)();
extern void (*__init_array_start[])();
extern void (*__init_array_end[])();
for (auto** fn = __init_array_start; fn < __init_array_end; ++fn)
(*fn)();
// 4. Jump to main
main();
// Should never return from main on bare-metal
while (true) { __WFI(); }
}Placing specific code in specific sections
cpp
// Force a function into FLASH (default for most compilers)
__attribute__((section(".text.fast"))) void CriticalISR() { /* ... */ }
// Place time-critical code in RAM for faster execution
__attribute__((section(".ramfunc"))) void UltraFastFunc() { /* ... */ }
// Then in linker script:
// .ramfunc : { *(.ramfunc) } > RAM AT > FLASH
// Place a variable in a specific address (e.g., at SRAM2)
__attribute__((section(".sram2"), used)) uint8_t dma_buffer[4096];
// In linker script:
// MEMORY { SRAM2 (rw): ORIGIN = 0x10000000, LENGTH = 64K }
// .sram2 : { *(.sram2) } > SRAM2
// Non-initialized section (survives reset — used for watchdog crash logs)
__attribute__((section(".noinit"))) char crash_log[256];
// .noinit (NOLOAD) : { *(.noinit) } > RAM ← NOLOAD = don't init/zeroChecking your memory map
bash
# Generate a map file
arm-none-eabi-gcc ... -Wl,-Map=output.map
# Check section sizes
arm-none-eabi-size firmware.elf
# Typical output:
# text data bss dec hex filename
# 45320 1432 8192 54944 d6a0 firmware.elf
# text = FLASH used by .text + .rodata + .data (load copy)
# data = RAM used by .data
# bss = RAM used by .bssCommon rules:
data + bss + stack < RAM_SIZE— or you'll corrupt the stacktext + data_load < FLASH_SIZE— or you won't fit- Stack grows down from top of RAM — verify with
_stack_top = ORIGIN(RAM) + LENGTH(RAM)
Common linker script mistakes
cpp
1. Forgetting KEEP() on the vector table
→ linker optimizes it away → hard fault on first interrupt
2. No AT > FLASH for .data
→ data section has no ROM image → garbage on first access
3. Missing __init_array calls in startup
→ global C++ objects never constructed → undefined behavior
4. Overlapping RAM sections
→ stack corrupts .bss or vice versa
→ use -Wl,--check-sections to catch this
5. NOLOAD missing from .noinit
→ startup zeros the crash log → defeats the purposeEdit on GitHubUpdated 2026-05-24T00:00:00.000Z