Claude Cursor opencode Skill

hardfault-triage

Use when triaging processor exception crashes — HardFault, MemManage, BusFault, UsageFault, data/prefetch abort, illegal instruction, or watchdog timeout. Covers fault-register interpretation, stack-frame extraction, PC-to-source resolution, and root-cause classification.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download amethystluna-embedded-workbench-skills_hardfault-triage-94b6a4a.zip · 4 KB
Part of amethystluna/embedded-workbench — 7 skills

Install

skills CLI npx skills add https://github.com/AmethystLuna/embedded-workbench/tree/master/skills/hardfault-triage
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install amethystluna-embedded-workbench@llmmart
Git git clone https://github.com/AmethystLuna/embedded-workbench.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole amethystluna/embedded-workbench collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

HardFault & Exception Triage

When a processor exception fires, the register file and stack frame are a black-box recording of the crash. This skill covers how to read that recording.

The methodology is architecture-agnostic. Cortex-M (ARMv7-M/v8-M) fault registers and handlers are the primary reference; the same principles apply to Cortex-A aborts, RISC-V exceptions, Xtensa exception vectors, and other MCU fault paths.

REQUIRED BACKGROUND: Load Skill("debug-methodology") for structured root-cause analysis. If the crash involves RTOS tasks or ISR context, also load Skill("embedded-firmware-dev").

Triage Methodology

Every processor exception triage follows the same four-step flow:

Step 1 — Capture

Read the architecture's fault-status and fault-address registers before any other code runs. The first instructions after exception entry are the only guaranteed-clean snapshot.

  • Cortex-M: CFSR, HFSR, MMFAR, BFAR at SCB base 0xE000ED00
  • Cortex-A: DFSR, DFAR, IFSR, IFAR (data/prefetch abort)
  • RISC-V: mcause, mtval, mepc
  • Xtensa (ESP32): EXCCAUSE, EXCVADDR, EPC1

Step 2 — Classify

Match the fault-status bit pattern against known signatures. The tables in the architecture-specific sections below cover the most frequent Cortex-M patterns. For other architectures:

  • Look up the exception cause code in the architecture reference manual
  • Determine the fault class: memory access, instruction fetch, undefined opcode, alignment, privilege violation, bus error

Step 3 — Locate

Extract the program counter (PC) at the moment of the fault and resolve it to source:

  • .map file Global Symbols: search for the function whose address range contains the PC
  • addr2line: arm-none-eabi-addr2line -e firmware.axf <pc_address> (Cortex-M), equivalent for other toolchains
  • Disassembly listing: use the .lst file or debugger to find the exact instruction at PC − function_start

Step 4 — Root-Cause

Classify the defect behind the fault signature:

Fault Class Code Defect
Bus error at valid SRAM address Null pointer, use-after-free, buffer overflow into heap metadata
Bus error at peripheral address Unclocked or powered-down peripheral — RCC/PM init ordering
Undefined instruction / invalid state Corrupted function pointer, jumped to freed memory, stack smash overwrote LR
Stack overflow (exception entry) Task stack too small, deep call chain, large local variables, recursion
Imprecise bus error (no valid address) DMA to freed buffer, write-buffer coherency, cache maintenance
MPU / memory protection violation Region misconfiguration, attempted write to read-only region

Cortex-M Fault Architecture

Fault Register Quick Reference

All at System Control Block (SCB) base 0xE000ED00:

Register Address Key Bits
CFSR 0xE000ED28 Composite: UFSR[25:16] + BFSR[15:8] + MMFSR[7:0]
HFSR 0xE000ED2C Bit 30 FORCED=1 means escalated from other fault handler
MMFAR 0xE000ED34 Fault address (valid when MMFSR.MMARVALID=1)
BFAR 0xE000ED38 Fault address (valid when BFSR.BFARVALID=1)

CFSR sub-registers:

Sub-register Bits Covers
MMFSR (MemManage) CFSR[7:0] MPU violation, XN violation, access violation
BFSR (BusFault) CFSR[15:8] Precise/imprecise bus error, stack push/pop fault
UFSR (UsageFault) CFSR[25:16] Undefined instruction, invalid state, unaligned, divide-by-0

