Skip to content

Part 2 · C Meets the Metal29 min read

Linker Scripts

On a PC, the linker’s defaults are fine and you never see them. On a microcontroller they are useless, because only you know that flash starts at 0x08000000, that RAM is 128 KB, that the vector table must come first, and that initialized data needs two addresses. The linker script is where you say all of that. It is also the file that decides whether a crash counter survives a reset, whether a flash-programming routine runs from RAM, and whether a 20 KB code-size surprise is caught at build time. This lesson explains what the linker does, walks through every construct you need in a GNU ld script, shows how to read the map file, and ends with the complete annotated link.ld used for the rest of the course.

The compiler turns each .c file into an object file containing sections (.text.main, .data.g_mode, .bss.g_ticks, …), a symbol table (names defined here, names needed from elsewhere), and relocations (places in the code where an address must be filled in later). None of it has a final address yet. The linker does three jobs:

  1. Symbol resolution. Every undefined symbol in one object must be defined in another object or library. Reset_Handler calls main; main.o defines it. A strong definition overrides a weak one, as you saw with exception handlers. Archives (.a files) are searched only for members that resolve a still-undefined symbol.
  2. Section placement. Input sections from all objects are gathered into output sections (.text, .data, …) and each output section is given an address inside a memory region. This is what the script controls.
  3. Relocation. With every address known, the linker patches each reference: branch offsets, literal pool words, pointers in tables.

You can watch relocations waiting in an unlinked object:

$ arm-none-eabi-objdump -dr startup.o
...
3a: f7ff fffe bl 0 <main>
3a: R_ARM_THM_CALL main
...
5c: 00000000 .word 0x00000000
5c: R_ARM_ABS32 _sdata

The bl has a placeholder offset and an R_ARM_THM_CALL relocation saying “fill in the distance to main”. The literal pool word is zero with an R_ARM_ABS32 relocation saying “put the absolute address of _sdata here”. After linking, objdump -d firmware.elf shows real values in both places.

A GNU ld script for a microcontroller has three main parts:

ENTRY(Reset_Handler) /* entry point recorded in the ELF header */
MEMORY /* what memory exists */
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS /* what goes where */
{
.text : { *(.text*) } > FLASH
}

ENTRY(symbol) sets the ELF entry point. A Cortex-M core ignores it at reset (it uses the vector table), but debuggers use it, and it is a root for garbage collection. Use Reset_Handler.

Each line names a region, gives attributes, an origin, and a length. K and M suffixes multiply by 1024 and 1024 × 1024. The attributes (r read, w write, x execute) do not program any protection hardware; they only guide where the linker places orphan sections, input sections that no rule in SECTIONS mentions. The STM32F407 has three regions worth describing:

MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}

CCMRAM gets rw without x because on the STM32F407 the core-coupled memory is connected only to the core’s data bus. It can hold data and stacks, but the core cannot fetch instructions from it, and the DMA controllers cannot reach it at all.

Inside SECTIONS, each output section description looks like this:

.name [address] [(type)] :
{
input section patterns and symbol assignments
} > VMA_REGION [AT> LMA_REGION]

The patterns select input sections by file and section name:

PatternMatches
*(.text)Section .text from every input file
*(.text*).text, .text.main, .text.Reset_Handler, …
*(.rodata .rodata*)Several patterns at once
startup.o(.text*)Only from startup.o
*(EXCLUDE_FILE(*slow.o) .text*)Every file except slow.o
KEEP(*(.isr_vector))Same as without KEEP, but immune to --gc-sections
*(SORT(.init_array.*))Matching sections sorted by name

An input section goes to the first rule that matches it, in script order. That is why specific rules (.isr_vector) come before general ones.

. is the location counter: the current address inside the output section being built. Assigning a symbol to it records an address; assigning to it moves it.

.bss :
{
. = ALIGN(4); /* round the location counter up to a multiple of 4 */
_sbss = .; /* symbol = current address */
*(.bss*)
. = ALIGN(4);
_ebss = .;
} > RAM

