HomeAbout UsContact Us

Fixing Cortex-M Hard Fault Handler Stack Corruption

By Jithin Tom
Published in Embedded C/C++
August 28, 2026
5 min read
Fixing Cortex-M Hard Fault Handler Stack Corruption

Table Of Contents

01
The Problem: Fault Handler Stack Overflow
02
Root Cause Analysis
03
Solution Approach: Minimalist Handler Design
04
Implementation Example
05
Verification Steps
06
Common Mistakes to Avoid
07
Measurable Improvements
08
Advanced: Double Fault Handling and Lockup
09
Summary
10
Related Reading
11
References
12
Frequently Asked Questions

When a Cortex-M processor encounters a hard fault, the last thing you want is for your fault handler to make things worse by corrupting the stack. Yet this is a common issue that turns a debuggable fault into a system lockup. Let’s look at why this happens and how to fix it.

The Problem: Fault Handler Stack Overflow

A Cortex-M hard fault handler that uses too much stack space can corrupt the very exception stack frame it’s trying to examine. This creates a vicious cycle: the fault occurs, the handler starts executing, uses excessive stack, overflows into the faulted context’s stack frame, corrupts the return address or registers, and triggers another fault—often locking up the system completely.

Diagram showing normal exception stack vs corrupted stack during hard fault handling
Diagram showing normal exception stack vs corrupted stack during hard fault handling

Root Cause Analysis

The Cortex-M processor automatically stacks certain registers (R0-R3, R12, LR, PC, xPSR) when an exception occurs. This basic exception stack frame is fixed at 32 bytes (or 104 bytes if the floating-point unit (FPU) is active and extended stacking occurs). Your hard fault handler then executes in handler mode, always using the main stack pointer (MSP).

Exception Stack Frame Structure

When an exception occurs, the processor pushes the following 8 registers (32 bytes total for a basic frame):

  • R0-R3 (argument registers)
  • R12 (intra-procedure call scratch register)
  • LR (link register, holding the return address of the caller)
  • PC (program counter at the point of exception)
  • xPSR (program status register)

Handler Mode Stack Configuration

Exception handlers on Cortex-M always execute using the MSP (Main Stack Pointer). The CONTROL[1] (SPSEL) bit determines which stack pointer Thread mode uses:

  • SPSEL = 0: Thread mode uses MSP (bare-metal default)
  • SPSEL = 1: Thread mode uses PSP (typical RTOS configuration)

When a fault occurs, the processor pushes the exception frame onto whichever stack the faulted code was using (MSP or PSP), then switches to MSP for the handler. The EXC_RETURN value in LR tells you which stack holds the frame. If your main stack is nearly exhausted, the handler’s own stack usage on MSP can overflow.

Stack Overflow Scenarios

Stack corruption typically occurs in these scenarios:

  1. Deep call chains in the handler: Calling printf, sprintf, or complex logging functions
  2. Large local arrays/buffers: Declaring char buffer[256] or struct FaultInfo info on stack
  3. Insufficient handler stack allocation: Linker script allocates 1KB but handler needs 2KB
  4. Nested exceptions: A second fault occurring while handling the first (double fault)

Solution Approach: Minimalist Handler Design

The fix follows a simple principle: keep your hard fault handler as minimal as possible. Every byte of stack you save in the handler reduces the risk of corruption.

1. Eliminate Large Local Variables

Instead of:

void HardFault_Handler(void) {
uint32_t registers[8]; // 32 bytes on stack
char buffer[128]; // 128 bytes on stack
struct FaultInfo info; // Another 32+ bytes
// ... handler code
}

Do this:

void HardFault_Handler(void) {
static uint32_t registers[8]; // Static allocation
static char buffer[128]; // Static allocation
static struct FaultInfo info; // Static allocation
// ... handler code using static variables
}

Static variables live in .bss or .data sections, not on the stack. This moves your storage requirements off the stack entirely.

Why Static Allocation Works

Stack Memory (grows down) .bss/.data Section (fixed)
+-------------------------+ +-------------------------+
| Exception Stack Frame | | static uint32_t regs[8] |
| (32 bytes, fixed) | | static char buf[128] |
+-------------------------+ vs | static struct info |
| Handler local vars | | |
| (DANGEROUS if large) | | (zero stack impact) |
+-------------------------+ +-------------------------+

2. Limit Function Calls

Each function call uses stack space for return addresses, parameters, and local variables. In a hard fault handler, avoid calling complex functions.

Instead of:

void HardFault_Handler(void) {
log_fault_details(); // Who knows how much stack this uses?
send_alert_via_uart(); // Another unknown
analyze_fault_cause(); // And another
}

