All notes
Firmware6 min read

What happens between reset and main()

The first milliseconds of a Cortex-M microcontroller, and the bugs that hide there.

Written by the engineers at NovaHeap Technologies

Most embedded work starts at main(). But by the time main() runs, the processor has already loaded a stack pointer, found its first instruction, configured parts of the system, and built the C runtime environment your code quietly depends on.

When one of those steps goes wrong, the symptoms rarely point back to it. You see a hard fault before the first breakpoint, a global variable holding garbage, or a board that boots fine under the debugger but not after a power cycle.

This note walks through the path from reset to main() on an ARM Cortex-M, using an STM32 as the example. Vendors differ in the details, but the shape is the same on almost every Cortex-M part.

Power on / resetSupply stable, reset releasedHARDWAREHardware loads SP and PCFirst two words of the vector tableHARDWAREReset_HandlerFirst line of your codeSTARTUP CODESystemInit()FPU access, vector table offset, early clock setupSTARTUP CODECopy .dataInitial values from flash into RAMSTARTUP CODEZero .bssUninitialized globals set to 0STARTUP CODEConstructors__libc_init_array(): C++ statics, init hooksSTARTUP CODEmain()Your application startsYOU
The path from reset to main(). The first two steps happen in silicon, before any of your code runs.

1. Power on and reset

When power is applied, the supply ramps up and the power-on reset and brown-out circuitry hold the core in reset until the voltage is stable. When reset releases, the core starts in Thread mode, privileged, using the Main Stack Pointer.

Very little is set up at this point. The CPU runs from its default internal oscillator (the HSI on STM32), peripherals are in their reset state, and RAM contents are undefined.

2. The vector table: two words the hardware reads for you

Before executing a single instruction, the Cortex-M core reads two 32-bit values from the start of the vector table:

  • Word 0 is loaded into the stack pointer (MSP).
  • Word 1 is loaded into the program counter. This is the address of Reset_Handler.

On an STM32 the vector table sits at the start of flash, 0x08000000. Depending on the BOOT pin configuration, that region is also mapped at address 0x00000000, which is where the core looks at reset.

FLASH 0x08000000 (vector table)0x080000000x20008000Initial stack pointer (MSP)loaded into SP0x080000040x080001C5Reset_Handler addressloaded into PC0x080000080x080001F1NMI_Handler0x0800000C0x080001F3HardFault_Handler......Other exceptions and IRQsOdd handler addresses are intentional: bit 0 set means Thumb mode.
The first entries of a typical vector table. The hardware loads the first two words directly into SP and PC.

Notice that the handler addresses are odd numbers. Cortex-M cores only execute Thumb code, and bit 0 of every handler address must be set to say so. If a hand-edited table or a broken linker script produces an even address, the processor faults on its very first instruction.

Bootloader trap: An application that runs behind a bootloader lives at a higher flash address, so its vector table is not at the start of flash. Either the bootloader or the application’s startup code must write the application’s table address into the VTOR register. If it doesn’t, the first interrupt jumps into the bootloader’s handlers, and the crash looks random.

3. Reset_Handler: the first code that runs

Reset_Handler is ordinary code, usually provided by the vendor as an assembly startup file. Its job is to make the C language’s promises true before your code relies on them. Here is the same logic written in C, which is easier to follow:

extern uint32_t _sidata;  /* start of .data initial values in flash */
extern uint32_t _sdata;   /* start of .data in RAM */
extern uint32_t _edata;   /* end of .data in RAM */
extern uint32_t _sbss;    /* start of .bss */
extern uint32_t _ebss;    /* end of .bss */

void Reset_Handler(void)
{
    SystemInit();                          /* FPU, VTOR, early clock setup */

    uint32_t *src = &_sidata;
    uint32_t *dst = &_sdata;
    while (dst < &_edata) {                /* copy .data from flash to RAM */
        *dst++ = *src++;
    }

    for (dst = &_sbss; dst < &_ebss; ) {   /* zero .bss */
        *dst++ = 0;
    }

    __libc_init_array();                   /* C++ constructors, init hooks */

    main();

    while (1) { }                          /* main() must never return */
}

Every symbol here comes from the linker script, which we will get to in a moment.

4. SystemInit: early hardware setup

SystemInit() is the vendor’s early hardware hook. Depending on the vendor and version, it typically enables access to the FPU, sets the vector table offset, and resets the clock configuration to a known state. On STM32 projects generated by CubeMX, the full clock tree (PLL, external crystal) is usually configured later, in SystemClock_Config() called from main().