ALIGN(4) matters whenever startup code processes a range a word at a time. If _ebss were not aligned, the zeroing loop’s last word could reach past the end of .bss, or stop short of it.

Any assignment in a script creates a symbol that C code can reference by address, as Reset_Handler does with _sdata. PROVIDE(name = value); defines name only if the program references it and does not define it itself, which is how a script offers defaults (end, used by the C library’s _sbrk) without clashing with a definition in code. PROVIDE_HIDDEN does the same but keeps the symbol out of the dynamic symbol table, irrelevant on bare metal.

Useful built-in functions:

FunctionValue
ORIGIN(region), LENGTH(region)Start and size of a MEMORY region
ADDR(.sec)Run-time address (VMA) of an output section
LOADADDR(.sec)Load address (LMA) of an output section
SIZEOF(.sec)Size of an output section
ALIGN(n)Location counter rounded up to a multiple of n

The stack top is the canonical example:

_estack = ORIGIN(RAM) + LENGTH(RAM); /* 0x20020000 */

Every output section has two addresses. The VMA (virtual memory address, despite the name there is no virtual memory here) is where the section is when the program runs; it is the address the code uses. The LMA (load memory address) is where the section’s bytes are stored in the image that gets programmed. For code and constants they are the same: code runs from flash where it was programmed. For .data they differ:

_sidata = LOADADDR(.data);
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} > RAM AT> FLASH

> RAM assigns the VMA in RAM, so the code refers to g_mode at 0x20000000. AT> FLASH assigns the LMA at the next free address in flash. The linker writes the initial values to the LMA; nothing ever puts them at the VMA except Reset_Handler’s copy loop, which reads from _sidata (the LMA) and writes from _sdata to _edata (the VMA range).

ELF file / flash image RAM at run time
+-------------------------+
0x08000000 | .isr_vector |
0x08000188 | .text |
| .rodata |
0x0800035C | .data contents (LMA) |----copy---> 0x20000000 .data (VMA)
+-------------------------+ 0x20000008 .bss (zeroed)
...
0x20020000 _estack

objdump -h shows both addresses for each section (abridged):

$ arm-none-eabi-objdump -h firmware.elf
Idx Name Size VMA LMA File off Algn
0 .isr_vector 00000188 08000000 08000000 00010000 2**2
1 .text 000001d4 08000188 08000188 00010188 2**2
5 .data 00000008 20000000 0800035c 00020000 2**2
6 .bss 00000010 20000008 20000008 00020008 2**2

.data runs at 20000000 and is stored at 0800035c, right after .text (0x08000188 + 0x1d4). .bss has equal VMA and LMA, but it is a NOBITS section: it has no contents in the file at all, only a size.

Two ways this goes wrong. If you forget AT> FLASH, the LMA defaults to the VMA, the initial values are “loaded” into RAM addresses that no flash programmer can write, and objcopy -O binary produces a file of nearly 400 MB because it must span from 0x08000000 to 0x20000000. If you give .data a fixed LMA with AT(0x08000400) and the code grows past it, the linker reports that .data’s LMA overlaps .text’s.

The (NOLOAD) type on an output section means: allocate addresses, but put no contents in the image and nothing to load. .bss already behaves this way because the compiler emits its input sections as NOBITS, so it needs no keyword; what makes .bss zero is the startup loop over _sbss to _ebss, not the linker.

.noinit is RAM that is neither loaded nor zeroed: it keeps whatever it held before a reset. Because the RAM contents survive a watchdog or software reset (power stays on), it is the standard place for crash records and reset counters.

.noinit (NOLOAD) :
{
. = ALIGN(4);
*(.noinit)
*(.noinit*)
. = ALIGN(4);
} > RAM

It sits outside the _sbss.._ebss range, so Reset_Handler never touches it.