Common Fault Signatures (Cortex-M)

CFSR Value Decomposition Typical Root Cause
0x00008200 BFSR.PRECISERR (BFAR valid) Null pointer dereference, access to unclocked peripheral
0x00000400 BFSR.IMPRECISERR (BFAR not valid) Write-buffer async fault; DMA target freed; cache coherency
0x00020000 UFSR.INVSTATE Tried to execute ARM code in Thumb mode; corrupted function pointer
0x00010000 UFSR.UNDEFINSTR Jumped to data region; function pointer to freed or zeroed memory
0x01000000 UFSR.UNALIGNED Unaligned load/store (CCR.UNALIGN_TRP must be enabled)
0x00000001 MMFSR.IACCVIOL Executing from XN (execute-never) region — MPU config
0x00000002 MMFSR.DACCVIOL Writing to read-only memory — MPU config
0x00000800 BFSR.STKERR Stack overflow on exception entry (push to full stack)
0x00001000 BFSR.UNSTKERR Stack corruption on exception return (pop from corrupted stack)

Cortex-M Stack Frame Capture

The exception handler must be __attribute__((naked)) — no compiler prologue — so SP points directly at the exception stack frame. The processor automatically pushes R0-R3, R12, LR, PC, PSR on exception entry.

Minimal Fault Dump

static void fault_dump_basic(const char *name) {
    printf("FAULT: %s\n", name);
    printf("CFSR: 0x%08lX  HFSR: 0x%08lX\n", SCB->CFSR, SCB->HFSR);
    printf("MMFAR: 0x%08lX  BFAR: 0x%08lX\n", SCB->MMFAR, SCB->BFAR);
}

static __attribute__((used)) void HardFault_HandlerC(uint32_t *stacked) {
    fault_dump_basic("HardFault");
    printf("R0: 0x%08lX  R1: 0x%08lX  R2: 0x%08lX  R3: 0x%08lX\n",
           stacked[0], stacked[1], stacked[2], stacked[3]);
    printf("R12: 0x%08lX  LR: 0x%08lX  PC: 0x%08lX  PSR: 0x%08lX\n",
           stacked[4], stacked[5], stacked[6], stacked[7]);
    while (1) { /* halt for debugger attachment */ }
}

Naked Handler With Nested-Fault Guard

// Prevent infinite recursion if the fault handler itself faults (e.g., UART not init'd)
static volatile uint8_t in_fault = 0;

__attribute__((naked)) void HardFault_Handler(void) {
    __asm volatile(
        "tst lr, #4\n"          // EXC_RETURN bit 2: 0=MSP, 1=PSP
        "ite eq\n"
        "mrseq r0, msp\n"       // Thread used MSP → stack pointer in R0
        "mrsne r0, psp\n"       // Thread used PSP (RTOS tasks) → stack pointer in R0
        "b HardFault_HandlerC\n"
    );
}

Key points:

  • naked attribute is mandatory — without it, the compiler prologue overwrites SP before you can read the stacked frame
  • Check LR bit 2 to determine MSP vs PSP (RTOS tasks use PSP for thread stack; handlers and bare-metal threads use MSP)
  • The nested-fault guard (in_fault) prevents infinite HardFault recursion when the handler itself triggers a fault
  • Dump all fault registers AND stacked registers before halting — these are the only evidence

Stacked Register Layout

Exception frame on stack (Cortex-M, from low to high address):
Offset 0: R0          Offset 16: R12
Offset 4: R1          Offset 20: LR  (exception return address)
Offset 8: R2          Offset 24: PC  (faulting instruction)
Offset 12: R3         Offset 28: PSR (program status register)

PC-to-Source Resolution

Given the stacked PC from the exception handler:

Method 1: addr2line (fastest, one command)

arm-none-eabi-addr2line -e firmware.axf 0x08001234

Output: src/module/file.c:158. Works for any toolchain with an ELF-aware addr2line.

