Cortex-M Startup Code and Vector Table: From Reset to main() in C

When you press reset, main() is not the first code that runs. Something must set the stack pointer, copy initialised variables from flash into RAM, clear uninitialised variables to zero and configure the clock. That something is the startup code, and the vector table tells the core where to find it. IDEs generate these files for you, which is convenient until a variable starts with the wrong value or an interrupt jumps to the wrong place. This tutorial builds a complete startup file and linker script from scratch so you know exactly what happens between reset and main.

What the Core Does at Reset

A Cortex-M core does very little by itself at reset. It reads the first word of the vector table and loads it into the main stack pointer, then reads the second word and loads it into the program counter. The vector table starts at address 0x00000000 after reset, which is the beginning of flash on most parts. Because Cortex-M only runs Thumb code, the least significant bit of every handler address must be 1. The compiler sets this bit automatically when you place function addresses in the table.

OffsetEntryException number
0x00Initial main stack pointer value-
0x04Reset_Handler1
0x08NMI_Handler2
0x0CHardFault_Handler3
0x10MemManage_Handler (M3/M4/M7)4
0x14BusFault_Handler (M3/M4/M7)5
0x18UsageFault_Handler (M3/M4/M7)6
0x2CSVC_Handler11
0x38PendSV_Handler14
0x3CSysTick_Handler15
0x40 onwardsDevice interrupts IRQ0, IRQ1 and so on16 and above

Offsets 0x1C to 0x28 and 0x30 to 0x34 are reserved or used for debug monitor on ARMv7-M. NXP LPC microcontrollers use the reserved word at offset 0x1C as a checksum: the boot ROM only runs user code if the sum of the first eight words is zero. Tools such as Flash Magic and Keil's flash loader fill it in, but if you program a raw binary with another tool you may need to compute it yourself.

The Linker Script

The startup code needs to know where sections are. The linker script defines symbols for the start and end of .data in RAM, its load address in flash, and the boundaries of .bss. The example targets a Cortex-M3 with 512 KB of flash at 0x00000000 and 32 KB of RAM at 0x10000000, such as the LPC1768. Change the MEMORY lines for your chip.

lpc1768.ld: minimal linker script with symbols used by the startup code
MEMORY
{
  FLASH (rx)  : ORIGIN = 0x00000000, LENGTH = 512K
  RAM   (rwx) : ORIGIN = 0x10000000, LENGTH = 32K
}

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

ENTRY(Reset_Handler)

SECTIONS
{
  .text :
  {
    KEEP(*(.isr_vector))      /* vector table must be first */
    *(.text*)
    *(.rodata*)
    . = ALIGN(4);
  } > FLASH

  _sidata = LOADADDR(.data);  /* where .data initial values live in flash */

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

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

A Startup File Written in C

Startup files are traditionally written in assembly, but on Cortex-M plain C works because the core loads the stack pointer before running any code. The table below is an array of function pointers placed in the .isr_vector section. Every handler except Reset is declared weak and aliased to a default handler, so the application can override any of them simply by defining a function with the same name.

startup.c: vector table, default handlers and Reset_Handler for a Cortex-M3
#include <stdint.h>

/* Symbols from the linker script */
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;

extern int main(void);
void SystemInit(void) __attribute__((weak));

void Reset_Handler(void);
void Default_Handler(void);

/* Weak aliases: define a function with the same name to override */
void NMI_Handler(void)        __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void)  __attribute__((weak, alias("Default_Handler")));
void MemManage_Handler(void)  __attribute__((weak, alias("Default_Handler")));
void BusFault_Handler(void)   __attribute__((weak, alias("Default_Handler")));
void UsageFault_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")));
void TIMER0_IRQHandler(void)  __attribute__((weak, alias("Default_Handler")));

typedef void (*vector_t)(void);

__attribute__((section(".isr_vector"), used))
const vector_t vector_table[] = {
    (vector_t)&_estack,     /* 0x00 initial MSP */
    Reset_Handler,          /* 0x04 */
    NMI_Handler,            /* 0x08 */
    HardFault_Handler,      /* 0x0C */
    MemManage_Handler,      /* 0x10 */
    BusFault_Handler,       /* 0x14 */
    UsageFault_Handler,     /* 0x18 */
    0,                      /* 0x1C reserved (LPC checksum) */
    0, 0, 0,                /* 0x20 to 0x28 reserved */
    SVC_Handler,            /* 0x2C */
    0,                      /* 0x30 debug monitor */
    0,                      /* 0x34 reserved */
    PendSV_Handler,         /* 0x38 */
    SysTick_Handler,        /* 0x3C */
    /* Device interrupts: on LPC17xx, IRQ0 = WDT, IRQ1 = TIMER0 */
    Default_Handler,        /* IRQ0 */
    TIMER0_IRQHandler,      /* IRQ1 */
    /* ... continue for every IRQ the device defines ... */
};

void Reset_Handler(void)
{
    uint32_t *src = &_sidata;
    uint32_t *dst = &_sdata;

    /* 1. Copy initialised data from flash to RAM */
    while (dst < &_edata)
        *dst++ = *src++;

    /* 2. Zero the .bss section */
    for (dst = &_sbss; dst < &_ebss; )
        *dst++ = 0;

    /* 3. Clock setup supplied by the vendor system file, if linked */
    if (SystemInit)
        SystemInit();

    /* 4. Run the application */
    main();

    /* 5. main should never return; trap here if it does */
    while (1)
        ;
}

void Default_Handler(void)
{
    while (1)     /* unexpected interrupt: inspect IPSR in the debugger */
        ;
}

Build with arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -nostartfiles -T lpc1768.ld startup.c main.c -o app.elf. The -nostartfiles option stops gcc from linking its own crt0 startup, which is meant for hosted systems. Run arm-none-eabi-objdump -h app.elf and confirm that .isr_vector content sits at address 0x00000000 and that .data has a VMA in RAM and an LMA in flash.

Why Each Step Matters

A global such as int count = 5; lives in .data. Its initial value 5 is stored in flash, but the variable itself must be in RAM so it can change. The copy loop transfers all such values. A global such as int total; lives in .bss; the C standard guarantees it starts at zero, and the clearing loop provides that guarantee. If either loop is missing, variables start with random contents left in RAM, which is one of the hardest bugs to track down.

Cortex-M3, M4 and M7 have a Vector Table Offset Register, SCB->VTOR at 0xE000ED08. A bootloader can place an application at a higher flash address and write that address to VTOR before jumping to it, so the application's own table handles interrupts. Cortex-M0 has no VTOR; on M0+ it is optional.

Common Mistakes

  • The vector table is removed by the linker's garbage collection; use KEEP in the script and the used attribute in C.
  • The table is not at the start of flash because another object was placed first; the core loads garbage into SP and PC.
  • Missing entries in the device interrupt list shift every later handler to the wrong IRQ.
  • Initialised globals show wrong values: .data is copied from the wrong address because _sidata does not use LOADADDR.
  • Code compiled with an FPU but the startup does not enable the FPU in CPACR; the first float instruction causes a fault.

Summary

At reset the Cortex-M core loads MSP from word 0 and PC from word 1 of the vector table. The Reset_Handler then copies .data from flash to RAM, zeroes .bss, calls SystemInit and finally main. The linker script supplies the addresses for these steps and places the vector table first in flash. With weak aliases, any handler can be overridden simply by defining a function with the matching name.