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) andSECTIONS(place sections into regions) - The location counter
.represents the current address; assigning to it inserts padding >REGIONsets the VMA (where the CPU sees it);AT>REGIONsets the LMA (where it physically lives)KEEP(*(.section))prevents the linker from garbage-collecting the sectionALIGN(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
| Element | Purpose | Example |
|---|---|---|
MEMORY { ... } | Declare physical memory regions | FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K |
SECTIONS { ... } | Place sections into regions | .text : { *(.text*) } >FLASH |
. (location counter) | Current address being filled | _sdata = .; |
>REGION | Set VMA region | } >RAM |
AT>REGION | Set LMA region (different from VMA) | } >RAM AT>FLASH |
LOADADDR(.section) | Get LMA of a section | _sidata = LOADADDR(.data); |
KEEP(...) | Prevent garbage-collection | KEEP(*(.isr_vector)) |
ALIGN(N) | Round . up to N-byte boundary | . = ALIGN(8); |
PROVIDE(sym = ...) | Define symbol only if not defined elsewhere | PROVIDE(_etext = .); |
ENTRY(symbol) | Set entry point in ELF header | ENTRY(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:
MEMORY{FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512KRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128KCCMRAM (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:
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:
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.textinput 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
.textcontains things that must come first. The vector table goes in its own.isr_vectorsection, 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:
. = 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:
.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():
.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)
.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:
.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:
__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:
ENTRY(Reset_Handler)MEMORY{FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512KRAM (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
| Symptom | Cause | Fix |
|---|---|---|
region 'FLASH' overflowed by N bytes | .text + .rodata + .data init > Flash size | Reduce code size; add --gc-sections; or get a bigger MCU |
| Vector table at wrong address (boot hangs) | Missing KEEP() or wrong >FLASH placement | Add 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 exported | Verify symbols match between linker script and Reset_Handler |
| HardFault on first FPU instruction | FPU not enabled (separate issue, but stack alignment can mask it) | Set CPACR; ensure stack ALIGN(8) |
| DMA garbage reads | Cache coherency / unaligned buffer | Move buffer to .dma_buffer section with ALIGN(32) |
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:
.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
MEMORYandSECTIONS - You understand the location counter
.and how to use ALIGN - You can describe VMA vs LMA and the
>REGION AT>REGIONsyntax - 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)?"
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.