Do this:

static void fault_handler_impl(void) {
// Minimal implementation - no function calls
// Direct register manipulation only
}
void HardFault_Handler(void) {
fault_handler_impl(); // Single, predictable call
}

Or better yet, inline the critical code directly in the handler.

Stack Cost of Function Calls

Call TypeTypical Stack Usage
Leaf function (no locals)0-8 bytes (may not push LR)
Function with 4 params16-32 bytes
printf family100-500+ bytes
memcpy/memset16-64 bytes
Complex logging200-1000+ bytes

3. Monitor Stack Usage

You can’t fix what you don’t measure. Add stack monitoring to your hard fault handler:

void HardFault_Handler(void) {
// Get current stack pointer
uint32_t* sp = (uint32_t*)__get_MSP();
// Stack grows downward on Cortex-M
// Check if we're getting too close to stack limit
extern uint32_t _estack; // Defined in linker script
uint32_t stack_used = (uint32_t)&_estack - (uint32_t)sp; // Calculate in bytes
// If stack usage is critical, trigger minimal fault indication
if (stack_used > CRITICAL_STACK_THRESHOLD) {
// Set GPIO pin, toggle LED, etc. - minimal action
GPIOB->ODR |= GPIO_ODR_ODR_5;
}
// Continue with normal fault handling...
}

Compile-Time Stack Analysis

Use compiler flags to analyze stack usage:

# GCC/Clang: report stack usage per function
arm-none-eabi-gcc -fstack-usage -c hard_fault.c
# Output: hard_fault.su with per-function stack usage

4. Allocate Adequate Handler Mode Stack

Ensure your linker script allocates sufficient stack for handler mode. The default stack size might be too small if you have nested exceptions or deep fault handling.

In your linker script:

/* Stack size for handler mode (exceptions) */
_estack = 0x20008000; /* Example: 32KB stack top */
_stack_size = 0x2000; /* 8KB for handler mode */
_Min_Heap_Size = 0x200; /* 512B heap */
_Min_Stack_Size = 0x400; /* 1KB minimum stack */

Calculating Required Stack Size

Minimum handler stack = Exception frame (32B, or 104B with FPU)
+ Handler locals (static = 0B, dynamic = X)
+ Nested exception margin (2x frame = 64B or 208B)
+ Function call overhead (Y)
+ Safety margin (256B-1KB)
Typical safe allocation: 2KB-8KB for handler mode

5. Handle Fault Status Registers Efficiently

Extract fault information without function calls:

void HardFault_Handler(void) {
// Read fault status registers directly
uint32_t cfsr = SCB->CFSR; // Configurable Fault Status Register
uint32_t hfsr = SCB->HFSR; // Hard Fault Status Register
uint32_t dfsr = SCB->DFSR; // Debug Fault Status Register
uint32_t mmfar = SCB->MMFAR; // MemManage Fault Address Register
uint32_t bfar = SCB->BFAR; // Bus Fault Address Register
// Store to static variables for later analysis
static uint32_t fault_cfsr, fault_hfsr, fault_mmfar, fault_bfar;
fault_cfsr = cfsr;
fault_hfsr = hfsr;
fault_mmfar = mmfar;
fault_bfar = bfar;
// Minimal indication
GPIOD->ODR ^= GPIO_ODR_ODR_12;
while (1) { /* halt */ }
}

Implementation Example

Here’s a complete hard fault handler that minimizes stack usage:

#include "stm32f4xx.h"
// Static storage for fault analysis - zero stack usage
static uint32_t stacked_r0;
static uint32_t stacked_r1;
static uint32_t stacked_r2;
static uint32_t stacked_r3;
static uint32_t stacked_r12;
static uint32_t stacked_lr;
static uint32_t stacked_pc;
static uint32_t stacked_psr;
// Fault status registers
static uint32_t fault_cfsr;
static uint32_t fault_hfsr;
static uint32_t fault_dfsr;
static uint32_t fault_mmfar;
static uint32_t fault_bfar;
// Naked wrapper: no compiler prologue, so LR still holds EXC_RETURN
__attribute__((naked)) void HardFault_Handler(void) {
__asm volatile (
" tst lr, #4 \n"
" ite eq \n"
" mrseq r0, msp \n"
" mrsne r0, psp \n"
" b HardFault_Handler_C \n"
);
}
// C handler receives the stack frame pointer in r0 (first argument)
void HardFault_Handler_C(uint32_t *frame) {
// Capture fault status registers immediately
fault_cfsr = SCB->CFSR;
fault_hfsr = SCB->HFSR;
fault_dfsr = SCB->DFSR;
fault_mmfar = SCB->MMFAR;
fault_bfar = SCB->BFAR;
// Extract stacked registers from the exception frame
stacked_r0 = frame[0]; // offset 0
stacked_r1 = frame[1]; // offset 4
stacked_r2 = frame[2]; // offset 8
stacked_r3 = frame[3]; // offset 12
stacked_r12 = frame[4]; // offset 16
stacked_lr = frame[5]; // offset 20
stacked_pc = frame[6]; // offset 24
stacked_psr = frame[7]; // offset 28
// Minimal fault indication - toggle LED
GPIOD->ODR ^= GPIO_ODR_ODR_12;
// Optional: send minimal fault code via ITM
if (ITM->TCR & ITM_TCR_ITMENA_Msk) {
ITM->PORT[0].u8 = 0xDE; // Fault marker
ITM->PORT[0].u8 = stacked_r0 & 0xFF;
ITM->PORT[0].u8 = stacked_r1 & 0xFF;
}
// Halt or reset as appropriate for your system
while (1) {
// Spin forever or trigger watchdog reset
}
}

