Memory Map, Linker Scripts & Build Process
Where does your code actually go? Learn the compile–assemble–link flow, object files and ELF, the MCU memory map (Flash, SRAM, peripherals), sections (.text, .rodata, .data, .bss, stack, heap), what the linker script does, how startup code initialises RAM, reading map files and size output, and stack sizing and overflow detection.
On a PC, the operating system loads your program anywhere in virtual memory. On a microcontroller there is no loader: the linker decides the exact address of every function and variable, and the image is burned into Flash at fixed addresses.
Understanding sections and the linker script lets you answer practical questions: Why did RAM usage grow when I initialised an array? Where is my stack? Why does a variable keep its value after reset? How do I place a function in RAM for faster execution?
An analogy
The linker is an architect assigning rooms in a building: code goes to the read-only library (Flash), variables to offices (RAM), and the stack gets a corridor that grows downward — the architect must make sure nothing overlaps.
From source to binary
The toolchain runs four stages:
- Preprocessor: expands #include and #define → .i
- Compiler: C → assembly/object code per file → .o
- Linker: combines .o files and libraries, assigns addresses using the linker script → .elf
- objcopy: .elf → raw .bin or .hex for flashing; .map lists every symbol's address and size
Which section does my variable land in?
The declaration decides:
- const uint16_t table[256] = {…}; → .rodata (Flash only)
- uint32_t counter = 5; → .data (Flash copy + RAM)
- static uint8_t buf[512]; → .bss (RAM, zeroed)
- Local variables → stack; malloc() → heap (avoid in most firmware)
Stack and heap
The stack grows downward from _estack; every function call pushes a frame (return address, saved registers, locals). Recursion and large local arrays blow the stack silently, corrupting .bss/.data below it. Fill the stack with a pattern (0xDEADBEEF) at boot and measure the 'high-water mark', use the MPU or stack limit registers (Cortex-M33 MSPLIM) to trap overflow, and use -fstack-usage for static estimates.
Advanced placement
Custom sections place code in fast RAM (__attribute__((section(".ramfunc")))), put DMA buffers in specific SRAM banks, reserve a no-init area that survives soft reset (for crash logs), or split Flash into bootloader and application regions. Link-time optimisation (-flto), -ffunction-sections with --gc-sections, and -Os shrink images substantially.
Formulas
- Flash usage:
Flash = .text + .rodata + .data (initial values) + vector table - RAM usage (static):
RAM = .data + .bss + stack + heap - From 'arm-none-eabi-size':
Flash = text + data; RAM = data + bss - Stack estimate:
Stack ≥ deepest call chain frames + ISR frames (nested) + margin (≈ 25%) - Exception frame (Cortex-M):
8 words = 32 B (104 B with lazy FPU context)
Worked example: Reading size output
Given: arm-none-eabi-size: text 12345, data 256, bss 2048
Find: Flash and static RAM.
- Flash = text + data = 12 601 B
- RAM = data + bss = 2304 B
Answer: 12.3 KB Flash, 2.25 KB RAM (+ stack/heap)
Worked example: Moving a table to Flash
Given: uint16_t sine[256] = {…}; (not const)
Find: Effect of adding const.
- Without const: 512 B Flash + 512 B RAM (.data)
- With const: 512 B Flash (.rodata) only
Answer: Saves 512 B of RAM
Worked example: Stack budget
Given: Deepest call chain 640 B; Two nested ISRs using 96 B each incl. frames
Find: Stack size with 25% margin.
- 640 + 2×96 = 832 B
- × 1.25 = 1040 B
Answer: Reserve ≥ 1 KB (round up to 1.5–2 KB)
A minimal GNU linker script (STM32-class MCU)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /* stack starts at top of RAM */
_Min_Stack_Size = 0x1000; /* 4 KB reserved and checked */
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH /* vector table first */
.text : { *(.text*) *(.rodata*) } > FLASH
_sidata = LOADADDR(.data); /* init values in Flash */
.data : { _sdata = .; *(.data*) _edata = .; } > RAM AT > FLASH
.bss : { _sbss = .; *(.bss*) *(COMMON) _ebss = .; } > RAM
._stack_check : { . = . + _Min_Stack_Size; } > RAM /* link fails if no room */
}.data has a run address in RAM but is stored (loaded) in Flash — 'AT > FLASH'. The startup code copies it from _sidata to _sdata… and zeroes .bss before main().
Startup: initialising RAM before main()
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;
void Reset_Handler(void)
{
uint32_t *src = &_sidata, *dst = &_sdata;
while (dst < &_edata) *dst++ = *src++; /* copy .data Flash -> RAM */
for (dst = &_sbss; dst < &_ebss; ) *dst++ = 0; /* zero .bss */
SystemInit(); /* clocks, FPU, etc. */
main();
for (;;) { } /* main must never return */
}This is why a global 'int count = 5;' costs both Flash (the initial 5) and RAM, while 'int count;' only costs RAM (.bss).
Common mistakes
- Wrong: Large local arrays in functions. — Right: Make them static or global to move them off the stack, or size the stack for them.
- Wrong: Forgetting const on lookup tables. — Right: const places tables in Flash and saves RAM.
- Wrong: Assuming globals are zero before startup runs. — Right: Only after Reset_Handler zeroes .bss; don't use them in pre-main hooks.
Applications
- Bootloader + app: Separate Flash regions with their own linker scripts.
- Crash logging: No-init RAM sections survive a watchdog reset.
- Performance: Time-critical ISRs placed in RAM or tightly-coupled memory (TCM).
- Cost reduction: Map-file analysis to fit a cheaper, smaller MCU.
Key takeaways
- Compile → link (linker script) → .elf → .bin/.hex.
- Flash = text + data; RAM = data + bss + stack + heap.
- Startup copies .data and zeroes .bss.
- Reserve and monitor the stack.
Going further
- Scatter loading: Keil/IAR equivalents of GNU linker scripts.
- Overlays & XIP: Execute-in-place from external QSPI Flash.
- Position-independent code: -fPIC for relocatable app images.