crash_record.c
#include <stdint.h>
#define CRASH_MAGIC 0xC0DEFEEDu
typedef struct {
uint32_t magic;
uint32_t reset_count;
uint32_t last_fault_pc;
} crash_record_t;
__attribute__((section(".noinit")))
static crash_record_t g_crash;
uint32_t crash_record_boot(void)
{
if (g_crash.magic != CRASH_MAGIC) { /* power-on: RAM is random */
g_crash.magic = CRASH_MAGIC;
g_crash.reset_count = 0u;
g_crash.last_fault_pc = 0u;
}
g_crash.reset_count++;
return g_crash.reset_count;
}

After a power-on the contents are random, so the magic word decides whether the record is valid. A random 32-bit value matches the magic with odds of about 1 in 4 billion; production code adds a CRC over the record, as Watchdogs, faults, and recovering from them does. The location of .noinit also moves whenever .bss grows, which is fine within one firmware image but breaks a bootloader that expects the record at a fixed address. For boot-to-application handoff, give it its own MEMORY region at a fixed address.

Garbage collection: -ffunction-sections, --gc-sections, KEEP

Section titled “Garbage collection: -ffunction-sections, --gc-sections, KEEP”

By default the compiler puts all of a file’s functions into one .text section, and the linker can only include or discard whole input sections. With -ffunction-sections -fdata-sections, every function and every variable gets its own section (.text.uart_init, .bss.g_ticks), and -Wl,--gc-sections lets the linker discard every section that nothing reachable references. Reachability starts from the entry point and from every KEEP section, and follows relocations.

The vector table is the section that proves why KEEP exists. Nothing in the code references vector_table; only the hardware does. Without KEEP(*(.isr_vector)), garbage collection removes it, flash starts with whatever came next, and the chip faults at reset. The same applies to .init_array tables and to any data that only a bootloader or host tool reads. To see what was removed, add -Wl,--print-gc-sections, or read the “Discarded input sections” part of the map file.

Code normally executes from flash. Sometimes it must not: while the STM32F407 erases or programs its single flash bank, any instruction fetch from flash stalls until the operation finishes, which can take seconds for a 128 KB sector. A routine that must keep running during that time, or the routine that polls the flash controller itself, has to live in RAM. Flash, EEPROM, and non-volatile storage builds the full driver; here is the linker side.

#include <stdint.h>
#define FLASH_SR (*(volatile uint32_t *)0x40023C0Cu)
#define FLASH_SR_BSY (1u << 16)
__attribute__((section(".ramfunc"), noinline, long_call))
void flash_wait_idle(void)
{
while (FLASH_SR & FLASH_SR_BSY) {
}
}

The script puts .ramfunc input sections inside .data, so they get a RAM VMA and a flash LMA and are copied to RAM by the same loop that copies initialized variables:

.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
*(.ramfunc)
*(.ramfunc*)
. = ALIGN(4);
_edata = .;
} > RAM AT> FLASH
  • long_call. A Thumb bl reaches about ±16 MB. Flash at 0x08000000 and RAM at 0x20000000 are 384 MB apart. long_call makes callers load the full address into a register and use blx. Without it, GNU ld inserts a small veneer (a trampoline it generates automatically) for out-of-range calls, which also works but adds an indirection you might not expect.
  • noinline. An inlined copy would sit in the caller, in flash, defeating the purpose.
  • Everything it touches must be in RAM too. The literal pool holding 0x40023C0C is part of the function’s section, so it travels with it. A call to a library function or a switch that jumps through a table in .rodata would fetch from flash and stall.

Recent versions of GNU ld then print warning: firmware.elf has a LOAD segment with RWX permissions, because the RAM segment now contains both writable data and executable code. Here that is intentional, and -Wl,--no-warn-rwx-segments silences it; without RAM functions, the warning means something executable landed somewhere it should not.

