5.5.17 · D2Embedded Systems & Real-Time Software

Visual walkthrough — Linker scripts — memory regions, sections (.text, .data, .bss)

2,848 words13 min readBack to topic

Before we start, three plain-English words we will keep coming back to:

  • Address — a house number for memory. Every byte of storage in the chip has one. We write them like 0x08000000 (the 0x just means "this is a hexadecimal number", a base-16 counting system used because it lines up neatly with how memory is wired). You can read it as "a big number that names a location."
  • Flash — the chip's attic: keeps its contents when the power is off, but you cannot cheaply rewrite it while the program runs.
  • RAM — the chip's desk: fast to read and write, but completely blank garbage the instant power returns.

Everything below is the story of reconciling those two facts for a variable that must both remember its starting value and be changeable.


Step 1 — Two memories, two number lines

WHAT. We draw the chip's memory as two separate number lines: Flash starting at address 0x08000000, RAM starting at 0x20000000. Each line is a row of numbered boxes (bytes).

WHY. Every confusion about linker scripts comes from forgetting that these are two physically different chips of silicon with two unrelated address ranges. If we never draw them side by side, we will mix them up. So we anchor the whole derivation to this picture first.

PICTURE. Look at the two grey strips below. The blue strip (top) is Flash — notice its numbers start at 0x08000000. The green strip (bottom) is RAM — its numbers start at 0x20000000. They do not touch. There is a huge gap between them; that gap is just addresses the chip doesn't use.

So in the picture: Flash is and RAM is .


Step 2 — Where must int x = 5; live? A contradiction

WHAT. We ask two simple questions about our one variable and discover they demand opposite answers.

WHY. This contradiction is the entire reason linker scripts are complicated. If we don't feel the contradiction, the fix that follows looks like arbitrary magic.

PICTURE. The two questions, drawn as two demands pulling x in opposite directions:

  • Demand A (the orange arrow → Flash): the value 5 must survive a power-off. RAM is wiped every power-up, so the number 5 cannot be stored only in RAM — it must be recorded somewhere non-volatile, i.e. Flash.
  • Demand B (the green arrow → RAM): the program will later do x = x + 1;. You cannot cheaply write Flash at runtime. So the live, changeable copy of x must sit in RAM.

Step 3 — Naming the two addresses: LMA and VMA

WHAT. We give the two addresses their official names and draw the exact bytes.

WHY. We must name things before we can move data between them. These two names appear in every real linker script, so we earn them now on the picture, not on faith.

PICTURE. The single value 5 shown as 4 bytes (int is 4 bytes) sitting at its LMA in Flash, with a dotted "will be copied to" arrow pointing at its future VMA in RAM. Right now, at power-on, the RAM boxes are red — they hold garbage.

  • LOADADDR(.data) ::: the linker function that reports the Flash address — the LMA
  • ADDR(.data) ::: the linker function that reports the RAM address — the VMA
  • the ::: this inequality is the whole point. For code (.text) these two are equal; for initialized data they differ.

Step 4 — Sorting every variable into a bucket (.text / .data / .bss)

WHAT. We generalise from one variable to all of them. Every piece of your program falls into exactly one bucket based on two yes/no questions.

WHY. The linker can't reason about variables one at a time — it groups thousands of them into a handful of sections, then places each section as a block. Understanding the sorting rule means you can predict which section anything lands in.

PICTURE. A decision tree: each item flows down through two questions into one of three boxes.

The two questions, in order:

  1. "Does it ever change at runtime?" — No → it's code or a constant → goes to .text/.rodata (Flash only, LMA = VMA).
  2. If yes, "Is its starting value zero?"
    • Non-zero start (int x = 5;) → .data. Must remember 5 → needs a Flash copy → two addresses (LMA ≠ VMA).
    • Zero start (int y; or int z = 0;) → .bss. Its starting value is always zero, so there's nothing worth storing.

Step 5 — Laying the sections onto the two number lines

WHAT. We place .text, then .data, then .bss onto the Flash and RAM strips from Step 1, tracking a moving pointer called the location counter.

WHY. The linker doesn't guess addresses — it stacks sections one after another, advancing a cursor. Seeing that cursor move is how you predict the final address of anything.

