HomeAbout UsContact Us

Fixing Cortex-M Hard Fault Handler Stack Corruption

By Jithin Tom
Published in Embedded C/C++
August 28, 2026
4 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
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, RETURN_ADDRESS, xPSR) when an exception occurs. This exception stack frame is fixed at 32 bytes. Your hard fault handler then executes in handler mode, using the main stack pointer (MSP) or process stack pointer (PSP) depending on your configuration.

Exception Stack Frame Structure

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

  • R0-R3 (argument registers)
  • R12 (intra-procedure call scratch register)
  • LR (link register)
  • RETURN_ADDRESS (program counter)
  • xPSR (program status register)

Handler Mode Stack Configuration

The stack pointer used in handler mode depends on the CONTROL register bit 1:

  • MSP (Main Stack Pointer): Default, used when CONTROL[1] = 0
  • PSP (Process Stack Pointer): Used when CONTROL[1] = 1 and in Thread mode

Most bare-metal and RTOS configurations use MSP for all exception handlers. If your hard fault handler uses MSP and the main stack is nearly exhausted, you have a problem.

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] │
├─────────────────────────┤ │ static struct info │
│ Handler local vars │ vs │ (zero stack impact) │
│ (DANGEROUS if large) │ └─────────────────────────┘
└─────────────────────────┘

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)8-16 bytes (return addr + frame)
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 = &_estack - sp;
// 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)
+ Handler locals (static = 0B, dynamic = X)
+ Nested exception margin (2x frame = 64B)
+ 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;
void HardFault_Handler(void) {
// 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;
__asm volatile (
" tst lr, #4 \n"
" ite eq \n"
" mrseq r0, msp \n"
" mrsne r0, psp \n"
" ldr r1, [r0, #24] \n"
" ldr r2, [r0, #20] \n"
" ldr r3, [r0, #16] \n"
" ldr r4, [r0, #12] \n"
" ldr r5, [r0, #8] \n"
" ldr r6, [r0, #4] \n"
" ldr r7, [r0] \n"
" str r0, [%[stacked_pc]] \n"
" str r1, [%[stacked_r0]] \n"
" str r2, [%[stacked_r1]] \n"
" str r3, [%[stacked_r2]] \n"
" str r4, [%[stacked_r3]] \n"
" str r5, [%[stacked_r12]] \n"
" str r6, [%[stacked_lr]] \n"
" str r7, [%[stacked_psr]] \n"
:
: [stacked_pc] "r" (&stacked_pc),
[stacked_r0] "r" (&stacked_r0),
[stacked_r1] "r" (&stacked_r1),
[stacked_r2] "r" (&stacked_r2),
[stacked_r3] "r" (&stacked_r3),
[stacked_r12] "r" (&stacked_r12),
[stacked_lr] "r" (&stacked_lr),
[stacked_psr] "r" (&stacked_psr)
: "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7"
);
// 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 static storage for all variables (zero stack usage)
  • Uses inline assembly to minimize function call overhead
  • Performs only essential actions (toggle LED, optional ITM output)
  • Has predictable, bounded execution time

Assembly Breakdown

┌─────────────────────────────────────────────────────────────────┐
│ 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 │
├─────────────────────────────────────────────────────────────────┤
│ ldr r1, [r0, #24] │ Load stacked R0 (offset 24) │
│ ldr r2, [r0, #20] │ Load stacked R1 (offset 20) │
│ ldr r3, [r0, #16] │ Load stacked R2 (offset 16) │
│ ldr r4, [r0, #12] │ Load stacked R3 (offset 12) │
│ ldr r5, [r0, #8] │ Load stacked R12 (offset 8) │
│ ldr r6, [r0, #4] │ Load stacked LR (offset 4) │
│ ldr r7, [r0] │ Load stacked PC (offset 0) │
├─────────────────────────────────────────────────────────────────┤
│ str r0-r7 to static │ Store to pre-allocated static variables │
└─────────────────────────────────────────────────────────────────┘

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 for testing
void test_hard_fault(void) {
volatile uint32_t* null_ptr = (uint32_t*)0x0;
*null_ptr = 0xDEADBEEF; // Null pointer dereference
}

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 lockup (Cortex-M3/M4) or escalates to NMI (Cortex-M7/M33).

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

On Cortex-M3/M4, a fault during hard fault handling causes immediate lockup. On Cortex-M7/M33, it escalates to NMI. You can prepare:

// NMI Handler for Cortex-M7/M33 double fault escalation
void NMI_Handler(void) {
// Check if this is a double fault escalation
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.

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

AddressSanitizer for Embedded C: Finding Memory Bugs in Firmware
AddressSanitizer for Embedded C: Finding Memory Bugs in Firmware
August 28, 2026
8 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media