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:
- Reproduce reliably (and note exactly how)
- Observe: breakpoints, logs, scope/logic analyzer
- Hypothesise one cause; predict what you'd see if true
- Test by changing one thing; keep notes
- Fix the root cause and add a regression test
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:
- NULL or dangling pointer dereference
- Stack overflow corrupting return addresses
- Calling a function pointer that holds garbage (INVSTATE — address with bit 0 clear)
- Accessing a peripheral whose clock is disabled (BusFault)
- Buffer overflow corrupting neighbouring variables
- FPU instructions before enabling the FPU (NOCP)
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
- CFSR layout:
bits 0–7 MMFSR, 8–15 BFSR, 16–31 UFSR - Key BusFault bits:
PRECISERR = bit 9, IMPRECISERR = bit 10, BFARVALID = bit 15 - Key UsageFault bits:
UNDEFINSTR 16, INVSTATE 17, INVPC 18, NOCP 19, UNALIGNED 24, DIVBYZERO 25 - Key MemManage bits:
IACCVIOL 0, DACCVIOL 1, MMARVALID 7 - Stack frame:
SP[0..7] = R0, R1, R2, R3, R12, LR, PC, xPSR → faulting PC = SP[6]
Worked example: Decode a crash
Given: CFSR = 0x00008200, BFAR = 0x40023830, PC = 0x08000F12
Find: Cause.
- Bit 15 BFARVALID + bit 9 PRECISERR
- 0x40023830 is a peripheral register region
- Instruction at PC reads that register
Answer: Precise BusFault accessing a peripheral — typically its clock was not enabled
Worked example: INVSTATE
Given: CFSR = 0x00020000; Stacked PC = 0x00000000
Find: Cause.
- Branch to an address without Thumb bit
- PC = 0 → called a NULL function pointer
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?
- list *0x08001A3C → faulting line
- list *0x08002345 → caller
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 haltWatchpoints 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
- Wrong: Leaving HardFault_Handler as an empty loop. — Right: Capture PC/LR/CFSR and keep a crash record.
- Wrong: Debugging only at -O2 and trusting variable views. — Right: Use -Og or read disassembly; understand optimisation.
- Wrong: Changing several things at once. — Right: One hypothesis, one change, one observation.
Applications
- Bring-up: Finding clock, pin and memory-map mistakes.
- Field reliability: Crash records reported over the network.
- Memory corruption: Watchpoints on canary values and buffers.
- Regression prevention: Every fixed bug becomes a test case.
Key takeaways
- Reproduce → hypothesise → test one change.
- CFSR says why, stacked PC says where.
- Capture crash records; map PC with addr2line.
- Use conditional breakpoints and watchpoints.
Going further
- Core dumps: Save full RAM + registers for offline GDB analysis (Memfault, Zephyr coredump).
- Semihosting: Target uses host I/O via the debugger — slow but handy.
- RTOS-aware debugging: IDE plugins show per-task stacks and states.