Embedded Systems · Bare-Metal Programming

Relocating the Vector Table and Running It from RAM

How the table base register changes where exceptions dispatch, why a misaligned base fails silently, and what a writable copy of the table enables.

The table described in the reset sequence has no existence of its own from the core’s point of view. The core holds a base address and computes an entry as base plus four times the exception number. Out of reset that base is zero, so the table appears to live at address zero because the core says it does. Changing the base is a single register write, and it is the mechanism behind bootloader handoff, run-time handler installation, and a class of faults that present as interrupts landing in the wrong place.

One register, and what it buys

The register that holds the base is a system control register, writable only from privileged state and cleared to zero by a system reset on the common implementations. It is not indexed per exception; it applies to the whole table, so dispatch moves for every exception at once. There is no per-handler redirection anywhere else in the architecture.

The reset value is zero on the common implementations, which is why a single-image firmware never touches the register: the table occupies the lowest addresses of the region the core reads at reset, and dispatch needs no configuration. The register becomes interesting the moment a second image exists, or the moment a handler address has to change after the image was written to flash.

Alignment, and the way it fails

The base address must be aligned to a power of two that is at least as large as the table itself, with a floor of 128 bytes, which is 32 words. The low seven bits of the register are reserved and read as zero, so a write whose value is not a multiple of 128 cannot take effect as written; the core stores the aligned form.

The failure this produces has no diagnostic. A firmware that copies its table to an address 0x40 bytes past a 0x400 boundary, then writes that address into the register, ends up dispatching from the boundary the core computed rather than from the copy. If that boundary happens to hold another image’s table, the interrupt works and does the wrong thing; if it holds data, the first exception jumps into whatever bytes are there and faults. The only reliable protection is to place the table with an alignment attribute in the source or an alignment directive in the linker script, so the address the program computes is already aligned and the constant cannot be wrong.

The same requirement applies to the flash-resident table in an application that is not at address zero. A bootloader that will hand control to that application has to know the application’s base, and the application’s table entry alignment determines how the image may be placed.

Why a table in RAM is worth the copy

Flash is not writable at run time, so a handler address in a flash table is fixed for the life of the image. Three requirements break that assumption. A debug path may want to install a handler that records a fault and then restores the original entry. A system with two operating configurations may want to switch a peripheral’s interrupt between two service routines. A bootloader with a full table of stub handlers may want to install the application’s handlers into a common table instead of switching the base. Each of these is satisfied by keeping the table in RAM, copying it there at startup, and changing the base to point at the copy.

The copy is unremarkable in structure:

/* Interrupts must be masked across this: until the base register is updated,
   dispatch still uses the flash table, and a partly copied table is worse
   than either source. */
for (unsigned i = 0; i < VECTOR_WORDS; i++) {
    ram_vectors[i] = vectors[i];
}
SCB->VTOR = (uint32_t)ram_vectors;   /* must be aligned to the table size, min 128 bytes */

The costs are specific. The table occupies RAM equal to four bytes per exception — 448 bytes on a part with 96 interrupt lines, which is a real fraction of a small SRAM and may be cheaper to spend on a software dispatch table of function pointers indexed by exception number, accepting one extra indirection per interrupt. The RAM holding the table must be writable and readable in handler mode, which constrains any memory protection configuration. And the copy must be atomic with respect to dispatch: masking interrupts around a few hundred bytes of copying is cheap relative to the rest of startup and removes the window entirely.

Once the table is in RAM, entry 0 is dead weight. The core reads the initial stack pointer from word zero only at reset, and after the copy it never reads the entry again; nothing prevents that word from holding something else, though leaving it consistent with the flash table costs four bytes and avoids confusing a debugger that displays the image. The first entry that matters after reset is the reset vector itself, and a RAM table is copied too late for the hardware to use it, which is why the reset handler in a RAM-table system still comes from the flash table.

Handing control to a second image

A bootloader that transfers control to an application performs a fixed sequence. It masks interrupts, because any interrupt taken between the base update and the jump dispatches into the application’s table while the application’s data sections are still uninitialised. It loads the main stack pointer from the application’s word zero, because the exception entry sequence would otherwise stack frames on the bootloader’s stack, which belongs to a different region and may have been overwritten. It writes the application’s table base into the base register. Then it branches to the application’s reset entry, having cleared or disabled the peripherals it had enabled so that the application does not inherit pending flags it does not understand.

Each step has a failure mode. Loading the stack pointer but not the base means the application runs its own stack and the bootloader’s handlers, which typically produces an interrupt that executes bootloader code against application state. Writing the base but not the stack pointer means the first deep call chain writes over the bootloader’s stack. Writing either one before masking interrupts means the window described above, whose width is the time between the two writes.

What memory may hold the table

The core reads a table entry once per exception entry, as a word-sized read at the privilege level of the exception. The table therefore has to live in memory that responds to that read, at an address that does not require an access the exception entry cannot make. Placing it in a region the protection unit marks as unprivileged-only, or in a region whose access attributes forbid data reads, produces a failure at the moment of the first interrupt rather than at the moment of the write, and on some implementations the failure is the vector-fetch bit in the fault status register rather than a bus fault naming the address.

Where the implementation has a data cache, a table in RAM acquires a second requirement. Entries written by software may sit in the cache while the exception entry reads them, which is coherent as long as both go through the same cache, and is not coherent if the table region is configured with attributes that permit speculative or buffered access while the modification path bypasses the cache. A table that is written once at startup and never changed avoids the problem entirely; a table that is modified at run time needs the write to reach the point of coherence before an exception can be taken that depends on it, which in practice means a clean of the region followed by a barrier, or a table region configured as non-cacheable at the cost of slower entry.

Read-only memory holds the alternative form of the same idea: a table in flash with a dispatcher as its entry, where the dispatcher reads an index from RAM and branches. That arrangement never writes the table, so it has none of the coherency requirements, at the cost of the extra indirection and of the fact that the branch target is opaque to the hardware, which prevents the return path shortcuts described in interrupt entry and exit mechanics.

Alternative arrangements

RISC-V puts the trap base in a control and status register as well, with the low bits of the value selecting direct dispatch from the base or vectored dispatch from the base plus four times the cause number; the table therefore holds either one entry or a set, chosen by the same register. AArch64 cores keep a vector base register at the privilege level that owns exception dispatch, and their vectors are not addresses but 128-byte slots containing instructions, sixteen of them — 2 KB in total, with the base aligned to 2 KB. The AArch64 arrangement removes the address indirection and lets each entry begin executing immediately, at the cost of a fixed 128-byte stride and an instruction sequence per entry. The Cortex-M arrangement trades a load for flexibility in table size.

The bug class that spans all of them is the same: the base address is a value the software computes and the hardware interprets, and nothing in either checks that the two agree about the table’s identity. A table whose base is right and whose contents are a stale copy is indistinguishable, from the core’s point of view, from a correct one, until an interrupt that should have been serviced does something else. Where a handler is entered but behaves incorrectly, the trace leads back through the vector table to the copy and the base write, and then forward into what the handler does with the state the hardware saved for it, which is the sequence described in interrupt entry and exit mechanics.

References

  1. ARMv7-M Architecture Reference Manual (ARM DDI 0403) — Arm
  2. Cortex-M3 Processor Technical Reference Manual (ARM DDI 0337) — Arm
  3. Arm Architecture Reference Manual for A-profile architecture (ARM DDI 0487) — Arm
  4. The Definitive Guide to Arm Cortex-M3 and Cortex-M4 Processors — Joseph Yiu, Newnes