Procedural Programming with C · Professional · Week 52

Linker Scripts and Firmware Memory Layout

On a hosted system the linker's default script is invisible and correct. On bare metal you write it, and it decides where every byte lands. This week opens the file that weeks 49 through 51 have been quietly relying on.

By the end of this week you can
  • Write a linker script defining memory regions and section placement.
  • Explain load address versus virtual address, and why .data has both.
  • Define and use symbols that C code can read.
  • Place the stack, and detect an overflow before it corrupts anything.
  • Read a map file and account for every byte of an image.

1Sections

The compiler does not emit a flat stream of bytes. It sorts everything into named sections, and the linker gathers matching sections from every object file and places each group at an address.

SectionContainsLives inIn the image?
.textMachine codeFlashYes
.rodataconst data, string literalsFlashYes
.dataInitialized globalsRAMYes — the initial values
.bssZero-initialized globalsRAMNo — only its size
StackLocals, call framesRAMNo
Heapmalloc, if used at allRAMNo

Week 17 met these on a hosted system, where the loader handles them. Here the same four sections exist and nothing handles them for you.

2Load address and virtual address

This is the idea the whole script turns on. Most sections live where they are stored. .data does not.

.text .rodata .data init values .data .bss (zeroed) stackgrows down FLASH 0x00000000 RAM 0x20000000 copied by the reset handler LMA = where it is stored · VMA = where it is addressed

Week 49's startup loop, seen from the linker's side.

  • VMA — virtual memory address — is where the code expects the section to be. For .data, RAM.
  • LMA — load memory address — is where it is actually stored in the image. For .data, flash.

For .text and .rodata the two are the same: code executes directly from flash. For .data they differ, and the gap is exactly what the startup copy closes.

3The script

ENTRY(reset_handler)                 /* the entry symbol, for debuggers */

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x00000000, LENGTH = 256K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}

_stack_size = 4K;
_heap_size  = 0;                     /* no heap: week 53 */

SECTIONS
{
    .text : {
        KEEP(*(.vectors))            /* must be first, and never discarded */
        *(.text*)
        *(.rodata*)
        . = ALIGN(4);
        _etext = .;
    } > FLASH

    .data : {
        . = ALIGN(4);
        _data_start = .;
        *(.data*)
        . = ALIGN(4);
        _data_end = .;
    } > RAM AT> FLASH                /* VMA in RAM, LMA in FLASH */

    _data_load = LOADADDR(.data);    /* where the startup code reads from */

    .bss (NOLOAD) : {
        . = ALIGN(4);
        _bss_start = .;
        *(.bss*)
        *(COMMON)
        . = ALIGN(4);
        _bss_end = .;
    } > RAM

    .stack (NOLOAD) : {
        . = ALIGN(8);                /* ARM requires 8-byte stack alignment */
        _stack_bottom = .;
        . = . + _stack_size;
        _stack_top = .;
    } > RAM

    /* Fail the build rather than the device. */
    ASSERT(_stack_top <= ORIGIN(RAM) + LENGTH(RAM), "RAM overflow")
}
ConstructMeans
MEMORYNames the physical regions and their sizes
> FLASHPlace this section in that region
AT> FLASH…but store it in this one — sets the LMA
.The location counter, assignable
KEEP(...)Do not discard even if nothing references it
NOLOADReserve the space; store nothing in the image
_symbol = .;Define a symbol C can refer to
ASSERT(...)Fail the link if the condition is false

KEEP on the vector table is not optional. With --gc-sections the linker discards anything unreferenced, and nothing in C refers to the vector table — the processor reads it directly. Without KEEP it disappears and the device does not boot.

The ASSERT is the difference between a link error and a device that runs until the stack meets .bss. Every embedded project should have one.

4Linker symbols in C

A symbol defined in the script has no storage — the linker records only an address. That makes the C declaration counter-intuitive:

extern uint32_t _data_start;      /* declares a variable AT that address */

uint32_t *p = &_data_start;       /* CORRECT: take its address */
uint32_t  v = _data_start;        /* WRONG: reads whatever is stored there */

The value of a linker symbol is its address, so you always want &. Some projects avoid the trap by declaring them as arrays, where the name already decays to an address:

extern uint32_t _data_start[];    /* now _data_start is the address */
uint32_t *p = _data_start;        /* no & needed, and no way to misuse it */

5Stack placement and overflow

The stack grows downward from its top. Place it at the end of RAM and it grows toward .bss; there is no guard page and no fault — week 49's point about the absent MMU.

Three defences, in increasing cost:

A painted region. Fill the stack with a pattern at startup, then check later how far down it has been disturbed. This measures the high-water mark, which tells you how much margin you actually have.

#define STACK_PAINT 0xC0DEC0DEu

void paint_stack(void)               /* called early, before deep calls */
{
    uint32_t *p = _stack_bottom;
    uint32_t marker;
    while (p < &marker) *p++ = STACK_PAINT;   /* up to the current frame */
}

size_t stack_used(void)
{
    uint32_t *p = _stack_bottom;
    while (p < _stack_top && *p == STACK_PAINT) p++;
    return (size_t)((uint8_t *)_stack_top - (uint8_t *)p);
}

A canary. Reserve a word at the bottom of the stack, set it to a known value, and check it periodically. Cheap, and it detects an overflow after the fact rather than before.

Hardware. An MPU region marked no-access below the stack turns an overflow into a fault at the moment it happens. Available on many Cortex-M parts and worth configuring on anything safety-relevant.

6Worked example: taking an image apart

/* layout.c — every section populated deliberately */
#include <stdint.h>

/* Linker symbols, declared as arrays so no & is needed. */
extern uint32_t _etext[], _data_start[], _data_end[], _data_load[];
extern uint32_t _bss_start[], _bss_end[];
extern uint32_t _stack_bottom[], _stack_top[];

/* .rodata — stays in flash, costs no RAM */
static const char banner[]      = "firmware layout demo";
static const uint32_t table[16] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 };

/* .data — initial values in flash, variable in RAM */
static uint32_t marker   = 0xA5A5A5A5u;
static uint16_t revision = 42;

/* .bss — no flash cost at all */
static uint32_t counter;
static uint8_t  scratch[1024];

/* ---------- UART, from week 50 ---------- */

#define UART0_DR (*(volatile uint32_t *)0x4000C000u)
#define UART0_FR (*(volatile uint32_t *)0x4000C018u)

static void putc_raw(char c)
{
    while (UART0_FR & (1u << 5)) { }
    UART0_DR = (uint32_t)(unsigned char)c;
}
static void puts_raw(const char *s)
{
    for (; *s; s++) { if (*s == '\n') putc_raw('\r'); putc_raw(*s); }
}
static void put_hex(uint32_t v)
{
    static const char d[] = "0123456789ABCDEF";
    puts_raw("0x");
    for (int s = 28; s >= 0; s -= 4) putc_raw(d[(v >> s) & 0xFu]);
}
static void put_uint(uint32_t v)
{
    char t[11]; int i = 0;
    if (!v) { putc_raw('0'); return; }
    while (v) { t[i++] = (char)('0' + v % 10); v /= 10; }
    while (i) putc_raw(t[--i]);
}

/* ---------- stack painting ---------- */

#define PAINT 0xC0DEC0DEu

void paint_stack(void)
{
    uint32_t here;
    for (uint32_t *p = _stack_bottom; p < &here; p++) {
        *p = PAINT;
    }
}

static uint32_t stack_used_bytes(void)
{
    uint32_t *p = _stack_bottom;
    while (p < _stack_top && *p == PAINT) p++;
    return (uint32_t)((uint8_t *)_stack_top - (uint8_t *)p);
}

/* Recursion to consume stack on demand. */
static uint32_t consume(uint32_t depth)
{
    volatile uint8_t frame[64];
    frame[0] = (uint8_t)depth;
    if (depth == 0) return frame[0];
    return consume(depth - 1) + frame[0];
}