Method 2: .map File Global Symbols

  1. Open the .map file (at <listings>/<target>.map)
  2. Go to the Global Symbols section
  3. Find the function whose start address is the highest value ≤ PC
  4. Offset = PC − function_start. Cross-reference with the .lst listing file to find the exact instruction.

Method 3: Debugger Disassembly

In a debugger (Keil, Ozone, gdb), jump to the PC address in the disassembly window. The exact instruction and surrounding call context are visible.


Root-Cause Decision Tree

Fault type?
├── Precise bus error (BFAR valid)
│   ├── BFAR in SRAM region    → null pointer, use-after-free, buffer overflow
│   ├── BFAR in peripheral map → unclocked peripheral; check RCC/clock-gating init order
│   └── BFAR in flash/ROM      → alignment fault or const-data access violation
├── Imprecise bus error (BFAR not valid)
│   └── Suspect: DMA to freed buffer, cache-coherency after peripheral write, write-buffer
├── INVSTATE or UNDEFINSTR
│   ├── PC is a function pointer → freed object, overwritten vtable, buffer overflow on heap
│   └── PC is a return address   → stack corruption (local array overflow overwrote LR)
├── Stack overflow (STKERR)
│   ├── RTOS task → check uxTaskGetStackHighWaterMark() history, worst-case call chain
│   └── ISR/MSP  → check for large local variables in handlers, deep interrupt nesting
└── MPU violation (IACCVIOL / DACCVIOL)
    └── Compare MMFAR/BFAR against MPU region descriptors; check XN/read-only bits

Common Root Causes (Cross-Architecture)

Symptom Likely Root Cause Where to Look
PC in SRAM region Corrupted function pointer, stack overflow into code Stack high-water marks, vtable/heap integrity
PC = 0x00000000 or reset vector Null function pointer call, uninitialized callback Backtrace through LR to find caller
BFAR / fault-address = peripheral base Unclocked or powered-down peripheral Init ordering: clock enable before register access
IMPRECISERR / async fault, no valid address DMA to freed buffer, cache coherency DMA completion vs buffer deallocation ordering
INVSTATE + LR in event dispatch path Timer callback on already-freed RTOS object Timer lifecycle: stop timer before freeing object
HardFault escalates from other handler BusFault/MemManage/UsageFault not individually enabled Enable individual fault handlers (SHCSR register) for finer diagnostics
Fault only above certain uptime Slow resource leak: memory, timers, file descriptors Runtime telemetry trend before crash
Fault only after certain sequence State-dependent corruption: stale flag, uninitialized state State ownership boundaries, lifecycle cleanup completeness

MAP File for Crash Debugging

The .map file is essential for crash analysis. Key sections:

Section Use for Crash Analysis
Global Symbols Map PC → function name. Find the containing function, compute offset, locate the faulting line.
Memory Map of the image Verify the fault address falls in a valid region (Flash, SRAM, peripheral). Addresses outside all regions indicate a wild pointer.
Cross References Trace the call chain backward. Which .o files call into the faulting function?

For build-optimization .map analysis (size optimization, memory budget, linker garbage collection), see Skill("keil-mdk-build").


Cross-References