This handler:

  • Uses a naked assembly wrapper to capture EXC_RETURN before the compiler modifies LR
  • Passes the correct stack frame pointer to a C function via the AAPCS calling convention (r0)
  • Uses static storage for all variables (zero stack usage beyond the function call frame)
  • Performs only essential actions (toggle LED, optional ITM output)
  • Has predictable, bounded execution time

Assembly Breakdown

+-----------------------+---------------------------------------------------+
| HardFault_Handler (naked): |
+-----------------------+---------------------------------------------------+
| tst lr, #4 | Check EXC_RETURN bit 2 (MSP vs PSP) |
| ite eq | If-then-else: equal -> MSP, not equal -> PSP |
| mrseq r0, msp | Move MSP to R0 if equal |
| mrsne r0, psp | Move PSP to R0 if not equal |
| b HardFault_Handler_C | Branch to C handler with frame ptr in R0 |
+-----------------------+---------------------------------------------------+
| HardFault_Handler_C(frame): |
+-----------------------+---------------------------------------------------+
| frame[0] = R0 | Stacked R0 (offset 0) |
| frame[1] = R1 | Stacked R1 (offset 4) |
| frame[2] = R2 | Stacked R2 (offset 8) |
| frame[3] = R3 | Stacked R3 (offset 12) |
| frame[4] = R12 | Stacked R12 (offset 16) |
| frame[5] = LR | Stacked LR (offset 20) |
| frame[6] = PC | Stacked PC (offset 24) |
| frame[7] = xPSR | Stacked xPSR (offset 28) |
+-----------------------+---------------------------------------------------+

Verification Steps

After implementing your minimal hard fault handler:

1. Check Stack Usage in Map File

arm-none-eabi-gcc -Wl,-Map=output.map -o firmware.elf ...
grep -A5 -B5 "HardFault_Handler" output.map

Verify your handler doesn’t show large stack consumption.

2. Trigger a Known Fault

// Force a hard fault by branching to an invalid/non-thumb address
void test_hard_fault(void) {
void (*bad_func)(void) = (void (*)(void))0x00000000;
bad_func();
}

3. Observe Behavior

The system should fault, execute your handler minimally, and either halt or reset cleanly.

4. Monitor Fault Registers

Use a debugger to inspect the stacked register values and fault status registers:

(gdb) monitor reset halt
(gdb) b HardFault_Handler
(gdb) c
# Trigger fault...
(gdb) p/x fault_cfsr
(gdb) p/x fault_hfsr
(gdb) p/x stacked_pc

5. Test Fault Nesting

Ensure your handler doesn’t fault again when handling the initial fault. A second fault during hard fault handling causes the processor to enter the Lockup state on all Cortex-M variants.

Common Mistakes to Avoid

1. Using printf in Fault Handlers

The semi-hosting or UART functions can use hundreds of bytes of stack. Never call printf, sprintf, snprintf, or any variadic function.

2. Calling Complex Logging Functions

Even simple string formatting can blow your stack. If you need logging, use static buffers and direct register writes.

3. Large Stack Allocations for Debug Buffers

That 256-byte buffer for fault analysis? It’s on the stack. Make it static.

4. Insufficient Handler Mode Stack

Check your linker script - handler mode needs adequate space. A 1KB stack is often insufficient.

5. Faulting Within the Fault Handler

Creates infinite fault loops that lock up the system. Test thoroughly.

6. Ignoring the Process Stack Pointer (PSP)

If your RTOS uses PSP for tasks, the faulted context might be on PSP, not MSP. Always check the EXC_RETURN value.