int main(void)
{
    puts_raw("\n=== firmware layout ===\n\n");

    puts_raw("FLASH\n");
    puts_raw("  .text + .rodata end : "); put_hex((uint32_t)(uintptr_t)_etext);
    puts_raw("\n  banner (rodata)     : ");
    put_hex((uint32_t)(uintptr_t)banner);
    puts_raw("\n  table  (rodata)     : ");
    put_hex((uint32_t)(uintptr_t)table);
    puts_raw("\n  .data initial values: ");
    put_hex((uint32_t)(uintptr_t)_data_load);
    puts_raw("   <-- LMA, in flash\n");

    puts_raw("\nRAM\n");
    puts_raw("  .data  "); put_hex((uint32_t)(uintptr_t)_data_start);
    puts_raw(" .. ");      put_hex((uint32_t)(uintptr_t)_data_end);
    puts_raw("   <-- VMA, in RAM\n");
    puts_raw("  .bss   "); put_hex((uint32_t)(uintptr_t)_bss_start);
    puts_raw(" .. ");      put_hex((uint32_t)(uintptr_t)_bss_end);
    puts_raw("\n  stack  "); put_hex((uint32_t)(uintptr_t)_stack_bottom);
    puts_raw(" .. ");        put_hex((uint32_t)(uintptr_t)_stack_top);
    puts_raw("\n");

    puts_raw("\nsizes\n");
    puts_raw("  .data : ");
    put_uint((uint32_t)((uint8_t *)_data_end - (uint8_t *)_data_start));
    puts_raw(" bytes  (also occupies that much FLASH)\n");
    puts_raw("  .bss  : ");
    put_uint((uint32_t)((uint8_t *)_bss_end - (uint8_t *)_bss_start));
    puts_raw(" bytes  (zero FLASH)\n");
    puts_raw("  stack : ");
    put_uint((uint32_t)((uint8_t *)_stack_top - (uint8_t *)_stack_bottom));
    puts_raw(" bytes reserved\n");

    puts_raw("\nstartup did its job\n");
    puts_raw("  marker   = "); put_hex(marker);
    puts_raw(marker == 0xA5A5A5A5u ? "  .data copied\n" : "  NOT COPIED\n");
    puts_raw("  revision = "); put_uint(revision); puts_raw("\n");
    puts_raw("  counter  = "); put_uint(counter);
    puts_raw(counter == 0 ? "  .bss zeroed\n" : "  NOT ZEROED\n");

    puts_raw("\nstack high-water mark\n");
    puts_raw("  at entry        : "); put_uint(stack_used_bytes());
    puts_raw(" bytes\n");

    (void)consume(8);
    puts_raw("  after 8 frames  : "); put_uint(stack_used_bytes());
    puts_raw(" bytes\n");

    (void)consume(40);
    puts_raw("  after 40 frames : "); put_uint(stack_used_bytes());
    puts_raw(" bytes\n");

    uint32_t reserved = (uint32_t)((uint8_t *)_stack_top -
                                   (uint8_t *)_stack_bottom);
    uint32_t used = stack_used_bytes();
    puts_raw("  headroom        : "); put_uint(reserved - used);
    puts_raw(" of "); put_uint(reserved); puts_raw(" bytes\n");
    puts_raw("\n  this is a measurement, not an estimate — which is what\n");
    puts_raw("  lets you size the stack with evidence\n");

    for (;;) { }
}

Call paint_stack() from the reset handler, immediately after zeroing .bss and before calling main.

arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \
    -ffreestanding -nostdlib -O2 -Wall -Wextra -g \
    -T firmware.ld -Wl,-Map=firmware.map \
    -o firmware.elf startup.c layout.c

qemu-system-arm -M lm3s6965evb -nographic -kernel firmware.elf

Compare the output with the tools

arm-none-eabi-size firmware.elf
   text    data     bss     dec     hex filename
   1632       8    1028    2668     a6c firmware.elf

Eight bytes of .data — the uint32_t and the uint16_t with padding — and 1028 of .bss, which is the 1024-byte scratch plus counter. The .data figure appears in both flash and RAM; the .bss figure only in RAM. Add = {1} to scratch and watch text grow by a kilobyte as it moves into .data.

arm-none-eabi-objdump -h firmware.elf
Idx Name      Size      VMA       LMA       File off  Algn
  0 .text     00000660  00000000  00000000  00010000  2**2
  1 .data     00000008  20000000  00000660  00020000  2**2
  2 .bss      00000404  20000008  20000008  00020008  2**2

