HomeAbout UsContact Us

Preventing ISR Stack Overflow in Embedded C

By Jithin Tom
Published in Embedded C/C++
August 31, 2026
4 min read
Preventing ISR Stack Overflow in Embedded C

Table Of Contents

01
Why ISRs Are Prone to Stack Overflow
02
Solution Approaches
03
Code Examples
04
Verification and Testing
05
Summary
06
Related Reading
07
References
08
Frequently Asked Questions

Interrupt Service Routines (ISRs) are critical for real-time embedded systems, but they are also a common source of stack-related bugs. Stack overflow in an ISR can lead to silent data corruption, hard faults, or system crashes that are difficult to trace because the overflow often corrupts the return address or other stack frames. This article explores the root causes of ISR stack overflow, presents practical solutions, and provides verification techniques to ensure your interrupt handlers are stack-safe.

Why ISRs Are Prone to Stack Overflow

Unlike regular functions, ISRs execute in an asynchronous context and often have limited stack space allocated. When an interrupt occurs, the processor saves its context (registers, return address) onto the stack. The ISR then uses the same stack for its local variables, function calls, and any nested interrupts. Several factors contribute to stack exhaustion:

  1. Large local variables: Arrays, structures, or buffers declared locally in the ISR consume stack space immediately.
  2. Function calls: Each nested function call pushes return address and parameters onto the stack. Recursive or deeply nested calls can quickly deplete stack space.
  3. Nested interrupts: If interrupt nesting is enabled, a higher-priority interrupt can preempt the current ISR, causing the processor to save additional context and pushing the ISR deeper into stack usage.
  4. Compiler-generated temporary storage: Complex expressions or function calls may require temporary stack space for intermediate results.
  5. Inadequate stack allocation: The stack space allocated for ISRs (either in the linker script or RTOS configuration) may be insufficient for the worst-case scenario.

The Cortex-M processor, for example, uses the main stack pointer (MSP) for exceptions and interrupts by default. If the main stack is also used by threads in an RTOS, the available space for ISR execution is reduced further. Without proper stack overflow detection, an overflow can corrupt the stack pointer, return address, or saved registers, leading to unpredictable behavior.

+------------------------------------------------------------------+
| MAIN STACK (MSP) |
+------------------------------------------------------------------+
| Task Stack Frame (if RTOS) | HIGH ADDRESS |
| ----------------------------------- | |
| Guard Region (MPU) | <-- Stack limit |
| ----------------------------------- | |
| ISR Context (auto-saved by HW) | |
| - xPSR, PC, LR, R12, R3-R0 | |
| ----------------------------------- | |
| ISR Local Variables | |
| - buffer[256], fft_buffer[1024] | |
| ----------------------------------- | |
| Nested ISR Context (if nesting) | |
| - Additional auto-saved registers | |
| ----------------------------------- | |
| Nested ISR Local Variables | LOW ADDRESS |
| | (stack grows down) |
+------------------------------------------------------------------+

Solution Approaches

Keep ISRs Short and Defer Work

The most effective strategy is to minimize the work done inside the ISR. An ISR should only perform time-critical tasks such as:

  • Reading sensor data or clearing interrupt flags.
  • Setting a global variable or triggering a DMA transfer.
  • Sending a signal to a task (in an RTOS) via a queue, semaphore, or event flag.

All processing, logging, or complex calculations should be deferred to the main loop or a dedicated task. This reduces the stack footprint of the ISR to a minimum.

Avoid Large Local Variables

Large arrays or structures should not be declared as local variables in an ISR. Instead, use:

  • Static or global variables (if reentrancy is not a concern).
  • Dynamically allocated memory (if the system allows and allocation is deterministic).
  • Pre-allocated buffers passed to the ISR via pointers.

For example, instead of:

void UART_IRQHandler(void) {
uint8_t buffer[256]; // Large local buffer
// ... process data
}

use a static buffer:

static uint8_t uart_rx_buffer[256];
void UART_IRQHandler(void) {
// ... use uart_rx_buffer
}

Note: Static variables are not reentrant; if the ISR can be nested or interrupted by another instance of the same ISR, this approach is unsafe. In such cases, consider using a double-buffering scheme or allocating buffer space per ISR instance.

Limit Function Calls

Function calls increase stack usage due to return address, parameters, and register saving. Avoid calling non-trivial functions from an ISR. If a function call is necessary, ensure it is lightweight and does not itself call other functions that consume significant stack space.

Monitor and Analyze Stack Usage

Use compiler-provided stack analysis tools to estimate the maximum stack usage of your ISR. For example:

  • GCC: -fstack-usage generates a .su file showing stack usage per function.
  • IAR: Stack analysis in the IDE.
  • Keil: Stack usage in the MLINK linker.

Additionally, enable runtime stack checking if your microcontroller or RTOS supports it. For instance:

  • Cortex-M3/M4/M7: Use the Memory Protection Unit (MPU) to create a guard region at the stack limit.
  • FreeRTOS: Enable configCHECK_FOR_STACK_OVERFLOW to detect overflows via stack watermark checking.
  • RTOS stack overflow hooks: Provide a callback when a task or ISR exceeds its stack limit.

Allocate Adequate Stack Space

Determine the worst-case stack usage for your ISR, considering:

  • Context save by hardware (varies by architecture).
  • ISR local variables and function call depth.
  • Maximum interrupt nesting depth (if enabled).
  • Any compiler-generated temporary storage.

Add a safety margin (e.g., 20-30%) to the calculated worst-case usage and configure the stack size accordingly. In an RTOS, this may mean increasing the stack size for the system stack or the specific ISR stack (if separate stacks are used).

Code Examples

Problematic ISR with Stack Overflow Risk