Sometimes data must be at an address chosen in advance: calibration constants in the last flash sector that a factory tool writes, or a version header a bootloader reads at a known offset. Give it a memory region of its own and a section that only it uses. The STM32F407’s last flash sector, sector 11, is 128 KB at 0x080E0000:

MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 896K /* sectors 0-10 */
CALIB (r) : ORIGIN = 0x080E0000, LENGTH = 128K /* sector 11 */
RAM (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
/* ... other sections ... */
.calib :
{
KEEP(*(.calib))
} > CALIB
}
#include <stdint.h>
typedef struct {
uint32_t magic;
int16_t offset;
int16_t gain;
} calibration_t;
__attribute__((section(".calib"), used))
const calibration_t g_calibration = { 0xCA1B0001u, 0, 1024 };

Shrinking FLASH to 896 KB is the important part: if code ever grows into sector 11, the link fails with a region overflow instead of silently overlapping the calibration data. If the factory writes the calibration and the application image must not overwrite it, mark the section (NOLOAD): the address is reserved and the symbol exists, but flashing the application leaves the sector alone.

The same technique, a dedicated region plus a NOLOAD section, places a RAM buffer at a fixed address. Another option, .calib 0x080E0000 : { ... }, sets the section’s address directly inside an existing region; it works but makes the linker report confusing overlaps when code grows into it, so prefer a separate region.

The 64 KB of CCM RAM at 0x10000000 is fast (zero wait states, on its own bus, so it does not compete with DMA traffic on the main SRAM) but data-only and invisible to DMA. Good uses: the main stack, RTOS task stacks, large work buffers that the CPU processes but DMA never touches.

.ccmram (NOLOAD) :
{
. = ALIGN(4);
*(.ccmram)
*(.ccmram*)
. = ALIGN(4);
} > CCMRAM
__attribute__((section(".ccmram")))
static float fft_work[4096]; /* 16 KB, CPU only */

Because the section is NOLOAD and outside .bss, variables placed there are not zeroed and cannot have initializers that take effect. Initialize them in code, or extend Reset_Handler with a third loop over symbols you define around .ccmram. Never place a DMA buffer in CCM: the transfer will fail with a DMA error, or silently do nothing, depending on the peripheral. To put the main stack in CCM, set _estack = ORIGIN(CCMRAM) + LENGTH(CCMRAM);, and then remember that local arrays can no longer be handed to DMA.

Section titled “Stack and heap, and checking them at link time”

The main stack occupies the space between the end of the last RAM section and _estack. Nothing stops the stack from growing down into .bss at run time, but you can at least make the build fail if the space left is smaller than you budgeted:

_Min_Heap_Size = 0x0000; /* no heap unless you ask for one */
_Min_Stack_Size = 0x1000; /* 4 KB budget for the main stack */
.heap (NOLOAD) :
{
. = ALIGN(8);
PROVIDE(end = .); /* the C library's _sbrk starts the heap here */
_heap_start = .;
. = . + _Min_Heap_Size;
_heap_end = .;
} > RAM
ASSERT(_estack - _heap_end >= _Min_Stack_Size, "RAM: not enough left for the stack")

ASSERT(expression, "message") is evaluated at link time; if the expression is false, the link fails with your message. It is the linker’s _Static_assert. The stack budget itself comes from measurement, -fstack-usage and stack painting, covered in Memory: where everything lives and later in Memory protection, stack safety, and robustness.

The end symbol matters if anything calls malloc: newlib’s malloc grows the heap through _sbrk, and the stub _sbrk from nosys.specs starts at end and does little or no checking against the stack. If you use the heap, write your own _sbrk that refuses to go past _heap_end.

-Wl,-Map=firmware.map writes a report of every decision the linker made. It is long, but four parts answer most questions.

Why a library object was pulled in:

Archive member included to satisfy reference by file (symbol)
.../thumb/v7e-m+fp/hard/libc_nano.a(libc_a-memset.o)
startup.o (memset)
.../thumb/v7e-m+fp/hard/libc_nano.a(libc_a-memcpy-stub.o)
startup.o (memcpy)

