Linker Scripts Explained: Controlling Memory Layout on Bare Metal

Published: (December 13, 2025 at 03:23 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

The Anatomy of a Minimal Linker Script

A linker script has two primary blocks: MEMORY and SECTIONS.

ENTRY(_start)

MEMORY
{
    RAM (rwx) : ORIGIN = 0x60000000, LENGTH = 128M
}

SECTIONS
{
    . = 0x60000000;
    .text : {
        *(.text)
    } > RAM
}

Each component has a specific purpose.

ENTRY Directive

ENTRY(_start) specifies the program’s entry point symbol. When the ELF file is loaded, this symbol’s address becomes the starting point that debuggers and loaders recognize. In bare‑metal code, _start should match the first instruction executed after the reset vector transfers control.

Note: QEMU does not read an architectural reset vector for vexpress‑a9 when using -kernel; it sets the CPU’s PC to the ELF entry point address.

MEMORY Block

The MEMORY block declares available address regions and their properties.

NameAttributesORIGINLENGTH
FLASHrx0x0000000064M
RAMrwx0x60000000128M
MEMORY
{
    FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64M
    RAM   (rwx): ORIGIN = 0x60000000, LENGTH = 128M
}

Attributes are advisory; the linker warns if a write‑able section is placed in a read‑only region.

SECTIONS Block

The SECTIONS block specifies the output memory layout. Each entry maps input sections (from object files) to output sections (in the final binary) and assigns a memory location.

SECTIONS
{
    .text : {
        *(.text)
    } > FLASH

    .rodata : {
        *(.rodata*)
    } > FLASH
}
  • .text : { … } defines an output section named .text.
  • *(.text) includes all .text input sections from all object files.
  • > FLASH assigns the output section to the FLASH memory region.

Wildcards (*) match all input files; more specific patterns (e.g., startup.o(.text)) are also possible.

The Location Counter: .

The linker maintains an implicit variable called the location counter, written as a dot: .. It tracks the current position within memory and automatically increments as sections are laid out.

SECTIONS
{
    . = 0.60000000;          /* Set location counter */

    .text : {
        *(.text)
    } > RAM                 /* .text placed at 0x60000000 */

    .rodata : {
        *(.rodata*)
    } > RAM                 /* .rodata follows .text */
}

You can also create symbols from the location counter:

_text_start = .;            /* Address where .text begins */
.text : { *(.text) } > RAM
_text_end = .;              /* Address where .text ends */

These symbols occupy no storage; they are merely addresses that can be referenced from assembly or C.

VMA vs LMA: Virtual and Load Memory Addresses

Every output section has two associated addresses:

  • VMA (Virtual Memory Address): where the section resides during execution.
  • LMA (Load Memory Address): where the section is stored initially (typically in non‑volatile flash).

In simple cases VMA and LMA are identical. For sections like .data, the initial contents reside in flash (LMA) but execute from RAM (VMA). Startup code copies the data from flash to RAM before use.

.data : {
    *(.data)
} > RAM AT > FLASH          /* VMA in RAM, LMA in FLASH */

> RAM specifies the VMA; AT > FLASH specifies the LMA.

Linker‑Defined Symbols

The linker can create symbols by assigning the location counter or other expressions to a name. These symbols exist only as addresses.

SECTIONS
{
    . = 0x60000000;

    _text_start = .;        /* Address where .text begins */

    .text : { *(.text) } > RAM

    _text_end = .;          /* Address where .text ends */

    _stack_top = ORIGIN(RAM) + LENGTH(RAM);  /* Custom symbol */
}

In assembly, these symbols are referenced using the = pseudo‑op:

ldr r0, =_text_start        @ Load symbol address into r0
ldr sp, =_stack_top         @ Load stack top address into sp

The pseudo‑op generates an LDR instruction with a literal‑pool reference; the linker resolves the symbol address at link time.

The Map File

The linker can generate a map file (-Map=output.map) that lists the layout of sections, symbols, and memory regions. This file is invaluable for verifying that sections are placed where you expect and for spotting size overruns.

Verification: What the Linker Produced

Step 1: Examine Disassembly

arm-none-eabi-objdump -d firmware.elf

Look for the entry point (_start) and verify that code resides at the intended addresses (e.g., 0x60000000).

Step 2: Examine Section Headers

arm-none-eabi-objdump -h firmware.elf

The output shows each section’s VMA, LMA, size, and the memory region it belongs to.

Step 3: Examine the Map File

Open firmware.map and search for symbols such as _text_start, _text_end, and _stack_top. Confirm that their addresses match the layout described in the script.

Verification: Loading in QEMU and Inspecting with GDB

qemu-system-arm -M vexpress-a9 -kernel firmware.elf -nographic -S -s

In another terminal:

arm-none-eabi-gdb firmware.elf
(gdb) target remote :1234
(gdb) info files          # Shows loaded sections and entry point
(gdb) break _start
(gdb) continue

Check that the PC starts at the address defined by ENTRY(_start) and that memory regions contain the expected data.

Alignment Directives

Linker scripts support alignment directives to satisfy architectural constraints:

.text ALIGN(4) : { *(.text) } > FLASH

ALIGN(4) forces the start of .text to be on a 4‑byte boundary. You can also align the location counter directly:

. = ALIGN(0x1000);   /* Align to the next 4 KB boundary */

Proper alignment prevents faults on CPUs that require word‑aligned accesses.

Conclusion

Linker scripts give you deterministic control over where code and data live in a bare‑metal system. By defining memory regions, using the location counter, specifying VMA/LMA, and creating linker‑defined symbols, you can match the exact memory map required by your hardware. Verifying the output with disassembly, section headers, map files, and runtime inspection (e.g., QEMU + GDB) ensures that the layout is correct before flashing the device.

Back to Blog

Related posts

Read more »

Advent of Embedded Linux — Day 3

!Forem Logohttps://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%...