void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
// Clear interrupt flag
TIM2->SR &= ~TIM_SR_UIF;
// Large local array on stack
uint32_t fft_buffer[1024]; // 4KB on stack!
// Process data (calls multiple functions)
arm_rfft_fast_f32(&fft_handler, fft_buffer, fft_output, 0);
arm_cmplx_mag_f32(fft_output, magnitude, 1024);
float peak = arm_max_f32(magnitude, 1024);
// Store result (if stack hasn't overflowed yet)
latest_peak = peak;
}
}

This ISR allocates a 4KB buffer on the stack, which is likely to exceed the available stack space on most microcontrollers. Additionally, the function calls to the CMSIS-DSP library may push further onto the stack, increasing the risk of overflow.

Stack-Safe ISR with Deferred Processing

// Static buffer shared with main loop (ping-pong buffering)
static uint16_t adc_buffer[2][BUFFER_SIZE];
static uint8_t buffer_index = 0;
void ADC_IRQHandler(void) {
if (ADC->SR & ADC_SR_EOC) {
// Clear interrupt flag
ADC->SR &= ~ADC_SR_EOC;
// Store sample in buffer (minimal stack usage)
adc_buffer[buffer_index][sample_index++] = ADC->DR;
// Check if buffer is full
if (sample_index >= BUFFER_SIZE) {
sample_index = 0;
buffer_index ^= 1; // Ping-pong switch
// Signal main loop to process buffer (sets a flag or uses RTOS event)
adc_buffer_ready = true;
}
}
}
// Main loop processes buffer when flag is set
int main(void) {
// ... initialization
while (1) {
if (adc_buffer_ready) {
adc_buffer_ready = false;
process_adc_buffer(adc_buffer[buffer_index ^ 1]); // Process the other buffer
}
// ... other background tasks
}
}

This ISR only performs the time-critical task of storing ADC samples and signaling the main loop. The heavy processing (process_adc_buffer) is done in the main loop, keeping the ISR stack usage minimal.

Verification and Testing

Static Analysis

Use your compiler’s stack analysis tool to verify the maximum stack usage of each ISR. For GCC, compile with -fstack-usage and inspect the generated .su file. Ensure the reported stack usage for each ISR is well below the allocated stack size.

Runtime Stack Checking

Enable hardware or runtime stack overflow detection:

  • For Cortex-M with MPU: Configure an MPU region as a no-access guard at the top of the stack. An overflow will trigger a MemManage fault.
  • In FreeRTOS: Set configCHECK_FOR_STACK_OVERFLOW to 1 or 2 and provide a stack overflow hook function to catch overflows.

Stress Testing

To verify your ISR under worst-case conditions:

  1. Maximize interrupt rate: Trigger the interrupt source at its maximum possible rate.
  2. Enable nested interrupts: If applicable, enable interrupt nesting and simulate higher-priority interrupts occurring during ISR execution.
  3. Monitor stack pointers: Use a debugger to watch the stack pointer (SP) during ISR execution and ensure it does not approach the stack limit.
  4. Check for corruption: Place a known pattern (e.g., 0xDEADBEEF) at the stack limit and verify it remains unchanged after stress testing.

Example: Stack Usage Monitoring with GCC

Compile with -fstack-usage and examine the .su file:

ISR.o: .text.TIM2_IRQHandler 0x123 456

The number 456 is the stack usage in bytes for the ISR. Compare this to your allocated stack size (e.g., 1024 bytes) to ensure sufficient margin.

Summary

Preventing stack overflow in ISRs requires a combination of design discipline, static analysis, and runtime verification. By keeping ISRs short, avoiding large local variables, limiting function calls, and ensuring adequate stack allocation, you can eliminate this class of bugs from your embedded firmware. Always verify your ISR stack usage with the tools available in your toolchain and test under worst-case interrupt scenarios to guarantee reliable operation.

  • Fixing Cortex-M Hard Fault Handler Stack Corruption
  • Stack Usage Analysis and Optimization in Embedded C
  • Debugging Production Firmware Issues

References

  1. ARM. “Cortex-M3 Devices Generic User Guide.” Section 4.4: Exception entry and return. ARM DDI 0337E, 2015.
  2. FreeRTOS.org. “FreeRTOS Kernel Developer Guide.” Stack overflow checking. https://www.freertos.org/Stack-check-and-stack-overflow-protection.html
  3. Barr, Michael. “Stack Overflow: The Silent Killer.” Embedded Systems Programming, 2002.
  4. STM32. “STM32F4xx Reference Manual.” Section 9.3.1: Nesting of interrupts and exceptions. RM0090, 2021.
  5. GCC. “GCC Option Summary.” -fstack-usage. https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

Frequently Asked Questions

What causes stack overflow in an Interrupt Service Routine (ISR)?

Stack overflow in an ISR occurs when the ISR uses more stack space than allocated, typically due to large local variables, deep function calls, or nested interrupts consuming the available stack.

How can I detect stack overflow in my ISRs during development?

Use compiler stack usage analysis, enable runtime stack checking (if available), or allocate a guard area and monitor for corruption. Many RTOSes provide stack overflow hooks or hardware mechanisms like the Cortex-M MPU.

What are the best practices to prevent stack overflow in ISRs?

Keep ISRs short, defer non-critical work to the main loop, avoid large local variables and function calls, and ensure adequate stack size is allocated for the worst-case interrupt nesting scenario.

Tags

isrstack-overflowembedded-cstm32cortex-m

Share


Previous Article
Embedded Linux: Fixing Slow Boot Time
Jithin Tom

Jithin Tom

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

Related Posts

Fixing Cortex-M Hard Fault Handler Stack Corruption
Fixing Cortex-M Hard Fault Handler Stack Corruption
August 28, 2026
5 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media