Jun 5 / Sebastian Helmut

Demystifying the Cortex-M Boot Sequence: What Happens Before main()

Bootloader Basics: What Happens Before Your main() Runs


1. Introduction

Every embedded developer has written a main() function and assumed that is where execution begins. In reality, by the time the CPU reaches main(), a significant amount of work has already taken place. The processor has initialized its stack, configured memory regions, and executed startup code that prepares the C runtime environment. Understanding this sequence is not just academic. It directly impacts how you design firmware, debug hard faults, and implement custom bootloaders.

On Cortex-M microcontrollers, the boot sequence is tightly defined by the ARM architecture and the startup code provided by your toolchain. Missing or misunderstanding any step in this sequence leads to subtle bugs: uninitialized global variables, corrupted stacks, or firmware that silently fails before main() is ever reached.

This article walks through the complete boot sequence on a Cortex-M microcontroller, from the moment power is applied to the first instruction of main(), explaining each stage and the role it plays in making your firmware run correctly.


2. The Reset Sequence: Where It All Begins:

When a Cortex-M microcontroller powers on or receives a reset signal, the CPU does not simply start executing from address 0x00000000. Instead, it follows a specific hardware-defined procedure to locate the initial stack pointer and the reset handler address.

The CPU reads two values from the beginning of Flash memory, which is typically mapped to address 0x08000000 on STM32 devices:

  • Address 0x08000000: Initial Stack Pointer (SP) value

  • Address 0x08000004: Reset Handler address (the first function to execute)

These two entries are the first two words of the vector table, a fixed structure in Flash that maps every exception and interrupt to its handler address. The CPU hardware reads these values automatically on reset, loads the stack pointer into the SP register, and jumps to the reset handler.Fig 1. Reset sequence on Cortex-M


This means your reset handler is the very first code that executes on the device, before any C code, before any peripheral initialization, and before main().


Struggling to implement this for a professional project? If you need to master full-scale firmware architecture, security, and build automation, join our 1-D or 4-Day Live Implemnetation Workshops. I'll show you the exact direct path to production-ready firmware without the trial and error.


3. The Vector Table: Anchoring the Boot Process

The vector table is defined in your startup assembly file and placed at the beginning of Flash by the linker script. Each entry is a 32-bit address pointing to an interrupt or exception handler. The first two entries are special: they are not handler addresses but raw data values used by the CPU during reset.

A simplified vector table definition in C looks like this:

/* startup.c */

extern void Reset_Handler(void);

extern void NMI_Handler(void);

extern void HardFault_Handler(void);

extern uint32_t _estack; /* top of stack, defined in linker script */



__attribute__((section(".isr_vector")))

const uint32_t vector_table[] = {

(uint32_t)&_estack, /* initial stack pointer */

(uint32_t)&Reset_Handler, /* reset handler */

(uint32_t)&NMI_Handler, /* NMI handler */

(uint32_t)&HardFault_Handler,/* hard fault handler */

/* ... remaining handlers ... */

};

Fig 2. Vector table layout in Flash


The linker script ensures .isr_vector is placed at the very start of Flash, so the CPU finds these values at the correct addresses on reset. If the vector table is misplaced or the linker script is incorrectly configured, the CPU will read garbage values for the stack pointer and reset handler, causing an immediate fault.


4. The Reset Handler: Preparing the C Runtime

Once the CPU jumps to Reset_Handler, the processor is running but the C runtime environment is not yet ready. Global variables may contain garbage values, zero-initialized variables have not been cleared, and hardware clocks are running at their default reset frequencies.

The reset handler is responsible for three critical tasks before calling main():

4.1 Copying .data from Flash to RAM

Global and static variables that have non-zero initial values are stored in Flash at compile time. At runtime, however, they must reside in RAM so they can be modified. The reset handler copies these values from their load address in Flash to their runtime address in RAM.

/* copy .data section from Flash to RAM */

extern uint32_t _sdata; /* start of .data in RAM */

extern uint32_t _edata; /* end of .data in RAM */

extern uint32_t _sidata; /* start of .data image in Flash */



uint32_t *src = &_sidata;

uint32_t *dst = &_sdata;



while (dst < &_edata)

{

*dst++ = *src++;

}

Fig 3. Reset_Handler copies the .data image from Flash to RAM

4.2 Zeroing the .bss Section

Global and static variables declared without an explicit initial value must be zero-initialized according to the C standard. These variables occupy the .bss section, which has no stored image in Flash. The reset handler fills this region with zeros.

/* zero-fill .bss section */

extern uint32_t _sbss; /* start of .bss in RAM */

extern uint32_t _ebss; /* end of .bss in RAM */



uint32_t *bss = &_sbss;



while (bss < &_ebss) {

*bss++ = 0;

}

4.3 Calling SystemInit

Before main() is called, SystemInit() is typically invoked to configure the system clock. On STM32 devices, the microcontroller starts up using the internal RC oscillator (HSI) at a low frequency. SystemInit() configures the PLL, selects the clock source, and sets the core to its target operating frequency.


void Reset_Handler(void)

{

/* copy .data section */

uint32_t *src = &_sidata;

uint32_t *dst = &_sdata;

while (dst < &_edata) {

*dst++ = *src++;

}



/* zero .bss section */

uint32_t *bss = &_sbss;

while (bss < &_ebss) {

*bss++ = 0;

}



/* initialize system clocks */

SystemInit();



/* call main */

main();



}

Fig 4. Complete Cortex-M boot sequence from reset signal to main().


5. Memory Layout: Understanding the Linker Script

The linker script defines where each section of your firmware is placed in memory. It is what tells the linker the boundaries of Flash and RAM, and provides the symbols (_estack, _sdata, _edata, _sidata, _sbss, _ebss) that the reset handler uses.

A simplified linker script for an STM32 with 512KB Flash and 128KB RAM looks like this:

MEMORY {

FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K

RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K

}



SECTIONS {

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



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



.rodata : { *(.rodata*) } > FLASH



_sidata = .; /* start of .data image in Flash */



.data : AT(_sidata) {

_sdata = .;

*(.data*)

_edata = .;

} > RAM



.bss : {

_sbss = .;

*(.bss*)

*(COMMON)

_ebss = .;

} > RAM



_estack = ORIGIN(RAM) + LENGTH(RAM); /* top of stack */

}

The AT(_sidata) directive is key. It tells the linker to store the .data section's initial values in Flash (at _sidata) while placing the section's runtime address in RAM. This is what makes the copy in the reset handler work correctly.


6. Conclusion

Before main() is called, the microcontroller has already executed a carefully orchestrated sequence: the CPU reads the initial stack pointer and reset handler address from the vector table, jumps to the reset handler, copies initialized variables from Flash to RAM, zeroes the BSS section, and initializes the system clocks. Only then does execution reach main().

Understanding this sequence is essential for embedded developers who need to debug startup failures, write custom startup code, or build their own bootloaders. The linker script and reset handler work together to create a valid C runtime environment from scratch, turning a freshly reset processor into a system ready to execute your application logic.

Want to master this? Here are your next steps:


Created with