Electronics Design AU
Firmware

What Is a Linker Script and What Does It Do?

Last updated 29 June 2026 · 9 min read

Direct Answer

A linker script is a text file (typically ending in .ld) that instructs the GNU linker how to map sections from compiled object files into the MCU's memory — which code goes into flash, which data initialises in RAM, where the stack lives, and where the heap starts. It defines available memory regions (FLASH and RAM), groups input sections into output sections, and exports symbol addresses that startup code uses to copy initialised data from flash to RAM and zero out BSS before main() runs.

Detailed Explanation

When the C compiler processes a source file it produces an object file containing named sections: .text (executable code), .rodata (read-only constants), .data (initialised global and static variables), .bss (zero-initialised variables), and others. The linker combines all these object files into a single firmware binary — but it needs to know where each section should live in the target MCU's memory map. That instruction set is the linker script.

Without a linker script the linker has no idea how much flash exists, how much RAM exists, what address flash starts at, or where the interrupt vector table must go. On a desktop system the OS fills this role. In embedded firmware the linker script is the sole source of that information.

The MEMORY Block

The MEMORY block declares the physical memory regions available on the MCU. A typical STM32F405 example:

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
  • ORIGIN — the base address of the memory region, taken directly from the MCU's datasheet address map (see how the Cortex-M memory map works for how these regions are defined).
  • LENGTH — the size of the region in bytes (or with a K/M suffix). This must match the specific MCU variant: an STM32F405RG has 1 MB flash and 128 KB RAM; an STM32F401RC has 256 KB flash and 64 KB RAM. Using one variant's values for another is a common source of hard-to-diagnose faults.
  • (rx) / (rwx) — access attributes: r = readable, w = writable, x = executable. These are informational for the linker; they do not configure hardware memory protection.

The SECTIONS Block

The SECTIONS block maps input sections (from compiled object files) to output sections (in the final binary) and assigns each to a memory region:

SECTIONS
{
  /* Code and read-only data — stored and executed from flash */
  .text :
  {
    KEEP(*(.isr_vector))      /* interrupt vector table — must be first */
    *(.text*)                 /* all compiled code */
    *(.rodata*)               /* read-only constants, string literals */
    _etext = .;               /* exported symbol: end of flash content */
  } > FLASH

  /* Initialised variables — stored in flash, run from RAM */
  .data :
  {
    _sdata = .;               /* VMA start of .data in RAM */
    *(.data*)
    _edata = .;               /* VMA end of .data in RAM */
  } > RAM AT > FLASH          /* VMA = RAM, LMA = flash */

  _sidata = LOADADDR(.data);  /* LMA start of .data in flash */

  /* Zero-initialised variables — run from RAM, no flash storage needed */
  .bss (NOLOAD) :
  {
    _sbss = .;
    *(.bss*)
    *(COMMON)
    _ebss = .;
  } > RAM

  /* Stack and heap reservation */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE(_end = .);
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } > RAM
}

Key points in this structure:

> FLASH assigns a section's VMA (runtime address) to flash — code and read-only data never moves at runtime.

> RAM AT > FLASH is the .data split: the VMA (the address the program uses to access the variable at runtime) is in RAM, but the LMA (the address the binary stores the initial value) is in flash. The AT > FLASH directive causes the linker to place the initial values immediately after the .text section in the flash image, and exports a pointer to that location (_sidata in this example via LOADADDR(.data)) so startup code can find it.

KEEP(*(.isr_vector)) prevents the linker from discarding the interrupt vector table as dead code during link-time optimisation. The vector table is read directly by the Cortex-M hardware at reset — there is no C reference to it that the linker can trace. Without KEEP, aggressive optimisation (-flto) can eliminate it, producing a binary that faults immediately on startup.

_sdata, _edata, _sbss, _ebss — these are linker-generated symbols. They are not C variables; they are addresses the linker calculates and makes visible to startup code via the symbol table.

(NOLOAD) on .bss tells the linker not to include space for this section in the output binary. BSS variables are always zero-initialised by startup code; there is no need to store 128 KB of zeros in the flash image.

LMA vs VMA: Why .data Lives in Two Places

RAM loses its contents when power is removed. Flash retains contents after power-off. Global and static C variables that are explicitly initialised (e.g. int count = 5;) must simultaneously:

  1. Be stored in the flash image so they have their correct initial values after programming or power cycling.
  2. Live in RAM at runtime so they can be written by the program.

The LMA/VMA distinction handles exactly this. For the .data section:

  • LMA (Load Memory Address) = an address in flash, immediately after .text. This is where the initial byte values are stored in the .bin or .hex file.
  • VMA (Virtual Memory Address) = the address in RAM where the linker places the section for runtime access. All code that references count uses the VMA.

The linker exports _sidata (the LMA start) and _sdata/_edata (the VMA range). Startup code copies from _sidata to [_sdata, _edata) before calling main().

What Startup Code Does with These Symbols

The startup file (typically startup_stm32xxx.s or a C equivalent provided by the vendor) runs before main(). It uses the linker symbols to initialise RAM:

Step 1 — Copy .data from flash to RAM:

uint32_t *src = &_sidata;    /* LMA: initial values stored in flash */
uint32_t *dst = &_sdata;     /* VMA: where variables live in RAM */
while (dst < &_edata) {
    *dst++ = *src++;
}

Step 2 — Zero-initialise .bss:

uint32_t *p = &_sbss;
while (p < &_ebss) {
    *p++ = 0;
}

Uninitialised global variables (e.g. int counter;) are placed in .bss. The C standard requires these to be zero at program start — the startup code fulfils that requirement, because RAM power-on state on a real MCU is not guaranteed to be zero.

