Embedded Systems · Bare-Metal Programming

Where the Reset Vector and Initial Stack Pointer Come From

Why the first two words of a firmware image are read by hardware before any instruction executes, how the vector table is indexed, and what breaks when the table moves.

At the instant a Cortex-M core leaves reset, no instruction has executed, no general-purpose register holds a meaningful value, and no memory location has been read. The core begins executing anyway. It can do so because two 32-bit words at fixed addresses in the image tell it where to begin: word zero is copied into the main stack pointer, and word one is copied into the program counter. Every later structure in the firmware (initialised data, interrupt handlers, the memory map itself) is built on that pair.

The stack pointer value is an address, not a symbol, so the linker script and the table must agree about where the stack region ends. Because the stack grows downwards, the correct value is the address one past the highest byte reserved for it, and the first push writes below that address. The procedure call standard requires the stack pointer to be eight-byte aligned at every public interface, so the value placed in entry 0 must be a multiple of eight; a value that is not produces a fault on the first double-word push or, worse, silently misaligns every subsequent frame. A linker script that reserves the region and exports its end address as a symbol keeps the two definitions in one place.

Two words that are not the same kind of thing

The vector table is an array of 32-bit entries, and the reset entry breaks the pattern that the rest of the array follows. Entry 0 is not a code address. It is a value placed directly into a register, and it happens to be the address of the top of the stack region, which grows downwards on this architecture. Entry 1 is a code address, the reset handler. Only from entry 2 onward does the array become a homogeneous list of exception handler addresses.

That asymmetry has a practical consequence: the reset handler can be written in ordinary C. By the time it runs, the stack pointer is already valid, so the compiler-generated prologue can push registers without faulting. The hardware performs the one initialisation step that C cannot perform for itself, establishing a stack, and then transfers control to compiled code. A C function is a legal reset handler, and on most parts it is the reset handler.

The encoding is strict in three ways. Each entry is exactly four bytes, so a single stray word inserted at the front shifts every handler by one index and every interrupt dispatches to the wrong function. Handler addresses must have bit 0 set, because the low bit of a value written to the program counter selects the instruction set state, and firmware on these cores is Thumb code. And the array must be contiguous from the start of the image: the first sixteen entries are fixed by the architecture and must be present whether or not the firmware uses them.

Exception numbers are shared identifiers

The index into the vector table is the same number the interrupt controller uses in its enable, pending, and priority bitmaps. Exception 1 is reset, 2 is the non-maskable interrupt, 3 is HardFault, 4 is MemManage, 5 is BusFault, 6 is UsageFault, exceptions 7 through 10 are reserved, 11 is SVCall, 12 is DebugMonitor, 13 is reserved, 14 is PendSV, and 15 is SysTick. External interrupts begin at 16, and the exact count is a property of the device.

The consequence is that a single integer identifies an interrupt in two unrelated tables. Setting bit 16 of the interrupt set-enable register and storing a handler at entry 16 of the vector table refer to the same interrupt line. Code that enumerates one and hardcodes the other drifts silently, and the drift shows up as an interrupt that enables correctly and dispatches into an unrelated function.

The reserved entries exist so the numbering stays stable across parts with different feature sets. A device with no memory protection unit still has exception 4. The first sixteen entries therefore cost a fixed 64 bytes of image, followed by four bytes per external interrupt: a part with 96 interrupt lines needs (16 + 96) × 4 = 448 bytes for the table.

A table written as data

The table is normally a constant array in C rather than a hand-written assembly blob. Because the first element is a stack pointer value and the rest are function addresses, the array cannot be typed as an array of function pointers without a cast on the first element.

/* The linker script places this section at the image base. 'used' keeps the
   compiler from discarding it; KEEP in the script keeps --gc-sections from
   removing it, since no instruction references the table. */
__attribute__((section(".vectors"), used))
void (*const vectors[])(void) = {
    (void (*)(void))0x20001000u,   /* initial MSP: top of the stack region */
    reset_handler,                 /* entry 1: first instruction executed */
    nmi_handler,
    hard_fault_handler,
    mem_manage_handler,
    bus_fault_handler,
    usage_fault_handler,
    0, 0, 0, 0,                    /* exceptions 7-10, reserved */
    svc_handler,
    debug_monitor_handler,
    0,                             /* exception 13, reserved */
    pendsv_handler,
    systick_handler,
    /* device interrupts from exception 16 upward */
    timer0_handler,
};

The table is data that the linker must place before it places anything else, and the linker does not know that. It places what the script tells it to place. A script that emits .text first and .vectors afterwards produces an image whose first word is an instruction, so the core loads an instruction encoding into the stack pointer and a following instruction into the program counter. The correct arrangement is stated with the base address of the flash region and the vector section pinned to the start of it. That same script also decides the addresses the initial stack pointer value must respect, which is why the runtime initialisation sequence starts by checking the regions the script declared rather than the C code.

Moving the table away from address zero

The initial table lives at address zero, which is flash on most devices, and nothing in software needs to point the core at it. That is true until something other than the core decides which image runs. A bootloader that transfers control to an application located elsewhere in flash must also redirect exception dispatch, and the core provides a register holding the table base address for exactly that purpose. The register is writable only from privileged state, and its value persists until the next reset.

The base must satisfy an alignment rule tied to the size of the table, and a write that violates it does not fault: the core masks the low bits, the table ends up at an address that is not where the software believes it is, and the visible symptom appears much later, as an interrupt taken through a table the software did not intend.

Two further pieces have to move at the same time. The stack pointer is not reloaded by the offset register, so a bootloader handing control to an application must load the main stack pointer from the application’s word zero before jumping, or the application runs on the bootloader’s stack. And handlers that carry absolute addresses of the table, such as a dispatcher that indexes it, must be built against the same base. The mechanics of doing this at run time, including keeping a writable copy of the table in RAM, are covered in vector table relocation and RAM vectors.

The same problem solved differently elsewhere

RISC-V does not read a table at reset. The trap base address lives in a control and status register whose low two bits select direct or vectored dispatch, and the address at which the core begins after reset is defined by the platform specification rather than by the instruction set. A RISC-V hart therefore has no equivalent of the initial stack pointer word, and its startup code must establish the stack before any function call. x86 cores begin at a fixed address near the top of the physical address space and obtain the rest of their initial state from firmware-provided structures rather than from the image. The Cortex-M arrangement is unusual in handing software a valid stack before its first instruction, and in making the dispatch table an ordinary array that the linker owns.

Failure modes rooted in entry zero

A reset vector whose low bit is clear is the classic first-build failure: the core attempts to interpret the target as non-Thumb code, and the result is a fault before any handler is installed. A table removed by link-time garbage collection produces a different symptom — the image links, the first word is not what the code expects, and the core faults immediately. A stack pointer value that points at a region the program uses for something else lets the first push overwrite it, and the damage surfaces as an unrelated branch into a data table. A device whose boot configuration remaps a ROM to address zero instead of flash runs the ROM’s table, not the one just linked, until the configuration is changed or the application takes over dispatch explicitly.

None of this says anything about how long a handler takes to run once it is entered. Saving and restoring context, prioritisation, and nesting are the subject of interrupt entry and exit mechanics, which start from the table entry this image selects.

References

  1. ARMv7-M Architecture Reference Manual (ARM DDI 0403) — Arm
  2. The Definitive Guide to Arm Cortex-M3 and Cortex-M4 Processors — Joseph Yiu, Newnes
  3. The RISC-V Instruction Set Manual, Volume II: Privileged Architecture — RISC-V International
  4. GNU ld documentation: Linker Scripts — GNU Binutils