PICTURE. Flash fills top-down: .text first (starting exactly at Flash's ORIGIN), then the load image of .data right after it. RAM fills separately: the run copy of .data at RAM's ORIGIN, then .bss right after. Notice .data appears on both strips — that's the two-address idea made concrete.

Reading the addresses straight off the picture:

  • ::: the ORIGINs from Step 1
  • ::: how far the location counter advanced while placing code — this is why .data's Flash copy isn't at the very start of Flash

And the boundary symbols the startup code will need (each is just "the location counter's value right there"):

  • _sdata ::: start of data in RAM (the run copy's first byte)
  • _edata ::: end of data in RAM (one byte past the last)
  • _sidata ::: start of the init-data image in Flash (the load copy's first byte)

Step 6 — Boot: the copy loop (the moment x becomes 5)

WHAT. We run the startup code that copies .data from Flash (LMA) to RAM (VMA), byte by byte, until the RAM copy is complete.

WHY. Right after reset, no C has executed and RAM is garbage (the red boxes from Step 3). If we skipped this copy, reading x would give whatever random bits RAM powered up with — not 5. This loop is the OS-loader's job that, bare-metal, you must do.

PICTURE. Two pointers, p walking Flash from _sidata and q walking RAM from _sdata, marching in lockstep. Each tick copies one word Flash→RAM; the red garbage boxes turn green as the correct value 5 arrives.

  • p = &_sidata ::: source pointer starts at the Flash LMA (Step 5)
  • q = &_sdata ::: destination pointer starts at the RAM VMA
  • q < &_edata ::: keep going until the RAM cursor reaches the end mark — this is where size = _edata − _sdata gets used as a count
  • *q++ = *p++ ::: read one word from Flash, write it to RAM, then bump both pointers forward

After this loop, the RAM byte at _sdata holds 5. x is finally initialised.


Step 7 — Boot: the zero loop (the .bss blanking)

WHAT. A second loop walks the .bss RAM range and writes 0 everywhere.

WHY. .bss has no Flash source to copy from (Step 4) — nothing was stored. But int y; in C is guaranteed to start at zero, and RAM powered up as garbage. So we must create the zeros ourselves, right here.

PICTURE. One pointer q sweeping from _sbss to _ebss, red garbage turning to clean 0 boxes as it passes.

  • _sbss ::: start of the .bss region in RAM (location counter, right after .data)
  • _ebss ::: end of .bss; _ebss − _sbss is the region's size, the only thing Flash stored about .bss
  • *q++ = 0 ::: write zero, advance — no source pointer needed, because the source is just the constant 0

Only now are the stack pointer set and main() called. Every global finally holds the value C promised.


Step 8 — The degenerate & edge cases (never let the reader hit a wall)

WHAT. We check what happens at the boundaries — the empty and the overflowing cases — so no scenario surprises you.

WHY. Real firmware hits these. If you've only seen the "nice" case, an empty .bss or a too-big program looks like a bug when it's actually normal behaviour (or a diagnosable error).

PICTURE. Four mini-panels of the degenerate situations.

Case What the picture shows What actually happens
Empty .data (no initialized globals) _sdata == _edata The copy loop's condition q < &_edata is false immediately → zero iterations → correct, no crash.
Empty .bss (no zero globals) _sbss == _ebss Zero loop runs 0 times. Fine.
A global int z = 0; goes to .bss, not .data Compilers see the value is zero and put it in .bss to save Flash — surprising but correct.
RAM overflow (.data + .bss + stack ) RAM strip runs off its right edge The linker errors at build time: region RAM overflowed. Better a build error than a mystery crash.

The one-picture summary

Everything above, compressed into one diagram: the full life of int x = 5; — sorted into .data, stored at its LMA in Flash, copied by the boot loop to its VMA in RAM, ready for main(); and int y; blanked into .bss with no Flash cost.

non-zero, changes

zero start

stored at LMA

copy loop at boot

size only, no bytes

int x = 5

.data

int y

.bss

Flash image

RAM run copy VMA

zero loop at boot

main reads correct values

Recall Feynman: tell the whole walkthrough as a story

You write int x = 5;. The compiler shrugs — it knows the value but not where it lives. The linker script steps in as the floor-plan of the house. It draws two rooms: an attic (Flash) that never forgets, starting at house-number 0x08000000, and a desk (RAM) that's wiped every night, starting at 0x20000000.

Now x has a problem: it must remember it's 5 (so its value has to be in the attic), but it must be changeable (so the live copy has to be on the desk). One thing, two homes — a load address in the attic and a run address on the desk. The linker sorts every variable this way: things that never change (code) stay in the attic; things that change and start at some value go to .data (two homes); things that change and start at zero go to .bss and don't waste attic space at all — the linker just writes down "this many empty boxes."

When you flip the power on, the desk is full of random junk. A little helper (the startup code) climbs to the attic, carries x's stored 5 down to its desk spot (the copy loop), then wipes the .bss desk spots clean to zero (the zero loop). Only then does it call main() — and every variable finally holds exactly what your C source promised. If your program is too big for a room, the linker refuses to build rather than let you crash mysteriously. That's the entire linker script, in pictures.

Recall Quick self-check

Where is 5 physically stored, and where does x live while running? ::: 5 is stored at .data's LMA in Flash; x lives and changes at .data's VMA in RAM; the boot copy loop bridges them. Why does int z = 0; go to .bss instead of .data? ::: Its starting value is zero, so nothing is worth storing in Flash — it's cheaper to zero it in RAM at boot. What guarantees the copy/zero loops handle empty sections safely? ::: They're written as while (q < end); when start == end the loop runs zero times.


Related vault topics: Startup code & Reset_Handler · Vector table & interrupt handling · Compilation pipeline — object files & relocation · Flash vs RAM tradeoffs · Stack vs Heap in embedded systems · ELF file format & sections · Volatile keyword & memory-mapped IO