Embedded Systems · Bare-Metal Programming

Fault Escalation and Reading the Stacked Frame After a Crash

Why an imprecise bus fault arrives as a hard fault, which status bits distinguish the causes, and how the stacked frame locates the instruction that failed.

A machine that faults has stopped being a program and become a piece of state: a stack pointer, a set of status registers, and eight words on the stack that were written by hardware before the handler ran. The difference between a crash that is diagnosed in an hour and one that is diagnosed in a week is almost entirely whether the handler preserves that state and whether anyone knows which bits mean what. The architecture is generous with the information, and it also has a well-defined set of cases in which the information is not what it appears to be.

Why the fault that fires is often not the fault that occurred

The exception model has four fault-class exceptions, of which three are optional in the sense that they must be enabled before they can be taken. A memory protection violation raises a memory management fault, a bus error raises a bus fault, and an illegal instruction, an illegal state, an unaligned access where the architecture forbids it, or a coprocessor access with the coprocessor disabled raises a usage fault. If the corresponding handler is not enabled in the system handler control register, the exception is escalated and the core takes the hard fault entry instead.

Escalation also occurs when the fault’s own priority does not allow it to be taken, when the fault happens while a handler of equal or higher priority is active, and when a fault occurs inside a fault handler. The last case is the one that ends badly: a fault taken while the hard fault handler is active cannot escalate further, and the core enters the locked state, in which it stops executing instructions and only a reset or an attached debugger recovers it.

The hard fault handler therefore has to determine both what happened and why it did not arrive through the specific handler. Two status bits answer the second question. One indicates that the fault was escalated from a disabled or masked fault exception. The other indicates that the vector fetch itself failed, which points at the vector table rather than at the faulting instruction — a table whose base was set to a region that does not respond produces this bit rather than a bus fault, and the cause lies in the relocation described in relocating the vector table.

The frame and how to reach it

The exception entry sequence saves eight words before the handler’s first instruction: the four argument registers, the intra-procedure register, the link register, the return address, and the program status register. They lie at ascending addresses from the stack pointer the handler sees before its prologue runs.

OffsetContents
+0R0
+4R1
+8R2
+12R3
+16R12
+20LR
+24PC of the faulting instruction
+28xPSR

The offset of the program counter slot is the one that matters most: it names the instruction the core was executing, or, for an imprecise fault, some instruction that happened to be executing later. Two facts make the frame reachable. The first is that a handler written in C has already pushed the callee-saved registers by the time its code runs, so the frame sits above the value the handler reads from the stack pointer; recovering it requires either knowing the prologue’s size or, more reliably, capturing the stack pointer at the very start in an assembly stub that saves it into a variable before calling the C handler. The second is that the stack in use is encoded in the low bits of the special return value the core placed in the link register; if the faulted context was using the process stack rather than the main stack, the frame is in that stack and the handler reads it from there.

Capturing the pointer in a stub is worth the few lines of assembly, because the alternative is a handler that works until the compiler decides to allocate a temporary and changes the frame’s distance from the stack pointer.

Status registers and the address that was accessed

The fault status register is a packed set of fields: the low byte describes memory management faults, the second byte describes bus faults, and the upper half-word describes usage faults. Each field distinguishes causes that suggest different bugs. A memory management field distinguishes an instruction fetch from a data access and distinguishes a violation reported by the protection unit from one reported by a stacking operation, which is the signature of a stack that has overflowed its protected region. The bus field distinguishes an instruction prefetch error from a precise or imprecise data access error, and a stacking or unstacking failure from a lazy floating-point state preservation error. The usage field distinguishes an undefined instruction, an illegal state encoded in the instruction’s execution-state bits, an illegal load of the program counter, an access to a coprocessor that is not enabled, a divide by zero, and an unaligned access where alignment is enforced.

Two further registers hold the address of the access that faulted, for the subsets of faults where the address is known. Whether the address is valid is itself a status bit, and the bit is the reason to read the status register before the address register: an address register read when its validity bit is clear holds whatever was last written to it, which is a value from an earlier fault or from reset. The address register is the fastest route to the cause of a protection fault, since it names the pointer that was used rather than the instruction that used it.

The distinction between precise and imprecise is the one that costs time. A precise fault is reported against the instruction that caused it, so the saved program counter is the faulting instruction and the diagnosis is direct. An imprecise fault is reported after the offending access has left the core, which happens when the store was buffered or when the access was merged or reordered. The saved program counter then points at an instruction that may be unrelated, and the status registers may or may not carry a hint about which bus master or which region was involved. The documented techniques narrow the window rather than close it: reconfigure the region so that accesses to it are strongly ordered instead of buffered, which turns imprecise faults into precise ones at the cost of performance, or halve the suspect code until the fault follows one half. A fault that moves when the code is recompiled is evidence of the imprecise kind rather than a compiler bug.

Preserving evidence, and the cases where there is none

A fault handler that reboots the machine immediately destroys the frame, and the frame is the only record of the values in the registers at the moment of failure. The usual arrangement is to copy the frame and the status registers into a region that survives a reset, mark it with a signature word so that the next boot can distinguish a crash restart from a power-on restart, and then reset. That region must be excluded from both the initialised-data copy and the zeroed region, since the startup sequence described in getting initialised data and BSS ready clears exactly the memory a naive implementation would store the evidence in. The status registers are cleared by writing ones to the bits that are set, so a handler that clears them and then faults again loses the first cause; read first, store the values, then clear.

There are three cases in which the frame is not usable. If the fault was caused by the stack, the hardware’s attempt to push the frame faulted as well, and the escalation rules send the exception to the hard fault handler, which faults on its own entry and locks the core; no handler runs and the only evidence is that execution stopped. An MPU region with no access permissions immediately below the stack region converts a silent overflow into a protection fault reported against the stacking operation, at the cost of one region and a granularity of the region size, which on the common implementations has a 32-byte floor and must be a power of two with a base aligned to its size. The third case is a fault in a handler that has already replaced the fault status registers — which is why reading the status registers as the first statement of every fault handler, before any other code, is the pattern that makes the mechanism work at all.

What the mechanism cannot supply is the state that was never stacked. Registers the handler’s prologue saved are in the frame only if the prologue is known; floating-point registers are in the frame only if the implementation stacked them, which depends on whether floating-point was active and on how the deferred stacking is configured, an ordering constraint that belongs with startup configuration. Everything else that was live at the moment of failure is in the stack below the frame, as long as the stack pointer was valid and the memory has not been reused, and that is often where the argument that produced the bad pointer is found.

References

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