GDB Debugging & HardFault Analysis

Systematic debugging separates professionals from beginners. Learn GDB for embedded (breakpoints, conditional breakpoints, watchpoints, backtraces, memory examination, register views), debugging optimised code, Cortex-M fault exceptions (HardFault, MemManage, BusFault, UsageFault), decoding CFSR/HFSR/MMFAR/BFAR, recovering the faulting PC from the stacked frame, and post-mortem crash logging in the field.

Embedded bugs rarely print nice error messages. The typical symptom is 'it stopped' — the CPU is spinning in HardFault_Handler. The information you need is still there: fault status registers say what went wrong, and the exception stack frame says where.

GDB (through your IDE or command line) is the tool for reading all of this. Combined with a scientific approach — reproduce, hypothesise, test one change at a time — even rare bugs become tractable.

An analogy

A HardFault is a black-box flight recorder: the crash already happened, but the recorder (stacked registers and fault status) tells you the instruction, the address and the reason — if you know how to read it.

A debugging method

Guessing wastes days. Follow a loop:

Fault exceptions on Cortex-M

MemManage: MPU violation or execute-never region. BusFault: access to a non-existent address or a bus error (precise = exact instruction known; imprecise = buffered write, PC is later). UsageFault: undefined instruction, invalid state (branching to an even address), divide-by-zero (if enabled), unaligned access (if enabled). If these are disabled or fault while handling, they escalate to HardFault with HFSR.FORCED set.

Common root causes

Most faults come from a short list:

Debugging optimised and field code

With -O2, variables live in registers and lines are reordered; use -Og for development, 'volatile' temporarily for inspection, or read the disassembly. Enable the separate fault handlers (SCB->SHCSR) and DIV_0_TRP to get precise causes. Make imprecise bus faults precise during debugging by disabling write buffering (ACTLR.DISDEFWBUF). In the field, store crash records in no-init RAM or Flash, and include build IDs so addresses map to the right .elf.

Formulas

Worked example: Decode a crash

Given: CFSR = 0x00008200, BFAR = 0x40023830, PC = 0x08000F12

Find: Cause.

Answer: Precise BusFault accessing a peripheral — typically its clock was not enabled

Worked example: INVSTATE

Given: CFSR = 0x00020000; Stacked PC = 0x00000000

Find: Cause.

Answer: Uninitialised callback pointer

Worked example: Using the stack frame

Given: x/8wx $sp → … LR = 0x08002345, PC = 0x08001A3C

Find: Where did it crash and who called?

Answer: Faulting line and caller identified without any logging

HardFault handler that captures the crash context

typedef struct { uint32_t r0, r1, r2, r3, r12, lr, pc, xpsr; } frame_t;

__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"
        "mrsne r0, psp     \n"
        "b hardfault_c     \n");
}

__attribute__((section(".noinit"))) volatile struct {
    uint32_t magic, pc, lr, cfsr, hfsr, bfar, mmfar;
} crash;                                   /* survives the reset below */

void hardfault_c(frame_t *f)
{
    crash.magic = 0xDEADC0DE;
    crash.pc    = f->pc;                    /* instruction that faulted */
    crash.lr    = f->lr;                    /* caller                   */
    crash.cfsr  = SCB->CFSR;
    crash.hfsr  = SCB->HFSR;
    crash.bfar  = SCB->BFAR;
    crash.mmfar = SCB->MMFAR;
    __BKPT(0);                              /* halt here if debugger attached */
    NVIC_SystemReset();                     /* field: reboot, report on next boot */
}

On the next boot, if crash.magic is set, log or transmit the record. 'addr2line -e app.elf 0x<pc>' converts the PC into a file and line number.

Essential GDB commands

(gdb) break motor.c:142 if speed > 3000     # conditional breakpoint
(gdb) watch -l rx_buf[63]                     # hardware watchpoint on a location
(gdb) bt                                      # backtrace (call stack)
(gdb) info registers                          # core registers
(gdb) p/x SCB->CFSR                           # fault status in hex
(gdb) x/8wx $sp                               # dump 8 words at SP (stacked frame)
(gdb) info symbol 0x08001a3c                  # which function contains this PC
(gdb) list *0x08001a3c                        # source line at that address
(gdb) display/i $pc                           # show next instruction when stepping
(gdb) monitor reset halt                      # OpenOCD: reset and halt

Watchpoints and conditional breakpoints find memory corruption and rare conditions without adding code. x/8wx $sp in a fault handler shows the stacked R0–xPSR frame directly.

Common mistakes

Applications

Key takeaways

Going further