There it is, printed by the tool: .text has VMA equal to LMA, and .data does not — VMA 0x20000000 in RAM, LMA 0x00000660 in flash. That one row is the entire content of section 2.

Read the map file

grep -A20 'Memory Configuration' firmware.map
grep -E '^\s+\.(text|data|bss)' firmware.map | head -20
grep -B2 -A8 '_stack_top' firmware.map

The map file accounts for every byte: which object file contributed which bytes to which section, at which address. When an image is unexpectedly large, this is where the answer is — usually one function, or a library pulled in by a single call to printf.

arm-none-eabi-nm --size-sort -S firmware.elf | tail -10   # the biggest symbols

Move a variable into flash

__attribute__((section(".rodata"))) static const uint32_t big_table[256] = { … };

Rebuild and compare size: text grows by a kilobyte and RAM is untouched. On a part with 8 KB of RAM and 64 KB of flash, moving constant tables out of RAM is often the difference between fitting and not.

Make the linker catch a mistake

Set _stack_size = 128K; in the script and rebuild:

arm-none-eabi-ld: RAM overflow
collect2: error: ld returned 1 exit status

The ASSERT turned a device that would have corrupted itself at run time into a build failure. Remove the assertion and the same configuration links cleanly, producing firmware whose stack overlaps .bss — and the symptom would be variables changing value for no reason.

Watch the stack grow into the danger zone

Raise the recursion depth until the high-water mark approaches the reserved size:

(void)consume(60);   /* 60 frames of 64 bytes plus overhead */

The reported headroom shrinks toward zero. Push past it and variables in .bss start changing — silently, because nothing is watching. That is the failure this week's painting and ASSERT exist to make visible, and it is why embedded projects size the stack from a measurement rather than a guess.

7Common mistakes

MistakeWhat happensFix
No KEEP on the vector tableDiscarded by --gc-sections; device will not bootKEEP(*(.vectors)).
Vector table not firstThe processor reads the wrong initial SPPlace it at the start of .text.
Omitting AT> FLASH on .dataInitial values are never stored in the imageSet the LMA explicitly.
Reading a linker symbol's valueReads the memory at that addressTake its address, or declare it as an array.
No RAM-overflow ASSERTStack silently overlaps .bssAdd the check to the script.
Guessing the stack sizeEither wasted RAM or an overflow in the fieldPaint it and measure.
Forgetting ALIGNUnaligned access faults — week 37Align sections to 4, the stack to 8.
Large tables without constThey occupy RAM as well as flashconst keeps them in .rodata.

8Check yourself

What is the difference between a section's LMA and VMA, and which section needs both?

The LMA is where the section is stored in the image; the VMA is the address the code expects it at. They are the same for .text and .rodata, which execute from flash. .data needs both: its initial values must be stored in flash because RAM is empty at power-on, but the variables are addressed in RAM — and the startup code copies one to the other.

Why does .bss occupy no space in the image?

Because every byte is zero, so there is nothing to store — only the size is recorded, and the startup code clears that much RAM. Give a .bss variable a non-zero initializer and it moves to .data, where the contents must actually be present in flash.

Why must the vector table be wrapped in KEEP?

Because nothing in the C code references it — the processor reads it directly from address 0 — so section garbage collection sees an unused section and discards it. Without it the image links successfully and the device fails to boot, which is a particularly unhelpful combination.

Why is uint32_t v = _data_start; wrong?

Because a linker symbol has no storage: the linker records only an address, and the declaration extern uint32_t _data_start describes a variable located at that address. Reading it reads whatever bytes are there. You want &_data_start — or declare it as an array, where the name is already the address.

How do you size a stack with evidence rather than a guess?

Fill the reserved region with a known pattern at startup, run the worst-case workload, then find the lowest address whose pattern is still intact. The distance from there to the top is the high-water mark. Combined with an ASSERT in the linker script that the regions fit, that converts a guess into a measurement and a build-time check.

9Where this leads

Week 53 takes the constraint seriously: no heap at all. Pool allocators, ring buffers, fixed-point arithmetic instead of floating point, and optimizing for code size — the techniques that let a program fit in the flash and RAM this week's script has been measuring.