Scenario Load
Build-level .map analysis, ROM/RAM size optimization Skill("keil-mdk-build")
Structured root-cause analysis methodology Skill("debug-methodology")
FreeRTOS task stack overflow, ISR context faults Skill("embedded-firmware-dev")
State machine lockup triggering watchdog reset Skill("state-machine-design")
Naked handler and linker section patterns Skill("c-cpp-dev")
Files (embedded-workbench)
  • SKILL.md 11.3 KB
    ---
    name: hardfault-triage
    description: "Use when triaging processor exception crashes — HardFault, MemManage, BusFault, UsageFault, data/prefetch abort, illegal instruction, or watchdog timeout. Covers fault-register interpretation, stack-frame extraction, PC-to-source resolution, and root-cause classification."
    ---
    
    # HardFault & Exception Triage
    
    When a processor exception fires, the register file and stack frame are a black-box recording of the crash. This skill covers how to read that recording.
    
    The methodology is architecture-agnostic. Cortex-M (ARMv7-M/v8-M) fault registers and handlers are the primary reference; the same principles apply to Cortex-A aborts, RISC-V exceptions, Xtensa exception vectors, and other MCU fault paths.
    
    **REQUIRED BACKGROUND:** Load `Skill("debug-methodology")` for structured root-cause analysis. If the crash involves RTOS tasks or ISR context, also load `Skill("embedded-firmware-dev")`.
    
    ## Triage Methodology
    
    Every processor exception triage follows the same four-step flow:
    
    ### Step 1 — Capture
    
    Read the architecture's fault-status and fault-address registers **before any other code runs**. The first instructions after exception entry are the only guaranteed-clean snapshot.
    
    - Cortex-M: CFSR, HFSR, MMFAR, BFAR at SCB base `0xE000ED00`
    - Cortex-A: DFSR, DFAR, IFSR, IFAR (data/prefetch abort)
    - RISC-V: mcause, mtval, mepc
    - Xtensa (ESP32): EXCCAUSE, EXCVADDR, EPC1
    
    ### Step 2 — Classify
    
    Match the fault-status bit pattern against known signatures. The tables in the architecture-specific sections below cover the most frequent Cortex-M patterns. For other architectures:
    
    - Look up the exception cause code in the architecture reference manual
    - Determine the fault class: memory access, instruction fetch, undefined opcode, alignment, privilege violation, bus error
    
    ### Step 3 — Locate
    
    Extract the program counter (PC) at the moment of the fault and resolve it to source:
    
    - **.map file Global Symbols**: search for the function whose address range contains the PC
    - **addr2line**: `arm-none-eabi-addr2line -e firmware.axf <pc_address>` (Cortex-M), equivalent for other toolchains
    - **Disassembly listing**: use the `.lst` file or debugger to find the exact instruction at `PC − function_start`
    
    ### Step 4 — Root-Cause
    
    Classify the defect behind the fault signature:
    
    | Fault Class | Code Defect |
    | ----------- | ----------- |
    | Bus error at valid SRAM address | Null pointer, use-after-free, buffer overflow into heap metadata |
    | Bus error at peripheral address | Unclocked or powered-down peripheral — RCC/PM init ordering |
    | Undefined instruction / invalid state | Corrupted function pointer, jumped to freed memory, stack smash overwrote LR |
    | Stack overflow (exception entry) | Task stack too small, deep call chain, large local variables, recursion |
    | Imprecise bus error (no valid address) | DMA to freed buffer, write-buffer coherency, cache maintenance |
    | MPU / memory protection violation | Region misconfiguration, attempted write to read-only region |
    
    ---
    
    ## Cortex-M Fault Architecture
    
    ### Fault Register Quick Reference
    
    All at System Control Block (SCB) base `0xE000ED00`:
    
    | Register | Address | Key Bits |
    | ---------- | --------- | ---------- |
    | **CFSR** | `0xE000ED28` | Composite: UFSR[25:16] + BFSR[15:8] + MMFSR[7:0] |
    | **HFSR** | `0xE000ED2C` | Bit 30 FORCED=1 means escalated from other fault handler |
    | **MMFAR** | `0xE000ED34` | Fault address (valid when MMFSR.MMARVALID=1) |
    | **BFAR** | `0xE000ED38` | Fault address (valid when BFSR.BFARVALID=1) |
    
    **CFSR sub-registers**:
    
    | Sub-register | Bits | Covers |
    | ------------ | ---- | ------ |
    | **MMFSR** (MemManage) | CFSR[7:0] | MPU violation, XN violation, access violation |
    | **BFSR** (BusFault) | CFSR[15:8] | Precise/imprecise bus error, stack push/pop fault |
    | **UFSR** (UsageFault) | CFSR[25:16] | Undefined instruction, invalid state, unaligned, divide-by-0 |
    
    ### Common Fault Signatures (Cortex-M)
    
    | CFSR Value | Decomposition | Typical Root Cause |
    | ----------- | ------------- | ------------------ |
    | `0x00008200` | BFSR.PRECISERR (BFAR valid) | Null pointer dereference, access to unclocked peripheral |
    | `0x00000400` | BFSR.IMPRECISERR (BFAR not valid) | Write-buffer async fault; DMA target freed; cache coherency |
    | `0x00020000` | UFSR.INVSTATE | Tried to execute ARM code in Thumb mode; corrupted function pointer |
    | `0x00010000` | UFSR.UNDEFINSTR | Jumped to data region; function pointer to freed or zeroed memory |
    | `0x01000000` | UFSR.UNALIGNED | Unaligned load/store (CCR.UNALIGN_TRP must be enabled) |
    | `0x00000001` | MMFSR.IACCVIOL | Executing from XN (execute-never) region — MPU config |
    | `0x00000002` | MMFSR.DACCVIOL | Writing to read-only memory — MPU config |
    | `0x00000800` | BFSR.STKERR | Stack overflow on exception entry (push to full stack) |
    | `0x00001000` | BFSR.UNSTKERR | Stack corruption on exception return (pop from corrupted stack) |
    
    ---
    
    ## Cortex-M Stack Frame Capture
    
    The exception handler must be `__attribute__((naked))` — no compiler prologue — so SP points directly at the exception stack frame. The processor automatically pushes R0-R3, R12, LR, PC, PSR on exception entry.
    
    ### Minimal Fault Dump
    
    ```c
    static void fault_dump_basic(const char *name) {
        printf("FAULT: %s\n", name);
        printf("CFSR: 0x%08lX  HFSR: 0x%08lX\n", SCB->CFSR, SCB->HFSR);
        printf("MMFAR: 0x%08lX  BFAR: 0x%08lX\n", SCB->MMFAR, SCB->BFAR);
    }
    
    static __attribute__((used)) void HardFault_HandlerC(uint32_t *stacked) {
        fault_dump_basic("HardFault");
        printf("R0: 0x%08lX  R1: 0x%08lX  R2: 0x%08lX  R3: 0x%08lX\n",
               stacked[0], stacked[1], stacked[2], stacked[3]);
        printf("R12: 0x%08lX  LR: 0x%08lX  PC: 0x%08lX  PSR: 0x%08lX\n",
               stacked[4], stacked[5], stacked[6], stacked[7]);
        while (1) { /* halt for debugger attachment */ }
    }
    ```
    
    ### Naked Handler With Nested-Fault Guard
    
    ```c
    // Prevent infinite recursion if the fault handler itself faults (e.g., UART not init'd)
    static volatile uint8_t in_fault = 0;
    
    __attribute__((naked)) void HardFault_Handler(void) {
        __asm volatile(
            "tst lr, #4\n"          // EXC_RETURN bit 2: 0=MSP, 1=PSP
            "ite eq\n"
            "mrseq r0, msp\n"       // Thread used MSP → stack pointer in R0
            "mrsne r0, psp\n"       // Thread used PSP (RTOS tasks) → stack pointer in R0
            "b HardFault_HandlerC\n"
        );
    }
    ```
    
    **Key points**:
    
    - `naked` attribute is mandatory — without it, the compiler prologue overwrites SP before you can read the stacked frame
    - Check LR bit 2 to determine MSP vs PSP (RTOS tasks use PSP for thread stack; handlers and bare-metal threads use MSP)
    - The nested-fault guard (`in_fault`) prevents infinite HardFault recursion when the handler itself triggers a fault
    - Dump all fault registers AND stacked registers before halting — these are the only evidence
    
    ### Stacked Register Layout
    
    ```text
    Exception frame on stack (Cortex-M, from low to high address):
    Offset 0: R0          Offset 16: R12
    Offset 4: R1          Offset 20: LR  (exception return address)
    Offset 8: R2          Offset 24: PC  (faulting instruction)
    Offset 12: R3         Offset 28: PSR (program status register)
    ```
    
    ---
    
    ## PC-to-Source Resolution
    
    Given the stacked PC from the exception handler:
    
    ### Method 1: addr2line (fastest, one command)
    
    ```text
    arm-none-eabi-addr2line -e firmware.axf 0x08001234
    ```
    
    Output: `src/module/file.c:158`. Works for any toolchain with an ELF-aware addr2line.
    
    ### Method 2: .map File Global Symbols
    
    1. Open the `.map` file (at `<listings>/<target>.map`)
    2. Go to the **Global Symbols** section
    3. Find the function whose start address is the highest value ≤ PC
    4. Offset = PC − function_start. Cross-reference with the `.lst` listing file to find the exact instruction.
    
    ### Method 3: Debugger Disassembly
    
    In a debugger (Keil, Ozone, gdb), jump to the PC address in the disassembly window. The exact instruction and surrounding call context are visible.
    
    ---
    
    ## Root-Cause Decision Tree
    
    ```text
    Fault type?
    ├── Precise bus error (BFAR valid)
    │   ├── BFAR in SRAM region    → null pointer, use-after-free, buffer overflow
    │   ├── BFAR in peripheral map → unclocked peripheral; check RCC/clock-gating init order
    │   └── BFAR in flash/ROM      → alignment fault or const-data access violation
    ├── Imprecise bus error (BFAR not valid)
    │   └── Suspect: DMA to freed buffer, cache-coherency after peripheral write, write-buffer
    ├── INVSTATE or UNDEFINSTR
    │   ├── PC is a function pointer → freed object, overwritten vtable, buffer overflow on heap
    │   └── PC is a return address   → stack corruption (local array overflow overwrote LR)
    ├── Stack overflow (STKERR)
    │   ├── RTOS task → check uxTaskGetStackHighWaterMark() history, worst-case call chain
    │   └── ISR/MSP  → check for large local variables in handlers, deep interrupt nesting
    └── MPU violation (IACCVIOL / DACCVIOL)
        └── Compare MMFAR/BFAR against MPU region descriptors; check XN/read-only bits
    ```
    
    ---
    
    ## Common Root Causes (Cross-Architecture)
    
    | Symptom | Likely Root Cause | Where to Look |
    | --------- | ------------------ | ------- |
    | PC in SRAM region | Corrupted function pointer, stack overflow into code | Stack high-water marks, vtable/heap integrity |
    | PC = `0x00000000` or reset vector | Null function pointer call, uninitialized callback | Backtrace through LR to find caller |
    | BFAR / fault-address = peripheral base | Unclocked or powered-down peripheral | Init ordering: clock enable before register access |
    | IMPRECISERR / async fault, no valid address | DMA to freed buffer, cache coherency | DMA completion vs buffer deallocation ordering |
    | INVSTATE + LR in event dispatch path | Timer callback on already-freed RTOS object | Timer lifecycle: stop timer before freeing object |
    | HardFault escalates from other handler | BusFault/MemManage/UsageFault not individually enabled | Enable individual fault handlers (SHCSR register) for finer diagnostics |
    | Fault only above certain uptime | Slow resource leak: memory, timers, file descriptors | Runtime telemetry trend before crash |
    | Fault only after certain sequence | State-dependent corruption: stale flag, uninitialized state | State ownership boundaries, lifecycle cleanup completeness |
    
    ---
    
    ## MAP File for Crash Debugging
    
    The `.map` file is essential for crash analysis. Key sections:
    
    | Section | Use for Crash Analysis |
    | --------- | ------------------- |
    | **Global Symbols** | Map PC → function name. Find the containing function, compute offset, locate the faulting line. |
    | **Memory Map of the image** | Verify the fault address falls in a valid region (Flash, SRAM, peripheral). Addresses outside all regions indicate a wild pointer. |
    | **Cross References** | Trace the call chain backward. Which `.o` files call into the faulting function? |
    
    For build-optimization `.map` analysis (size optimization, memory budget, linker garbage collection), see `Skill("keil-mdk-build")`.
    
    ---
    
    ## Cross-References
    
    | Scenario | Load |
    | -------- | ---- |
    | Build-level .map analysis, ROM/RAM size optimization | `Skill("keil-mdk-build")` |
    | Structured root-cause analysis methodology | `Skill("debug-methodology")` |
    | FreeRTOS task stack overflow, ISR context faults | `Skill("embedded-firmware-dev")` |
    | State machine lockup triggering watchdog reset | `Skill("state-machine-design")` |
    | Naked handler and linker section patterns | `Skill("c-cpp-dev")` |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related