UNIT 05 · LESSON 6 OF 6

Writing a Minimal Startup Sequence

What is the smallest start-up that honestly gives C everything it promises?

INTERACTIVEWhat breaks when a startup step is missing
The steps of a minimal startup sequence with the symptoms of each missing one✓ KEEP() around the vector table✓ correct initial SP in word 0✓ Thumb bit in handler addresses✓ .data copy loop✓ .bss zero loop✓ flash wait states before raising the clockAll steps present: the chip boots, statics hold their C-defined values, and main()starts (on the boot clock until something raises it).
The steps of a minimal startup sequence with the symptoms of each missing one✓ KEEP() around the vector table✓ correct initial SP in word 0✓ Thumb bit in handler addresses✓ .data copy loop✓ .bss zero loop✓ flash wait states before raising the clockAll steps present: the chip boots, statics holdtheir C-defined values, and main() starts (on theboot clock until something raises it).

Try this

All steps present: the startup is complete.

A minimal startup has only a handful of duties, and each one missing fails in its own recognisable way. Some failures stop the chip dead at reset; others let it run with quietly wrong data, often only from a cold power-on and not under a debugger that has just loaded RAM. Turn the steps off one at a time.

What you will be able to do
  • Write a vector table and a reset handler in C for a Cortex-M0+, with weak default handlers.
  • Write the linker-script lines that define _sidata, _sdata, _edata, _sbss, _ebss and _estack, and compute their values for given section sizes.
  • Explain what the used attribute and KEEP() each protect a vector table from.
  • Build a program with its own start-up (-nostartfiles, -T) and list what it deliberately leaves out.
  • Diagnose a start-up failure from its symptom: no boot at all, HardFault at reset, or wrong values only after a cold start.
Before you start
  • The vector table (lesson 3), the memory loops (lesson 4) and the order of start-up steps (lesson 5).
  • MEMORY, SECTIONS, AT> and LOADADDR (unit 4, lesson 4).
Steps in this lesson
  1. The whole thing
  2. Why each line is there
  3. What this start-up leaves out, on purpose
  4. Worked example: the symbols for one program
  5. Common misconceptions

The puzzle

Vendor start-up files run to hundreds of lines, but most of that is a long list of handler names. Strip it down and a working start-up for a Cortex-M0+ is about thirty lines of C and a dozen lines of linker script. Writing it once shows exactly which lines are essential and how a board fails when one is missing. What is the smallest start-up that honestly gives C everything it promises?

STEP 1

The whole thing

Start with the part the hardware reads: a table whose first word is the stack top and whose second is the reset handler, with every other slot pointing at a default.

#include <stdint.h>

extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss, _estack;
int main(void);
void Reset_Handler(void);

void Default_Handler(void) { for (;;) {} }
void NMI_Handler(void)       __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void)   __attribute__((weak, alias("Default_Handler")));

__attribute__((section(".vectors"), used))
void (* const vectors[48])(void) = {
    (void (*)(void))&_estack,       /* 0: initial SP              */
    Reset_Handler,                  /* 1: reset                   */
    NMI_Handler,                    /* 2                          */
    HardFault_Handler,              /* 3                          */
    [11] = Default_Handler,         /* SVCall; 4-10 stay 0        */
    [14] = Default_Handler,         /* PendSV; 12-13 stay 0       */
    [15] = SysTick_Handler,
    [16 ... 47] = Default_Handler,  /* IRQ 0-31 (GNU C ranges)    */
};

void Reset_Handler(void) {
    uint32_t *src = &_sidata, *dst = &_sdata;
    while (dst < &_edata) *dst++ = *src++;           /* copy .data  */
    for (dst = &_sbss; dst < &_ebss; ) *dst++ = 0;   /* zero .bss   */
    main();
    for (;;) {}                                      /* never return */
}

The linker script supplies every symbol the C code uses and puts the table first:

ENTRY(Reset_Handler)
MEMORY {
  FLASH (rx)  : ORIGIN = 0x00000000, LENGTH = 128K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 32K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS {
  .text : { KEEP(*(.vectors)) *(.text*) *(.rodata*) . = ALIGN(4); } > FLASH
  .data : { . = ALIGN(4); _sdata = .; *(.data*) . = ALIGN(4); _edata = .; } > RAM AT> FLASH
  _sidata = LOADADDR(.data);
  .bss (NOLOAD) : { . = ALIGN(4); _sbss = .; *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM
}

Build it with the compiler’s own start-up files switched off and your script in their place: arm-none-eabi-gcc -mcpu=cortex-m0plus -mthumb -O2 -ffunction-sections -fdata-sections -nostartfiles -T min.ld -Wl,--gc-sections start.c main.c -o app.elf. The memory map is illustrative, for a chip that boots from flash at address 0; take the real origins and lengths from your chip’s reference manual. On the RP2040 this start-up could not boot on its own: flash must begin with the 256-byte boot2 and its checksum, with the table after it at 0x1000_0100 (lesson 2).

INTERACTIVEThe symbols a minimal startup depends on
Linker-script symbols with their values for chosen section sizesvectors (word 0 = _estack)0x0000_0000_sidata (.data image in flash)0x0000_1830_sdata0x2000_0000_edata = _sbss0x2000_0100_ebss0x2000_0500_estack (initial SP)0x2000_8000copy 64 words from _sidata to _sdata…_edata; zero 256 words from _sbss to _ebss; stackgrows down from _estack with 31 488 B above .bss.
Linker-script symbols with their values for chosen section sizesvectors (word 0 = _estack)0x0000_0000_sidata (.data image in flash)0x0000_1830_sdata0x2000_0000_edata = _sbss0x2000_0100_ebss0x2000_0500_estack (initial SP)0x2000_8000copy 64 words from _sidata to _sdata…_edata; zero256 words from _sbss to _ebss; stack grows down from_estack with 31 488 B above .bss.
_sidata = 0x0000_1830, _sdata = 0x2000_0000, _edata = _sbss = 0x2000_0100, _ebss = 0x2000_0500, _estack = 0x2000_8000. copy 64 words from _sidata to _sdata…_edata; zero 256 words from _sbss to _ebss; stack grows down from _estack with 31 488 B above .bss.

The linker script defines a handful of symbols and the startup code uses nothing else: where the .data image starts in flash (_sidata), where .data and .bss start and end in RAM, and the top of the stack. Change the section sizes and watch the values the startup loops will use. The memory map is an illustrative 32 KiB of RAM at 0x2000_0000 and flash at 0; the vector table is 48 words.

STEP 2

Why each line is there

  • used and KEEP(). used stops the compiler from dropping the array when nothing references it, which matters if the table is static or the build uses link-time optimisation; KEEP() stops the linker’s --gc-sections from dropping its section. Neither tool sees a reference to the table, so it is safest with both.
  • &_estack in word 0. Script symbols have an address, not a value (unit 4, lesson 4): the C code always takes &. The stack top here is 0x2000_8000, a multiple of 8 as the procedure-call standard expects at calls.
  • Function names, not numbers. The linker sets bit 0 of each Thumb function’s address, so every entry is a valid Thumb vector (lesson 3).
  • The loops before main(). They are the only thing that makes .data and .bss match C (lesson 4). _sdata, _edata, _sbss and _ebss are word-aligned so that word loops cover them exactly.
  • The final loop. main() should not return; if it does, the core parks instead of executing whatever follows in flash.

↑ This step uses the figure at the top of the page.

STEP 3

What this start-up leaves out, on purpose

It raises no clock, so everything runs on the boot clock; a real start-up raises it, after setting the flash wait states the faster clock needs (in SystemInit or at the top of main()). It runs no constructors: add .init_array to the script and lesson 5’s loop before main() if C++ objects or constructor functions are used. It sets up no heap: malloc needs an _sbrk and an end symbol. And at -O2 GCC may turn the two loops into calls to memcpy and memset; that is fine with a library whose versions need no initialised data, and -fno-tree-loop-distribute-patterns keeps them as loops when it is not.

STEP 4

Worked example: the symbols for one program

The program has 6000 bytes of code and constants after the 48-word (0xC0-byte) vector table, 256 bytes of .data and 1024 bytes of .bss:

_sidata=0xC0+6000=192+6000=6192=0x1830\_sidata = 0xC0 + 6000 = 192 + 6000 = 6192 = 0x1830

.data runs from _sdata = 0x2000_0000 to _edata = 0x2000_0100 (64 words to copy); .bss from 0x2000_0100 to _ebss = 0x2000_0500 (256 words to zero). The stack grows down from 0x2000_8000 with 32 768 − 1280 = 31 488 bytes of room before it meets .bss, shared with any heap. The flash image is 6192 + 256 = 6448 bytes, because .data’s initial values are stored after the code.

MYTHS AND FACTS

Common misconceptions

used is enough to keep the table

It acts on the compiler only; with --gc-sections the linker needs KEEP().

A start-up must be written in assembly

On a Cortex-M the core loads SP before the first instruction, so C works from the first line.

The ELF entry point makes the chip start at Reset_Handler

The reset vector does; ENTRY() is for debuggers and loaders (lesson 3).

It works under the debugger, so the start-up is right

RAM survives a debugger reset and can hide a missing copy loop; test from a cold power-on.

-nostartfiles removes the C library

It removes only the start-up files; the libraries still link.

Check yourself

Answer in your head, then open the card.

The code grows to 20 000 bytes after the table and .data to 512 bytes. What are _sidata and _edata?

_sidata = 0xC0 + 20 000 = 20 192 = 0x4EE0. _edata = 0x2000_0000 + 512 = 0x2000_0200.

The board boots under the debugger but, from a cold power-on, a variable declared int mode = 2; reads as a random value. Which line is missing or wrong?

The .data copy. A debugger reset is not a power-on: RAM keeps what an earlier run (perhaps an earlier build whose loop worked) left there, so the variable happens to hold 2. Check the loop and that _sidata is LOADADDR(.data).

The linker script says *(.vectors) without KEEP and the build uses --gc-sections. What does the chip do at reset?

The table’s section is discarded because nothing references it. Words 0 and 1 of flash hold the start of some code, so SP and PC get meaningless values and the chip faults or runs garbage immediately.

Why is _estack defined outside SECTIONS as ORIGIN(RAM) + LENGTH(RAM) rather than inside .bss?

It is an address, not storage: the stack is the RAM above .bss, growing down from the top. Defining it from the region keeps the stack at the top however .data and .bss grow.

Sources (4)
  1. Arm, CMSIS-DFP, Device/ARMCM0plus/Source/startup_ARMCM0plus.c, and CMSIS 6, m-profile/cmsis_gcc_m.h — the pattern followed here: a const array of handler pointers in section .vectors with __attribute__((used)), the initial SP cast into the first entry, weak aliases to Default_Handler, and a Reset_Handler written in C
  2. STMicroelectronics, cmsis-device-f4, Source/Templates/gcc/startup_stm32f407xx.s — the symbol names used here (_sidata, _sdata, _edata, _sbss, _ebss, _estack) are those of ST’s GCC template and its linker scripts
  3. GNU ld manual, “Output Section LMA”, “Input Section and Garbage Collection” (KEEP) and “Source Code Reference” — AT> region and LOADADDR() for .data’s load image; KEEP protects a section from --gc-sections; a script symbol has an address but no storage (read from the ld texinfo source, unit 4)
  4. GCC manual, “Common Function Attributes” (alias, weak), “Common Variable Attributes” (section, used), “Link Options” (-nostartfiles) and “Optimize Options” (-ftree-loop-distribute-patterns) — -nostartfiles: “Do not use the standard system startup files when linking. The standard system libraries are used normally”; loop distribution into memset and memcpy calls is on at -O2 (read from gcc/doc/invoke.texi and extend.texi in the gcc-mirror repository)