The ordering trap: In many vendor startup files, SystemInit() is called before .data is copied and .bss is zeroed. That means any global variable it reads contains garbage, and any global it writes is overwritten a few instructions later. If you add code to SystemInit(), don’t touch globals, and check the order in your own startup file rather than assuming.

5. Building the C runtime: .data and .bss

C makes two promises about global and static variables. Initialized ones start with their initial value, and uninitialized ones start at zero. On a microcontroller, nothing makes those promises true except the startup code.

An initialized global like int32_t gain = 42; has a problem: the variable must live in RAM so it can change, but RAM is empty at power-up. So the linker stores the initial value 42 in flash (the load address) and places the variable itself in RAM (the run address). The startup code copies one to the other.

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

_sidata = LOADADDR(.data);

.bss :
{
    _sbss = .;
    *(.bss*)
    *(COMMON)
    _ebss = .;
} > RAM

The > RAM AT > FLASH line is the key: link the section for RAM, but store its contents in flash. LOADADDR(.data) gives the startup code the flash address to copy from.

Uninitialized globals go in .bss, which takes up no space in flash at all. The startup code simply zeroes that range of RAM.

Flash0x08000000RAM0x200000000x20008000.isr_vectorvector table.textyour code.rodataconstants.data initinitial valuesunused flash.data_sdata to _edata.bsszeroed at bootheap ↓free RAMstack ↑copied byReset_Handlerheap and stack can collide
Initial values for .data live in flash and are copied into RAM at boot. The heap and stack share whatever RAM is left.

6. The heap and stack: what the memory map leaves over

After .data and .bss, the rest of RAM is shared by two regions growing toward each other. The heap starts just after .bss and grows toward higher addresses each time malloc() asks the C library for more memory. The stack starts at the top of RAM (_estack, the value stored in word 0 of the vector table) and grows downward with every function call and local variable.

By default, nothing stops them from meeting. When they do, the stack silently overwrites heap data or the heap hands out memory the stack is using, and the device crashes hours or days later, far from the cause.

A few habits prevent most of these failures:

  • Reserve sizes explicitly in the linker script (for example _Min_Heap_Size and _Min_Stack_Size in STM32 projects), so the build fails if RAM runs out, instead of the device.
  • Paint the stack. Fill it with a known pattern at boot, run the device under worst-case load, then check how much of the pattern survived. That is your real stack high-water mark.
  • Add a guard region with the MPU at the stack limit, so an overflow faults immediately instead of corrupting memory.
  • Be careful with the heap on long-running devices. Repeated malloc() and free() of different sizes fragments memory until an allocation fails even though plenty of total memory is free. Many production firmware designs allocate only during initialization, or use fixed-size memory pools.

7. Constructors, then main()

Before calling main(), __libc_init_array() runs the functions listed in the .preinit_array and .init_array sections. That includes constructors for C++ global objects and any C functions marked __attribute__((constructor)).

This is where C++’s static initialization order problem lives: if one global object’s constructor uses another global object from a different source file, the order between them is not guaranteed.

Then, finally, main() runs. On a bare-metal system there is nothing to return to, so write main() as a loop that never exits. What happens if it does return depends entirely on your startup file.

When it goes wrong: symptoms and likely causes

Symptom Likely cause
Hard fault before main() Stack pointer outside RAM, even Reset_Handler address, or wrong vector table after a bootloader jump
Global variables have wrong values .data not copied, wrong load address in the linker script, or SystemInit() touching globals
First interrupt crashes, but only behind the bootloader VTOR not set to the application’s vector table
Works under the debugger, fails after a power cycle The debug session left clocks, BOOT mode, or RAM in a state that a true cold boot does not; always test with a real power cycle
Random crashes after hours of running Stack overflow into the heap, or heap fragmentation

Key takeaways

  • The hardware does exactly two things for you at reset: it loads the stack pointer and the reset handler address from the vector table.
  • Everything else the C language assumes, including initialized globals and zeroed statics, is created by startup code you can read and debug.
  • The linker script is part of your boot code. Treat changes to it as seriously as changes to C code.
  • Heap and stack collisions are silent by default. Size them explicitly and measure them.

NovaHeap Technologies designs electronics and writes firmware in McKinney, Texas, with PCB assembly at our facility in India, so bring-up problems like these are solved by the same team that designed the board. If your board is stuck somewhere between reset and main(), get in touch.

  • Cortex-M
  • STM32
  • Boot
  • Linker script
  • Startup code

More notes