Search topics...
Build Systems
advanced
Weight: 4/10

Linker Scripts

Master the linker script: MEMORY and SECTIONS blocks, the location counter, KEEP/ALIGN, and how to place code and data exactly where you want it.

build-systems
linker
linker-script
memory
sections
ld
Loading quiz status...

Quick Cap

A linker script is a recipe that tells the linker (ld) how to assemble object files into a final binary: which physical memory regions exist, which sections go where, and what alignment / ordering is required. For embedded targets — where you must put .text in Flash and .data in RAM, position a vector table at exactly address 0, and place a DMA buffer on a 32-byte alignment — the linker script is the single source of truth for memory layout. Interviewers test whether you can read one fluently and write a basic one from scratch.

Key Facts:

  • Two top-level blocks: MEMORY (define physical regions) and SECTIONS (place sections into regions)
  • The location counter . represents the current address; assigning to it inserts padding
  • >REGION sets the VMA (where the CPU sees it); AT>REGION sets the LMA (where it physically lives)
  • KEEP(*(.section)) prevents the linker from garbage-collecting the section
  • ALIGN(N) advances . to the next N-byte boundary
  • Linker-defined symbols (_sdata, _ebss, _estack) are how startup code knows what to copy

Deep Dive

At a Glance

ElementPurposeExample
MEMORY { ... }Declare physical memory regionsFLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
SECTIONS { ... }Place sections into regions.text : { *(.text*) } >FLASH
. (location counter)Current address being filled_sdata = .;
>REGIONSet VMA region} >RAM
AT>REGIONSet LMA region (different from VMA)} >RAM AT>FLASH
LOADADDR(.section)Get LMA of a section_sidata = LOADADDR(.data);
KEEP(...)Prevent garbage-collectionKEEP(*(.isr_vector))
ALIGN(N)Round . up to N-byte boundary. = ALIGN(8);
PROVIDE(sym = ...)Define symbol only if not defined elsewherePROVIDE(_etext = .);
ENTRY(symbol)Set entry point in ELF headerENTRY(Reset_Handler)

MEMORY: Declaring Physical Regions

The MEMORY block declares where things can go. Each entry has a name, attributes (r=readable, w=writable, x=executable), origin, and length:

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

The attributes are advisory — the linker uses them to warn if you put writable data in a region marked read-only, but won't stop you. They reflect the chip's actual permissions at the bus level.

The LENGTH is the cap. If your sections together exceed the region size, you'll see:

text
region 'FLASH' overflowed by 4096 bytes

This is one of the most common embedded build errors and the entry point to a binary-size investigation (covered in the ELF, Map & Binary Inspection page).

SECTIONS: Placing Output Sections

The SECTIONS block is processed top to bottom. For each output section, you specify which input sections to gather (using glob patterns), and which region to place them in:

text
SECTIONS
{
.isr_vector : {
KEEP(*(.isr_vector))
} >FLASH
.text : {
*(.text)
*(.text.*)
*(.rodata)
*(.rodata.*)
_etext = .;
} >FLASH
.data : {
_sdata = .;
*(.data)
*(.data.*)
_edata = .;
} >RAM AT>FLASH
_sidata = LOADADDR(.data);
.bss : {
_sbss = .;
*(.bss)
*(.bss.*)
*(COMMON)
_ebss = .;
} >RAM
}

A few things to note:

  • *(.text) means "the .text input section from any input file." With -ffunction-sections, individual functions live in .text.funcname, hence the *(.text.*) pattern.
  • The order of input-section patterns matters when your .text contains things that must come first. The vector table goes in its own .isr_vector section, placed first.
  • Linker-defined symbols (_etext, _sdata, etc.) are just labels at the current location — they become C symbols you can reference from startup code.

The Location Counter

The single most important concept in linker scripts is the location counter, written as .. It represents the current address being filled. When the linker encounters an input section, it places that section at . and advances . by the section's size.

You can also assign to . directly to insert padding or create gaps:

text
. = ALIGN(8); /* round up to 8-byte boundary */
. = . + 256; /* reserve 256 bytes of stack */
. = ORIGIN(RAM) + LENGTH(RAM); /* jump to top of RAM (e.g., for stack) */

Reading from . gives you the current address — that's how _sdata = .; captures the address where .data will start.

VMA vs LMA: The AT> Directive

For sections that need to be stored in one region but executed/accessed from another (the canonical case being .data), the linker script uses two addresses:

  • VMA (Virtual Memory Address) = where the CPU expects it at runtime
  • LMA (Load Memory Address) = where it physically lives in the binary

>RAM sets the VMA, AT>FLASH sets the LMA:

text
.data : {
_sdata = .; /* . refers to VMA (RAM) here */
*(.data*)
_edata = .;
} >RAM AT>FLASH /* placed in RAM at runtime, but stored in FLASH */

The startup code uses _sidata = LOADADDR(.data) to find the Flash address of the initial values and copies them to _sdata in RAM. This whole mechanism is the subject of the Memory Layout & Startup page.

KEEP and Garbage Collection

When linking with --gc-sections (the section-level garbage collector), the linker drops any section not reachable from the entry point. This is great for unused code — but the vector table is a special case: nothing in your C code calls the ISR addresses directly, so the linker would happily collect them. Wrap it in KEEP():

text
.isr_vector : {
KEEP(*(.isr_vector))
} >FLASH

Other things that need KEEP():

  • Custom sections you reference only by linker symbol (e.g., a firmware version block)
  • C++ static initializers (*(.init_array))
  • Any section the program needs but the linker can't see a reference to

ALIGN: Boundaries Matter

Many things need specific alignment:

  • The vector table needs power-of-2 alignment matching its size for VTOR
  • DMA buffers often need cache-line alignment (32 bytes on Cortex-M7)
  • Some peripherals require specific alignment for register-based DMA descriptors
  • Stack should typically be 8-byte aligned per AAPCS (ARM Architecture Procedure Call Standard)
text
.isr_vector : {
. = ALIGN(512); /* VTOR alignment for ~100 IRQs */
KEEP(*(.isr_vector))
} >FLASH
.dma_buffer : {
. = ALIGN(32); /* cache-line alignment for Cortex-M7 */
*(.dma_buffer)
} >RAM

Placing Code or Data at Fixed Addresses

Two ways:

Option 1 — fixed address inside SECTIONS:

text
.fw_metadata 0x0801FF00 : {
KEEP(*(.fw_metadata))
} >FLASH

The 0x0801FF00 overrides the location counter — the section starts at that exact address. Useful for firmware version blobs at a known offset.

Option 2 — section attribute in C plus catch-all in linker script:

c
__attribute__((section(".fw_metadata")))
const struct fw_meta meta = { .ver = 1, .crc = 0xCAFE };

The C attribute names a custom section; the linker script places it. This pattern is what you typically use for DMA buffers, NOLOAD sections, and any "I want this at a specific address" need.

A Realistic STM32-Class Example

A complete (but minimal) script for an STM32F4 with 512 KB Flash and 128 KB RAM:

text
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /* top of RAM */
SECTIONS
{
.isr_vector : {
. = ALIGN(512);
KEEP(*(.isr_vector))
} >FLASH
.text : {
*(.text)
*(.text.*)
*(.rodata)
*(.rodata.*)
*(.glue_7)
*(.glue_7t)
KEEP(*(.init))
KEEP(*(.fini))
. = ALIGN(4);
_etext = .;
} >FLASH
/* C++ static constructors */
.init_array : {
_sinit = .;
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
_einit = .;
} >FLASH
_sidata = LOADADDR(.data);
.data : {
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data.*)
. = ALIGN(4);
_edata = .;
} >RAM AT>FLASH
.bss : {
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >RAM
._user_heap_stack : {
. = ALIGN(8);
PROVIDE(end = .); /* sbrk uses 'end' as heap start */
PROVIDE(_end = .);
. = . + 0x1000; /* 4 KB heap reservation */
. = . + 0x1000; /* 4 KB stack reservation */
. = ALIGN(8);
} >RAM
}