Read it as “this archive member was included because this file referenced this symbol”. When a 20 KB printf implementation appears, this section tells you which of your files called it. (The path shows the multilib the toolchain chose: Thumb, ARMv7E-M, FPU, hard-float ABI, matching the -mcpu, -mfpu, and -mfloat-abi flags passed at link time. Exact member names vary between toolchain versions.)

What was garbage collected, under “Discarded input sections”.

The memory regions, under “Memory Configuration”.

Where every input section went, under “Linker script and memory map”:

.isr_vector 0x08000000 0x188
*(.isr_vector)
.isr_vector 0x08000000 0x188 startup.o
0x08000000 vector_table
.text 0x08000188 0x1d4
*(.text*)
.text.Default_Handler
0x08000188 0x2 startup.o
0x08000188 Default_Handler
.text.Reset_Handler
0x0800018a 0x5c startup.o
0x0800018a Reset_Handler
.text.main 0x080001e6 0x50 main.o
0x080001e6 main
.text.memset 0x08000238 0x10 .../libc_nano.a(libc_a-memset.o)
0x08000238 memset
...

Each output section lists the patterns from the script, then each input section that matched with its address, size, and source file, then the symbols inside it. To answer “where did my 20 KB go”, search the map for large sizes, or let nm sort them:

Terminal window
arm-none-eabi-nm --size-sort -S -r firmware.elf | head -20

The largest symbols come first. Typical surprises are _dtoa_r and _printf_float (floating-point printf, several KB), __aeabi_ddiv and friends (software double-precision math, because a double crept in), and large const tables. Performance and code size optimization turns this into a method.

For a quick summary on every build, add -Wl,--print-memory-usage:

Memory region Used Size Region Size %age Used
FLASH: 708 B 1 MB 0.07%
RAM: 16 B 128 KB 0.01%
CCMRAM: 0 B 64 KB 0.00%

RAM here counts only sections: .data, .bss, .noinit, and the heap reservation. The stack is not a section, which is why the ASSERT above is worth having.

.../arm-none-eabi/bin/ld: main.o: in function `main':
main.c:(.text.main+0x12): undefined reference to `uart_init'
collect2: error: ld returned 1 exit status

main calls uart_init and no object or library defines it. Usually a .c file missing from SRCS, a misspelled name, a function declared static in its own file, or (when mixing C and C++) name mangling. The +0x12 is the offset of the call inside main’s section, which objdump -d can show you.

.../ld: firmware.elf section `.text' will not fit in region `FLASH'
.../ld: region `FLASH' overflowed by 2840 bytes

The sections assigned to FLASH add up to more than its LENGTH. Either the code really grew (read the map file) or a region is smaller than you think, for example because you reserved sectors for calibration data.

.../ld: section .data LMA [000000000800c000,000000000800c00f] overlaps section .text LMA [0000000008000188,000000000800c2ff]

Two sections were given overlapping load addresses. This comes from a hard-coded AT(address) or a fixed section address that code later grew into. Use AT> FLASH and let the linker pack sections.

.../ld: uart.o:(.bss.g_counter+0x0): multiple definition of `g_counter'; main.o:(.bss.g_counter+0x0): first defined here

A variable was defined (not just declared extern) in a header included by two files. GCC 10 and later default to -fno-common, which turns this old habit into an error, as it should.