7. Not Clearing Fault Status Registers

Some fault status bits are sticky. Clear them before re-enabling interrupts or continuing.

Measurable Improvements

With a properly designed minimal hard fault handler:

MetricBeforeAfter
Stack usage200+ bytes< 32 bytes (just exception frame)
Fault recoveryUnreliableSystem reliably enters handler mode
Debug visibilityCorrupted registersFault context registers intact
Deterministic behaviorUnpredictablePredictable execution time
System stabilityLockups commonNo lockups from handler corruption

Advanced: Double Fault Handling and Lockup

On Cortex-M processors, a fault occurring during hard fault handling causes an immediate Lockup state. Once in Lockup, the processor halts instruction execution. However, an NMI (Non-Maskable Interrupt, priority -2) can preempt the Lockup state, providing a final opportunity to safely log the error or trigger a reset:

// NMI Handler can preempt a Lockup state
void NMI_Handler(void) {
// If we were in Lockup, NMI will still execute
// We can attempt emergency recovery or logging here
if (SCB->HFSR & SCB_HFSR_FORCED_Msk) {
// Double fault occurred - minimal recovery attempt
static uint32_t double_fault_count;
double_fault_count++;
// Emergency indication
GPIOE->ODR |= GPIO_ODR_ODR_5;
// Log to backup registers (if available)
// RTC backup registers survive reset on many STM32
RTC->BKP0R = double_fault_count;
RTC->BKP1R = fault_cfsr;
RTC->BKP2R = stacked_pc;
}
while (1) { /* halt */ }
}

Summary

Fixing Cortex-M hard fault handler stack corruption requires shifting your mindset from “diagnostic tool” to “minimal survival kit”. Your hard fault handler isn’t the place for comprehensive fault analysis—it’s the last line of defense before system failure. By keeping it minimal, using static storage, limiting function calls, and verifying adequate stack allocation, you ensure that when faults occur, your handler can actually do its job: preserve fault context, provide minimal indication, and allow for safe system recovery or reset.

The next time you’re debugging a hard fault, ask yourself: “Is my handler making the problem worse?” If it’s using significant stack space, the answer is likely yes. Apply these principles, and your hard fault handler will become a reliable diagnostic tool rather than a source of additional instability.

💡 Key Takeaway: In exception handlers, every byte of stack you save increases system reliability. Store data statically, minimize function calls, and keep your hard fault handler as small as possible.

  • Fixing Zephyr Devicetree Overlays That Silently Fail
  • Optimizing Cortex-M Tail Chaining for Sub-Microsecond Latency

References

  1. ARM Cortex-M4 Devices Generic User Guide, ARM DUI 0553A - Exception handling and fault behavior
  2. ARMv7-M Architecture Reference Manual, ARM DDI 0403E - Exception model and fault status registers
  3. Joseph Yiu, “The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors”, 3rd Edition, Newnes 2014 - Chapter 12: Fault Handling
  4. STM32F4 Reference Manual RM0090, STMicroelectronics - Section 4.3: Hard Fault and other fault exceptions
  5. FreeRTOS Hard Fault Handler Implementation Guide, FreeRTOS.org - Minimal handler patterns for RTOS contexts
  6. “Debugging Hard Faults on Cortex-M” - ARM Community Article, 2023 - Practical debugging workflows

Frequently Asked Questions

What causes stack corruption in Cortex-M hard fault handlers?

Stack corruption in hard fault handlers typically occurs when the handler itself uses too much stack space, causing it to overflow into other memory regions or corrupt the exception stack frame. This can happen due to large local variables, deep function calls, or insufficient stack allocation for the handler mode.

How can I prevent hard fault handler stack overflow?

Prevent hard fault handler stack overflow by keeping the handler minimal: avoid large local variables, limit function calls, use static allocation for necessary buffers, and ensure adequate stack size is allocated for handler mode in your linker script or startup code. Monitor stack usage with tools like __builtin_return_address or hardware stack pointers.

What are the signs of hard fault handler stack corruption?

Signs include the hard fault handler itself triggering another fault (lockup), erratic behavior after a hard fault, corrupted register values in the fault status registers, or the system resetting unexpectedly when attempting to debug a hard fault. The linker map may show the handler mode stack overlapping with other sections.

Tags

cortex-mhard-faultstack-corruptionstm32debugging

Share


Previous Article
AddressSanitizer for Embedded C: Finding Memory Bugs in Firmware
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Fixing Slow GPIO Toggling on STM32: Register-Level Optimization
Fixing Slow GPIO Toggling on STM32: Register-Level Optimization
September 05, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media