We’re Officially Live β€” Lifetime Founding Membership Available for a Limited Time
Free preview

Linker Scripts & Memory Sections

Telling the linker the chip's memory map: MEMORY and SECTIONS place .text in flash and .data/.bss in RAM, and the VMA-vs-LMA split is why startup code must copy initialized data.

24 min read

In this lesson:

This ties together sections (C module), the memory map and boot (Embedded Fundamentals), and the linker (toolchain).


What a linker script is

The linker must decide where every piece of your program lives in the chip's address space, code in flash, variables in RAM, the vector table at the very start. A linker script (.ld) is the file that tells it: the chip's memory regions and how to place output sections into them. On a desktop the default script suffices; on bare metal you supply one matching your MCU (the memory map from Embedded Fundamentals), or nothing boots.


MEMORY: the regions

The MEMORY command declares the chip's physical memory regions with their start address and size:

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K   /* code + constants */
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K   /* data, stack, heap */
}

(rx) = readable/executable (flash), (rwx) = read/write/execute (RAM). The origins are the chip's actual addresses (STM32 flash at 0x08000000, SRAM at 0x20000000). If your output overflows a region, the linker errors, region FLASH overflowed by N bytes, a useful "it doesn't fit" check.


SECTIONS: placing the output

The SECTIONS command says which output sections go in which region. The sections (from earlier modules): .text (code), .rodata (const data), .data (initialized globals), .bss (zero-init globals).

SECTIONS
{
  .text : {
    KEEP(*(.isr_vector))   /* vector table FIRST β€” and KEEP so GC won't drop it */
    *(.text*)              /* all code */
    *(.rodata*)            /* const data */
    _etext = .;            /* mark end of text (used as .data's load source) */
  } > FLASH
 
  .data : {
    _sdata = .;            /* start of .data in RAM (VMA) */
    *(.data*)
    _edata = .;            /* end of .data in RAM */
  } > RAM AT > FLASH       /* runs in RAM, but STORED in FLASH ← the key line */
 
  .bss : {
    _sbss = .;
    *(.bss*) *(COMMON)
    _ebss = .;
  } > RAM
}

The vector table is placed first via KEEP(*(.isr_vector)) (KEEP prevents --gc-sections from garbage-collecting it since "nothing calls it"). The all-important line is .data ... > RAM AT > FLASH, next.


VMA vs LMA: why .data is copied at boot

This is the concept the whole script hinges on. Every section has two addresses:

For most sections VMA = LMA (code runs from flash, stored in flash). But .data is special: initialized globals must be writable, so they run in RAM (VMA in RAM), yet RAM is volatile and starts undefined, so their initial values must be stored in flash (LMA in flash). > RAM AT > FLASH sets exactly that: VMA = RAM, LMA = FLASH.

 FLASH                          RAM
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ .isr_vector β”‚                β”‚             β”‚
 β”‚ .text       β”‚                β”‚ .data (VMA) │◀── runs here
 β”‚ .rodata     β”‚   startup      β”‚ .bss  (VMA) β”‚
 β”‚ .data (LMA) │── copies ─────▢│   ...       β”‚
 β”‚  (init vals)β”‚   at boot      β”‚  heap ↑     β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚  stack ↓    β”‚
                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 _sidata (LMA) ──────────────▢ _sdata.._edata (VMA): startup copies this range

The startup code (boot lesson) reads the .data initial values from flash (LMA) and copies them into RAM (VMA) at reset, then zeroes .bss. The linker script defines the boundary symbols the startup loop uses:

// Startup uses the linker-script symbols (Embedded Fundamentals boot lesson):
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;
for (uint32_t *d=&_sdata, *s=&_sidata; d<&_edata; ) *d++ = *s++;  // copy .data: FLASH→RAM
for (uint32_t *b=&_sbss; b<&_ebss; ) *b++ = 0;                    // zero .bss

So .data initialized globals work only because the linker stored their values in flash and startup copied them to RAM. Forget the AT > FLASH (or the copy) and your globals are garbage.


Other script duties

ENTRY(Reset_Handler)               /* the program's entry symbol */
_estack = ORIGIN(RAM) + LENGTH(RAM);  /* initial stack pointer = top of RAM */

ENTRY names the start symbol; _estack (top of RAM) becomes the first vector-table entry (the initial SP, boot lesson). Scripts also place special regions (a separate CCM RAM, a no-init section that survives reset, a bootloader/app split via different ORIGINs).


Gotchas


TL;DR

This is just the start

Sign up free to track progress on this lesson, get coding problems with the Run button, and unlock the full interview Q&A.

More in Debugging & Toolchain

The ToolchainThe Build Process & Makefiles
Debugging with GDB
On-Chip Debugging: JTAG & SWD
Breakpoints & Watchpoints
Logic Analyzer & Oscilloscope