Reading this top to bottom, every line has a purpose. The vector table is aligned and KEEP'd so VTOR can point to it. .text and .rodata go to Flash. .data has VMA in RAM and LMA in Flash via AT>FLASH. .bss is RAM-only. The stack/heap reservation makes sure the linker complains if RAM is over-committed.

Common Failure Modes

SymptomCauseFix
region 'FLASH' overflowed by N bytes.text + .rodata + .data init > Flash sizeReduce code size; add --gc-sections; or get a bigger MCU
Vector table at wrong address (boot hangs)Missing KEEP() or wrong >FLASH placementAdd KEEP(*(.isr_vector)); verify it's the first section in >FLASH
.bss not zeroed (globals start as garbage)Startup code missing or _sbss/_ebss symbols not exportedVerify symbols match between linker script and Reset_Handler
HardFault on first FPU instructionFPU not enabled (separate issue, but stack alignment can mask it)Set CPACR; ensure stack ALIGN(8)
DMA garbage readsCache coherency / unaligned bufferMove buffer to .dma_buffer section with ALIGN(32)
⚠️Order matters in SECTIONS

The order of output sections in the script is the order they're laid out in memory. The vector table must come first in >FLASH. C++ static initializers (.init_array) need to be reachable before main(). Always start with .isr_vector.

Debugging Story: The Vanishing ISR Table

A team enabled -ffunction-sections -fdata-sections -Wl,--gc-sections to shave Flash usage. The build size dropped beautifully — and the firmware refused to boot. JTAG showed the vector table at address 0x08000000 was full of zeros: the linker had garbage-collected it because nothing in C code referenced any ISR by symbol name (the CPU jumps to them based on the table contents).

The fix was a single line in the linker script:

text
.isr_vector : {
KEEP(*(.isr_vector))
} >FLASH

KEEP() tells the GC pass: "don't drop this section even if nothing references it." A 1-line fix that prevents an entire bring-up disaster.

The lesson: Section-level GC is valuable but blind. Anything the linker can't see a reference to — vector tables, .init_array, custom metadata sections — needs KEEP() to survive.

What Interviewers Want to Hear

  • You can read a basic linker script and explain every block
  • You know the difference between MEMORY and SECTIONS
  • You understand the location counter . and how to use ALIGN
  • You can describe VMA vs LMA and the >REGION AT>REGION syntax
  • You know KEEP() is required for the vector table when using --gc-sections
  • You can sketch a script for placing a DMA buffer at a specific alignment

Interview Focus

Classic Interview Questions

Q1: "What does a linker script define and what are its two main blocks?"

Model Answer Starter: "A linker script tells the linker how to assemble object files into the final binary — what physical memory exists, where each section goes, and what alignment is required. The two main blocks are MEMORY, which declares physical regions like FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K and RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K, and SECTIONS, which maps input sections into output sections placed inside those regions. So .text goes into FLASH; .data has its VMA in RAM but its LMA in FLASH via the AT>FLASH directive; .bss goes in RAM only and is zeroed by startup code."

Q2: "What is the location counter and how do you use it?"

Model Answer Starter: "The location counter . represents the address the linker is currently filling. When you place an input section, the counter advances by the section's size. You can read from . to define linker symbols like _sdata = .;, and you can assign to it to insert padding or alignment: . = ALIGN(8); rounds up to 8-byte boundary, . = . + 256; inserts 256 bytes of padding, . = ORIGIN(RAM) + LENGTH(RAM); jumps to a fixed address. Linker symbols defined in the script become C symbols startup code can reference — that's how Reset_Handler knows where to copy .data to and from."

Q3: "How would you place a DMA buffer at a fixed address with 32-byte alignment?"

