Programming · 55 pages
- C language — 15 lessonstutorial
- Lesson 1 — Hello, C
- Lesson 2 — Types & literals
- Lesson 3 — Expressions & operators
- Lesson 4 — Control flow
- Lesson 5 — Functions
- Lesson 6 — Arrays
- Lesson 7 — Pointers
- Lesson 8 — Strings
- Lesson 9 — Structs & unions
- Lesson 10 — Dynamic memory
- Lesson 11 — Preprocessor
- Lesson 12 — Multi-file projects
- Lesson 13 — Standard library
- Lesson 14 — Undefined behavior
- Lesson 15 — Modern C (C11/17/23)
- C++ programming — 15 lessonstutorial
- Lesson 1 — Hello, C++
- Lesson 2 — References & namespaces
- Lesson 3 — Classes & RAII
- Lesson 4 — Inheritance & polymorphism
- Lesson 5 — Templates
- Lesson 6 — STL containers
- Lesson 7 — Iterators & algorithms
- Lesson 8 — Smart pointers
- Lesson 9 — Move semantics
- Lesson 10 — Lambdas
- Lesson 11 — Error handling
- Lesson 12 — Concurrency
- Lesson 13 — Modules (C++20)
- Lesson 14 — Modern C++23
- Lesson 15 — C++ on Cortex-M
- Bare-metal C tutorial — 10 lessonsembedded
- Lesson 1 — Toolchain setup
- Lesson 2 — Bare-metal blink
- Lesson 3 — Registers in C
- Lesson 4 — Memory map
- Lesson 5 — Linker script
- Lesson 6 — Interrupts & ISRs
- Lesson 7 — Bit patterns
- Lesson 8 — Fixed-point math
- Lesson 9 — State machines
- Lesson 10 — Ring buffers + DMA
- Embedded C patterns
- volatile & memory-mapped IO
- CMSIS-Core
- CMSIS-DSP
- Linker scripts
- Startup & vector table
- C++ on Cortex-M
- Rust on Cortex-Mmodern
- Inline assembly
- Bare-metal libc & newlib-nano
- Bootloaders & DFU
- OTA update flow
Linker scripts
GNU ld linker scripts for Cortex-M — memory regions, section placement, AT-load addresses, alignment, and the directives you actually need.
The linker script is what tells the linker where your code, data, and stack live in the physical memory of the chip. Most vendor SDKs ship a default one and you never look at it. The moment you need to place a DMA buffer at a specific SRAM bank, run a function from ITCM, or implement A/B firmware update, you're in the linker script.
The two minimum-viable sections
For a Cortex-M binary, you almost always need:
.textin flash — your code and read-only data..datain RAM (but loaded from flash) — initialised globals. The linker emits both a flash image and a RAM destination, plus markers so the startup code can copy..bssin RAM — zero-initialised globals. The startup code clears it.- A stack in RAM — usually placed at the top of SRAM, growing down.
A minimal STM32F4 linker script:
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
SRAM (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}
_estack = ORIGIN(SRAM) + LENGTH(SRAM);
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text*)
*(.rodata*)
. = ALIGN(4);
_etext = .;
} > FLASH
.data :
{
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} > SRAM AT > FLASH /* placed in SRAM, loaded from FLASH */
_sidata = LOADADDR(.data);
.bss :
{
_sbss = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > SRAM
}That's 95% of what most projects need. Let's unpack the key bits.
MEMORY blocks
Each memory region has:
- Name — used in
> REGIONclauses below. - Attributes in parentheses —
r(readable),w(writable),x(executable). Affects nothing semantically but generates warnings if a section's attributes don't match. - ORIGIN and LENGTH — the actual address range.
You can have as many regions as you want — CCM RAM, ITCM, DTCM, separate flash banks. Most Cortex-M7 STM32 chips need 4+ regions.
SECTIONS and the AT-load trick
.data : { ... } > SRAM AT > FLASHThe > SRAM says "give this section virtual addresses in SRAM". The AT > FLASH says "but its actual bytes go in the FLASH region of the final image". The linker stores the load-time address in the ELF program header.
The result: your binary contains the .data initialisation values in flash, but your symbols (int counter = 5;) have addresses in SRAM. The startup code is responsible for copying flash → SRAM at boot:
extern uint32_t _sdata, _edata, _sidata;
void Reset_Handler(void) {
// Copy .data: flash → SRAM
uint32_t *src = &_sidata, *dst = &_sdata;
while (dst < &_edata) *dst++ = *src++;
// Clear .bss
dst = &_sbss;
while (dst < &_ebss) *dst++ = 0;
// Hand to C runtime
SystemInit();
__libc_init_array(); // run C++ static constructors
main();
}Placing code in ITCM
On Cortex-M7, you want hot ISRs in ITCM. Add a region and a section:
MEMORY
{
ITCM (rx) : ORIGIN = 0x00000000, LENGTH = 16K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
DTCM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
}
SECTIONS
{
.itcm_text :
{
*(.itcm.text*)
} > ITCM AT > FLASH
_sitcm = LOADADDR(.itcm_text);
_eitcm_load = _sitcm + SIZEOF(.itcm_text);
_sitcm_dst = ADDR(.itcm_text);
/* ... */
}Then in C:
__attribute__((section(".itcm.text")))
void TIM2_IRQHandler(void) { /* runs from single-cycle ITCM */ }And the startup code copies the ITCM section from flash to ITCM at boot, the same way it handles .data.
Stack and heap placement
PROVIDE(_estack = ORIGIN(SRAM) + LENGTH(SRAM)); /* top-of-stack */
PROVIDE(_min_stack = 4K);
PROVIDE(_min_heap = 8K);
.stack (NOLOAD) :
{
. = ALIGN(8);
_sstack = .;
. = . + _min_stack;
_estack_check = .;
} > SRAM
ASSERT(_estack_check < _estack, "Stack overflows SRAM")(NOLOAD) means "reserve this space but don't put anything in the binary for it" — useful for stack and uninitialised buffers. The ASSERT is checked at link time and fails the build if the layout is wrong.
_estack is what goes into vector table slot 0 — the initial main stack pointer. Most CMSIS startup files reference it as &_estack.
Putting a buffer at a specific address
__attribute__((section(".dma_buffer")))
static uint8_t rx_buf[1024];MEMORY
{
/* ... */
RAM_D2 (rwx) : ORIGIN = 0x30000000, LENGTH = 288K /* AHB SRAM2 on H7 */
}
SECTIONS
{
/* ... */
.dma_buffer (NOLOAD) :
{
KEEP(*(.dma_buffer))
} > RAM_D2
}The KEEP prevents --gc-sections from discarding the buffer if there are no direct references at link time (some DMA buffers are only referenced through their address — the linker can't tell they're "used").
PROVIDE vs assignment
_estack = ...; /* always defines _estack */
PROVIDE(_estack = ...); /* defines only if not already defined elsewhere */Use PROVIDE for symbols the user code might want to override.
A real-world stm32 linker script size
Vendor linker scripts grow. STMicro's STM32H7 default linker is ~250 lines because of:
- Multiple flash banks (dual-bank A/B for safe updates).
- Multiple SRAM regions (D1, D2, D3 with different cacheability).
- ITCM, DTCM.
- DMA buffer regions in each SRAM bank.
- Optional "no-init" RAM (preserved across resets).
Don't write your own from scratch for a complex chip — start from the vendor's template and modify.
See also
- Startup & vector table — writing the code that runs before
main. - Cortex-M7 — ITCM/DTCM details.
- Memory model — placing cacheable vs non-cacheable buffers.