.../ld: undefined reference to `__aeabi_uldivmod'

A 64-bit division needs a helper from libgcc, and you linked with -nostdlib. Link libgcc (-lgcc) or drop -nostdlib.

.../ld: warning: _write is not implemented and will always fail

Recent toolchains print this when a nosys.specs stub ends up in the image, typically because something calls printf. Not an error, but a reminder: The C standard library on a microcontroller shows how to implement _write for a UART.

.../ld: RAM: not enough left for the stack

Your own ASSERT fired. Reduce static RAM use or the stack budget, after reading the map file to see what grew.

This is the script used from here on. It is compatible with both the minimal startup.c from Blinky from nothing and the full one from Startup code and the vector table: the symbol names are the same.

link.ld
/* link.ld: STM32F407VG, 1 MB flash, 128 KB SRAM, 64 KB CCM RAM */
ENTRY(Reset_Handler)
_Min_Heap_Size = 0x0000; /* bytes reserved for malloc (none) */
_Min_Stack_Size = 0x1000; /* main stack budget, checked below */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /* initial MSP: 0x20020000 */
SECTIONS
{
/* Vector table: first in flash, so the core finds it at reset. */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* Code. */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
. = ALIGN(4);
_etext = .;
} > FLASH
/* Constants and string literals. */
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata*)
. = ALIGN(4);
} > FLASH
/* Unwind tables: used by C++ exceptions and some libgcc routines. */
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} > FLASH
.ARM.exidx :
{
__exidx_start = .;
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
__exidx_end = .;
} > FLASH
/* Constructor tables, walked by Reset_Handler. */
.preinit_array :
{
. = ALIGN(4);
__preinit_array_start = .;
KEEP(*(.preinit_array*))
__preinit_array_end = .;
} > FLASH
.init_array :
{
. = ALIGN(4);
__init_array_start = .;
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array*))
__init_array_end = .;
} > FLASH
/* Where the initial values of .data are stored in flash. */
_sidata = LOADADDR(.data);
/* Initialized data and RAM functions: run in RAM, stored in flash. */
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
*(.ramfunc)
*(.ramfunc*)
. = ALIGN(4);
_edata = .;
} > RAM AT> FLASH
/* Zero-initialized data, cleared by Reset_Handler. */
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
/* Survives resets: neither loaded nor zeroed. */
.noinit (NOLOAD) :
{
. = ALIGN(4);
*(.noinit)
*(.noinit*)
. = ALIGN(4);
} > RAM
/* Heap: from 'end' upward. The stack grows down from _estack. */
.heap (NOLOAD) :
{
. = ALIGN(8);
PROVIDE(end = .);
PROVIDE(_end = .);
_heap_start = .;
. = . + _Min_Heap_Size;
_heap_end = .;
} > RAM
/* CCM RAM: CPU-only data, not reachable by DMA, not initialized. */
.ccmram (NOLOAD) :
{
. = ALIGN(4);
*(.ccmram)
*(.ccmram*)
. = ALIGN(4);
} > CCMRAM
}
/* Link-time checks */
ASSERT(ADDR(.isr_vector) == ORIGIN(FLASH), "vector table must start at the beginning of flash")
ASSERT(SIZEOF(.isr_vector) > 0, "vector table missing: check KEEP and the section attribute")
ASSERT(_estack - _heap_end >= _Min_Stack_Size, "RAM: not enough left for the stack")

Reading it top to bottom:

  • Sizes first. _Min_Heap_Size and _Min_Stack_Size are plain symbols (type A, absolute, in nm), placed at the top so they are easy to find and change.
  • Flash order. Vector table, code, constants, unwind tables, constructor tables, then the .data load image. Every section ends 4-byte aligned, so _sidata is aligned for the word copy.
  • .ARM.extab and .ARM.exidx. C code rarely needs them, but parts of libgcc carry unwind entries, and without a rule they become orphans that the linker places wherever it sees fit. Naming them keeps the layout predictable.
  • *(.text) before *(.text*). Harmless redundancy that keeps any object compiled without -ffunction-sections (some libraries) grouped first.
  • *(COMMON). Old-style tentative definitions from code compiled with -fcommon. Not produced by modern GCC by default, but libraries may contain them.
  • RAM order. .data, .bss, .noinit, heap; the stack takes what is left at the top. With the heap size at 0, all free RAM belongs to the stack.
  • The assertions. The first two catch the most common way to brick a build (a vector table that moved or vanished) before it reaches a board. The third enforces the stack budget.
  • Omitting KEEP around the vector table or .init_array while using --gc-sections. Fix: KEEP every section that only hardware or startup code reads.
  • Forgetting AT> FLASH on .data, producing a huge .bin and globals that never get their initial values. Fix: always > RAM AT> FLASH for .data.
  • Unaligned section boundaries breaking word-copy loops. Fix: . = ALIGN(4); before every start and end symbol that startup code uses.
  • Putting DMA buffers in CCM RAM. Fix: keep DMA buffers in .data/.bss in main SRAM, and reserve CCM for CPU-only data.
  • Expecting .noinit contents to be valid after power-on. Fix: validate with a magic word and a CRC.
  • Calling flash-resident code from a RAM function that must run during a flash erase. Fix: keep RAM functions self-contained and check the disassembly for bl into 0x08... addresses.
  • Treating “RAM used” from --print-memory-usage as total RAM. Fix: the stack is not included; budget it with ASSERT.
  1. Build the project with the complete link.ld and run arm-none-eabi-objdump -h firmware.elf. Find .data and check that its LMA equals the address of _sidata from arm-none-eabi-nm firmware.elf, and that its VMA equals _sdata.
  2. Add -Wl,--print-memory-usage and -Wl,--print-gc-sections to LDFLAGS. Add an unused function to main.c and confirm in the output that its section is removed. Then call it from main and watch FLASH usage grow by its size.
  3. Implement the .noinit crash record. Increment reset_count at boot and trigger a software reset every 2 seconds with SCB->AIRCR = SCB_AIRCR_VECTKEY | SCB_AIRCR_SYSRESETREQ;. In gdb, watch the counter climb across resets; then power-cycle the board and observe it start again from 1.
  4. Add printf("%f\n", 1.5); to main with -u _printf_float in LDFLAGS (it will not output anything yet). Compare size before and after, then use the map file’s “Archive member included” section and nm --size-sort -S -r to list the five largest contributors to the growth. Expect well over 10 KB.
  5. Move the main stack into CCM RAM by changing _estack. Update the ASSERT so it still checks something meaningful (hint: the stack is now in a different region from the heap). Verify in gdb after monitor reset halt that msp is 0x10010000, and explain why handing a local array to a DMA transfer would now fail.

Question 1:In .data : { ... } > RAM AT> FLASH, what does LOADADDR(.data) return?

Question 2:After adding -Wl,--gc-sections, the board no longer boots, and nm shows no vector_table symbol. What is missing from the linker script?

Question 3:objcopy -O binary produces a .bin file of about 400 MB. What is the most likely cause?

Question 4:A function placed in .ramfunc is supposed to run while flash is being erased, but the system still stalls. Which bug is most likely?

Question 5:Which statement about a .noinit section placed in RAM with (NOLOAD) is true?

Question 6:The link fails with region 'FLASH' overflowed by 2840 bytes right after you shrank FLASH to 896K to reserve sector 11. What does this tell you?

  • The linker resolves symbols, places sections into memory regions, and patches relocations; the linker script controls placement.
  • MEMORY describes the chip, SECTIONS maps input sections to output sections, and the first matching pattern wins.
  • .data has a run-time address in RAM (> RAM) and a load address in flash (AT> FLASH); LOADADDR, _sdata, and _edata give startup code what it needs to copy.
  • KEEP protects sections that only hardware or startup code use from --gc-sections.
  • NOLOAD reserves space without contents: .noinit for data that survives reset, CCM RAM for CPU-only buffers.
  • The map file explains every byte: which archive members were pulled in and why, what was discarded, and where each section landed.
  • ASSERT and dedicated regions turn layout mistakes (missing vector table, stack too small, code overlapping reserved flash) into build errors.

Next: The Cortex-M architecture, where the core’s registers, exception model, and NVIC are explained in full.