Model Answer Starter: "Two parts. In C, declare the buffer with __attribute__((section(\".dma_buffer\"))) __attribute__((aligned(32))) uint8_t buf[1024]; to tag it into a custom section and request 32-byte alignment. In the linker script, place that section in RAM with explicit alignment: .dma_buffer : { . = ALIGN(32); KEEP(*(.dma_buffer)) } >RAM. The KEEP is needed if you're using --gc-sections because nothing else may reference the buffer by name — the DMA peripheral just reads from its physical address. For a Cortex-M7 with cache, you'd also typically configure the MPU to mark this region as non-cacheable to avoid coherency issues."

Q4: "Why does the vector table need KEEP() in modern build configurations?"

Model Answer Starter: "When you compile with -ffunction-sections -fdata-sections and link with -Wl,--gc-sections, the linker does section-level garbage collection — any section not reachable from the entry point gets dropped. The vector table is special: nothing in C code calls the ISR addresses by symbol name; the CPU jumps to them based on the table contents at runtime. So the linker sees no reference and would garbage-collect the table, leading to a boot hang. Wrapping the input section pattern in KEEP(*(.isr_vector)) tells the GC pass to preserve it. The same applies to .init_array for C++ static constructors."

Q5: "What's the difference between >RAM and >RAM AT>FLASH for the .data section?"

Model Answer Starter: "Just >RAM would place .data only in RAM — but RAM is volatile, so the initial values would be undefined at boot. >RAM AT>FLASH is the canonical pattern: VMA in RAM (where the CPU accesses .data at runtime, because globals must be writable), LMA in FLASH (where the initial values are physically stored in the binary). The startup code uses linker symbols _sidata = LOADADDR(.data) for the Flash source address and _sdata/_edata for the RAM destination, and copies the values from Flash to RAM before main() runs. This gives initialized globals their values without wasting RAM in the static binary."

Trap Alerts

  • Don't say: "The compiler decides where things go" — section placement is the linker's job, controlled by the script
  • Don't forget: KEEP() for vector table when using --gc-sections — easy to miss until the board won't boot
  • Don't ignore: Alignment requirements — VTOR alignment, DMA cache lines, AAPCS stack alignment

Follow-up Questions

  • "What does ENTRY() do in a linker script?"
  • "How do you reserve uninitialized RAM that startup code should NOT zero?"
  • "What's the difference between *(.text) and *(.text*)?"
  • "How do you generate a memory-map output file?"
  • "What is PROVIDE() and when do you use it?"
  • "How do you place specific files into specific sections (e.g., critical code in CCM RAM)?"
💡Practice Build Systems Interview Questions

Ready to test yourself? Head over to the Build Systems Interview Questions page for a full set of Q&A with collapsible answers — perfect for self-study and mock interview practice.

Practice

What does the directive `>RAM AT>FLASH` for the .data section mean?

Why is KEEP() required around `*(.isr_vector)` when using --gc-sections?

What does `. = ALIGN(8);` do in a linker script?

Where do you typically define `_estack` in a Cortex-M linker script?

A region 'FLASH' overflowed by 4096 bytes error indicates what?

Real-World Tie-In

Critical-Code Placement in CCM RAM — A motor-control project needed its inner control loop to run from Core-Coupled Memory (CCM RAM, 0x10000000 on STM32F4) for deterministic timing. The linker script added a CCMRAM region in MEMORY, defined a .ccmram section pulling functions tagged with __attribute__((section(".ccmram"))), and placed it appropriately. Result: control loop jitter dropped from ~200 ns to under 50 ns.

Firmware-Header Block at Fixed Offset — A team's bootloader needed a firmware version, build date, and CRC at exactly Flash address 0x0801FF00 (start of last sector). Solution: a C struct tagged with __attribute__((section(".fw_meta"))) and a linker-script entry .fw_meta 0x0801FF00 : { KEEP(*(.fw_meta)) } >FLASH. Bootloader and diagnostic tools read the block at the known address without needing to parse the ELF.

Stack Overflow Detection via Linker Reservation — A safety-critical project reserves stack and heap as explicit regions in the script with bracketing symbols (_stack_start, _stack_end). At runtime, an MPU region with no-access permissions sits between heap-top and stack-bottom. Stack overflow → MemManage fault → emergency safe shutdown rather than silent memory corruption.

Was this helpful?