Reset Sequence, Startup Code
& Privilege Transitions
What actually happens between the moment you press the reset button and your first line of
main() executing? This lecture traces every step — the vector table fetch,
the startup file, section initialisation, and how the processor’s privilege level can
shift during runtime.
- What Happens Before main()?
- The Vector Table and Reset Entry
- The Reset Sequence Step by Step
- Reset Handler Responsibilities
- Initialising the .data Section
- Initialising the .bss Section
- C Standard Library Initialisation
- Complete Startup File Example
- Privilege Level Transitions at Runtime
- ISR Entry Forces Privileged Mode
- FAQ
1. What Happens Before main()?
Most programmers think of their embedded program as starting at main(). In
reality, main() is called quite late — after the processor has gone through a
carefully orchestrated boot sequence that sets up the runtime environment C code needs.
Understanding this sequence matters for three practical reasons:
- Debugging early crashes — if your program crashes before
main(), you need to know where to look. - Writing bare-metal projects from scratch — if you do not use an IDE template, you must supply a startup file yourself.
- Optimising boot time — knowing what
Reset_Handlerdoes lets you decide what to skip in latency-critical applications.
What you think happens
Power on → main() starts → global variables are initialised →
your code runs.
What actually happens
Power on → processor reads vector table → jumps to Reset_Handler →
.data section copied from flash to SRAM → .bss section
zeroed → C library constructors called → main() called.
2. The Vector Table and Reset Entry
Every Cortex-M processor starts in the same way after any reset (power-on, external pin,
software, or watchdog). The processor does two reads from address 0x00000000
before executing a single instruction:
Reset_Handler function in flash.
This array of addresses starting at 0x00000000 is called the
vector table. Entry 0 is the initial MSP value; entry 1 (at offset 4) is
the reset handler address; entries 2 onward are addresses of exception and interrupt
handlers.
/* Typical vector table definition in a startup file (C style) */
extern uint32_t _estack; /* linker symbol: top of SRAM */
extern void Reset_Handler(void);
extern void NMI_Handler(void);
extern void HardFault_Handler(void);
/* ... more handlers ... */
/* Placed in .isr_vector section by the linker script */
__attribute__((section(".isr_vector")))
const uint32_t vector_table[] = {
(uint32_t)&_estack, /* 0x00: Initial MSP value */
(uint32_t)&Reset_Handler, /* 0x04: Reset entry point */
(uint32_t)&NMI_Handler, /* 0x08: Non-maskable interrupt */
(uint32_t)&HardFault_Handler,/* 0x0C: Hard fault */
/* ... */
};
Vector Table Layout at 0x08000000 (STM32F411)
3. The Reset Sequence Step by Step
When reset is released, hardware performs these steps automatically — no code executes during this phase:
At the moment Reset_Handler begins executing, the processor is in
Thread mode with Privileged access level. The MSP is
valid (loaded by hardware), but .data and .bss have not been
set up yet — so any C code that runs before the startup file completes cannot rely on
global or static variables being initialised.
Reset_Handler that references a global variable
before .data is initialised, you will read garbage. This is a real
bug in hand-written startup files and is extremely hard to debug because it only manifests
in specific compile configurations.
4. Reset Handler Responsibilities
The Reset_Handler function lives in the startup file
(e.g. startup_stm32f411retx.s or startup_stm32f411.c).
Its job is to bring the C runtime to a state where C code can execute correctly.
It has exactly three responsibilities before calling main():
Initialise .data section
Copy initialised global/static variables from flash (where they are stored after programming) into SRAM (where the processor can read and write them at runtime).
Initialise .bss section
Zero-fill all uninitialised global/static variables. The C standard guarantees these are zero at program start — but SRAM power-up value is random, so we must do this explicitly.
Initialise C standard library
Call __libc_init_array() which runs C++ static constructors and any
__attribute__((constructor)) functions. For pure C projects, this is
often a no-op but must still be called.
After these three steps, the environment meets the requirements of the C standard and
main() is called.
5. Initialising the .data Section
When you write a global variable like int x = 42;, the compiler places
the initial value 42 in the .data section. But at link time,
the linker places the initial values in flash (so they survive power-off) and
reserves space for the variable in SRAM.
.data Section: Flash → SRAM Copy
42 (x init val)
hello (y init)
_edata mirror
to _sdata, size =
_edata – _sdata
x = 42
y = “hello”
_edata
The linker script exports three symbols that Reset_Handler uses:
| Linker Symbol | Meaning |
|---|---|
_sidata | Start of initial values in flash (Load Memory Address) |
_sdata | Start of .data in SRAM (Virtual Memory Address) |
_edata | End of .data in SRAM — copy bytes from _sdata to _edata |
/* .data initialisation in C (equivalent to what startup .s file does) */
extern uint32_t _sidata; /* initial values in flash */
extern uint32_t _sdata; /* start of .data in SRAM */
extern uint32_t _edata; /* end of .data in SRAM */
static void init_data_section(void)
{
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) {
*dst++ = *src++; /* copy word by word from flash to SRAM */
}
}
6. Initialising the .bss Section
Uninitialised global and static variables — like int counter; or
static uint8_t buffer[256]; — are placed in the .bss
section. The C standard requires these to be zero at program start.
Flash stores only the size of .bss (no actual data, because it is all zeros). The startup code must zero-fill the SRAM region reserved for .bss.
extern uint32_t _sbss; /* start of .bss in SRAM */
extern uint32_t _ebss; /* end of .bss in SRAM */
static void init_bss_section(void)
{
uint32_t *dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0U; /* zero every word in .bss */
}
}
.data vs .bss — what goes where
/* These variables go into .data (stored in flash, copied to SRAM) */
int global_count = 100; /* initialised global */
float pi = 3.14159f; /* initialised global */
static int s_limit = 50; /* initialised static */
/* These variables go into .bss (zeroed in SRAM, no flash storage) */
int error_code; /* uninitialised global */
uint8_t tx_buffer[512]; /* uninitialised array */
static uint32_t s_timestamp; /* uninitialised static */
/* Local variables go on the stack — NOT in .data or .bss */
void foo(void) {
int local = 10; /* stack — must be initialised explicitly */
}
7. C Standard Library Initialisation
The function __libc_init_array() is provided by newlib (the C library used
by arm-none-eabi-gcc). It iterates through a list of function pointers stored in the
.preinit_array, .init_array, and .fini_array
sections of the ELF file and calls each one.
Usually a no-op
If your project is pure C with no __attribute__((constructor))
functions, __libc_init_array() does almost nothing and returns
immediately. Still call it — the C standard technically requires it.
Runs static constructors
For C++ code, every class with a static instance (e.g. static Logger log;)
has its constructor in .init_array. Skipping
__libc_init_array() would leave static objects uninitialised —
instant undefined behaviour.
/* The call in Reset_Handler */
extern void __libc_init_array(void);
void Reset_Handler(void)
{
init_data_section();
init_bss_section();
__libc_init_array(); /* run constructors / C++ static init */
main(); /* call user application */
/* If main() ever returns (it shouldn't in embedded code),
loop forever to prevent the processor from fetching garbage */
while (1);
}
8. Complete Startup File Example
Below is a minimal but complete startup file written in C, suitable for STM32F411. This is the kind of file that IDEs generate automatically — understanding it means you can debug it when things go wrong.
/* startup_stm32f411.c — minimal bare-metal startup for STM32F411 */
#include <stdint.h>
/* ----------------------------------------------------------------
Linker-provided symbols (defined in the .ld linker script)
---------------------------------------------------------------- */
extern uint32_t _estack; /* top of SRAM stack area */
extern uint32_t _sidata; /* .data initial values in flash */
extern uint32_t _sdata; /* .data start in SRAM */
extern uint32_t _edata; /* .data end in SRAM */
extern uint32_t _sbss; /* .bss start in SRAM */
extern uint32_t _ebss; /* .bss end in SRAM */
/* ----------------------------------------------------------------
Forward declarations for handlers
---------------------------------------------------------------- */
void Reset_Handler(void);
void Default_Handler(void);
/* Weak aliases so every ISR has a safe default */
void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void)__attribute__((weak, alias("Default_Handler")));
void SVC_Handler(void) __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));
/* ... add all 98 STM32F411 IRQ handlers here as weak aliases ... */
extern void main(void);
extern void __libc_init_array(void);
/* ----------------------------------------------------------------
Vector table — placed at 0x08000000 by the linker script
---------------------------------------------------------------- */
__attribute__((section(".isr_vector")))
const uint32_t vector_table[] = {
(uint32_t)&_estack, /* 0: Initial MSP value */
(uint32_t)Reset_Handler, /* 1: Reset */
(uint32_t)NMI_Handler, /* 2: NMI */
(uint32_t)HardFault_Handler, /* 3: HardFault */
/* ... remaining entries ... */
};
/* ----------------------------------------------------------------
Reset Handler — first code that runs after reset
---------------------------------------------------------------- */
void Reset_Handler(void)
{
/* 1. Copy .data initial values from flash to SRAM */
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) {
*dst++ = *src++;
}
/* 2. Zero-fill the .bss section in SRAM */
dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0U;
}
/* 3. Initialise C standard library (C++ constructors etc.) */
__libc_init_array();
/* 4. Call user application */
main();
/* Should never reach here in an embedded system */
while (1);
}
/* ----------------------------------------------------------------
Default handler — infinite loop for unhandled exceptions
---------------------------------------------------------------- */
void Default_Handler(void)
{
while (1); /* Breakpoint here to catch unexpected interrupts */
}
weak attribute means any handler you define in your own code
(e.g. void SysTick_Handler(void) { ... }) will override the default.
If you do not define a handler, the linker uses Default_Handler as the
safe fallback — which loops forever so you can catch it with a debugger.
9. Privilege Level Transitions at Runtime
After reset, the processor runs in Thread mode, Privileged access level.
This is the most permissive state — the code can access all registers including NVIC, SCB,
MPU, and system control registers. The CONTROL register’s nPRIV bit (bit 0)
is 0 (privileged).
A well-designed RTOS or secure application may want to drop application tasks to unprivileged level to prevent them from corrupting the OS or hardware. Here is how the transitions work:
Privilege Level State Machine — Thread Mode
PRIVILEGED
Can switch to unprivileged
UNPRIVILEGED
Cannot switch back directly
Drop to unprivileged
ISR returns to privileged
CONTROL[0] forced to 0 on entry
Returns to Thread mode on exit
/* ---- Dropping from privileged to unprivileged in Thread mode ---- */
static inline void drop_privileges(void)
{
uint32_t ctrl;
__asm volatile ("MRS %0, CONTROL" : "=r"(ctrl));
ctrl |= 1U; /* set nPRIV bit 0 */
__asm volatile ("MSR CONTROL, %0\n\t" "ISB\n\t" : : "r"(ctrl) : "memory");
}
/* ---- Regaining privileges from unprivileged Thread mode ----
ONLY possible via a supervisor call (SVC) — the SVC handler
runs in Handler mode (always privileged) and can clear nPRIV */
void SVC_Handler(void)
{
uint32_t ctrl;
__asm volatile ("MRS %0, CONTROL" : "=r"(ctrl));
ctrl &= ~1U; /* clear nPRIV bit */
__asm volatile ("MSR CONTROL, %0\n\t" "ISB\n\t" : : "r"(ctrl) : "memory");
}
/* Application calls: */
void task_A(void) {
drop_privileges(); /* now unprivileged — safe for user tasks */
/* ... user code ... */
__asm volatile ("SVC #0"); /* request OS service — goes to SVC_Handler */
}
10. ISR Entry Forces Privileged Mode
One very important property of the Cortex-M exception model is that entering any exception or interrupt automatically switches the processor to Handler mode with Privileged access level, regardless of what Thread mode it was in.
Mode and Privilege Across an Interrupt
When the ISR returns (via the special EXC_RETURN value in LR), the
processor restores the previous Thread mode state including the CONTROL register value —
so if Thread mode was unprivileged before the interrupt, it returns to unprivileged after.
SVC #N which triggers the SVC exception (Handler
mode, privileged). The SVC handler reads the syscall number N, performs the OS operation
with full hardware access, and returns to Thread mode. The task never has permanent
privilege elevation — it only gets it for the duration of the syscall.
11. FAQ
__attribute__((section(".RamFunc"))). The startup code copies those
functions from flash to SRAM during the .data init phase, and then they execute from
SRAM for higher speed (no flash latency). Reset_Handler itself must start in flash.
while(1) after main()
which loops forever. In other implementations, returning from main() may call
_exit() which on embedded targets typically does the same — loop forever
or trigger a reset. Never design an embedded system where main() is
expected to return; it is considered a fatal programming error.
Next: Cortex-M Memory Map and Bus Architecture
With reset and modes covered, the next lecture maps out the complete 4 GB Cortex-M address space — code, SRAM, peripherals, private peripheral bus — and explains how the AHB and APB buses determine where you can reach peripherals in C.

2 Comments