Step 3 — Call main(), typically after SystemInit() or equivalent clock configuration.

If startup code is absent, incorrect, or skipped, initialised globals will contain garbage and BSS variables will be non-zero — producing subtle bugs that are difficult to trace because they are invisible in the linker script or build output. For a detailed walkthrough of what the startup file actually does — reset handler, SystemInit(), data copy, BSS zeroing, and C++ constructors — see what does embedded startup code do before main()?.

The Interrupt Vector Table

KEEP(*(.isr_vector)) at the very start of .text places the interrupt vector table at address 0x08000000 on most STM32 devices. On Cortex-M, the processor reads the initial stack pointer from offset 0 and the reset handler address from offset 4 in the vector table immediately on startup — these must be at the correct address or the MCU immediately hard faults.

When an application runs after a bootloader, the application's linker script must set its flash ORIGIN past the bootloader region, and the startup code must write the application's vector table address to SCB->VTOR before any interrupts fire. The linker script is what places the application's vector table at the correct offset. For more on how interrupts are dispatched at runtime, see what are interrupts in embedded systems.

Locating the Linker Script in a Real Project

In STM32CubeMX-generated projects the linker script appears at the project root, named after the MCU (e.g. STM32F405RGTX_FLASH.ld). In the IDE project settings, the _Min_Heap_Size and _Min_Stack_Size symbols referenced by the script are injected as command-line defines (-D_Min_Heap_Size=0x200 -D_Min_Stack_Size=0x400). If the script is used outside the IDE, those defines must be supplied manually or the link fails with "undefined reference."

For custom or open-source toolchain setups (Makefiles, CMake with arm-none-eabi-gcc), the linker script is passed with the -T flag: arm-none-eabi-gcc ... -T STM32F405RGTX_FLASH.ld.

For embedded firmware projects with custom memory layouts or multi-stage bootloader architectures, Zeus Design's firmware team handles linker configuration, startup code, and boot-stage design as part of complete firmware deliverables.

Design Considerations

  • A/B partition layout for OTA: Over-the-air firmware updates require the linker script to define separate flash regions for the primary slot (running firmware) and secondary slot (staged image). Each slot needs its own ORIGIN address in the MEMORY block, and both application images plus the bootloader must fit within the device's total flash.
  • Validate section sizes in CI: Add a post-link step that checks the .data + .text + .rodata total against the device's actual flash capacity. The linker only checks against the LENGTH declared in the script — it does not query the hardware.
  • CCM RAM on STM32F4/F7: These devices have Core Coupled Memory (CCMRAM) that is faster but not DMA-accessible. Place time-critical code or stack there by adding a CCMRAM region to the MEMORY block and a separate .ccmram output section. Never place DMA buffers in CCMRAM on devices where DMA cannot access it.
  • Multiple load regions for external flash: When firmware partially executes from external QSPI flash, a second MEMORY region and AT > QSPI_FLASH directives place specific sections there. The startup code must initialise the QSPI peripheral and configure the XSPI memory-mapped mode before copying .data or executing from that region.

Common Mistakes

  • Wrong LENGTH for the MCU variant. Different members of an STM32 family have different flash and RAM. Always read the specific device's reference manual, not the family overview.
  • Missing KEEP on the vector table. With -flto enabled, the linker eliminates unreferenced sections. The vector table is accessed by hardware, not by C code, so the linker considers it unused without KEEP.
  • Mismatched symbol names between startup file and linker script. If startup code references __data_start__ but the script exports _sdata, the link fails with "undefined reference." Vendor startup files and linker scripts are paired — always use matching versions.
  • Forgetting _sidata (the LMA pointer). Some minimal linker scripts export _sdata and _edata but omit the LMA start. Without _sidata or LOADADDR(.data), startup code has no source address to copy from and .data initialisation is broken.
  • Placing DMA buffers in non-DMA-accessible RAM. On STM32F4, CCMRAM (0x10000000) is inaccessible to the DMA controller. Placing receive or transmit buffers there produces silent failures — the DMA completes without error but the buffer is never filled.

Frequently Asked Questions

Do I need to write my own linker script?
For most Cortex-M development using STM32CubeMX, ESP-IDF, or a vendor SDK, a linker script is generated or provided. You need to understand it when: the default script has wrong flash/RAM sizes for your MCU variant; you are placing firmware in a non-standard memory region (external flash, CCM RAM, ITCM); you are implementing a custom bootloader with a separate application region; or the linker produces errors about overlapping sections or undefined symbols you need to diagnose.
What is the difference between LMA and VMA in a linker script?
LMA (Load Memory Address) is where a section is stored in the binary — on Cortex-M, this is typically in flash. VMA (Virtual Memory Address) is where the section is accessed at runtime. For the .data section these differ: .data is stored (LMA) in flash so values survive power-off, but accessed (VMA) from RAM because RAM is read-write. Startup code copies .data from its LMA in flash to its VMA in RAM before calling main(). The AT() keyword in a linker script sets the LMA when it differs from the VMA.
What happens if FLASH or RAM length is wrong in the linker script?
If FLASH LENGTH is too small the linker errors: 'region FLASH overflowed with X bytes'. If it is too large the linker accepts the binary but it will not fit on the actual device — there is no hardware cross-check. If RAM LENGTH is too small, stack and heap are truncated silently, causing stack overflows or malloc failures at runtime with no clear error. Always match ORIGIN and LENGTH to the exact MCU variant's reference manual — different package variants of the same MCU family can have different flash and RAM configurations.

References

Related Questions

Related Forum Discussions