
Memory bugs are among the most insidious issues in embedded firmware. A single out-of-bounds write can corrupt control flow, trigger silent data corruption, or cause intermittent crashes that are nearly impossible to reproduce. Traditional debugging techniques like logging or breakpoint stepping often fail to catch these bugs because they happen sporadically and may not manifest until specific timing conditions align.
AddressSanitizer (ASan) changes the game by providing compile-time instrumentation that detects memory errors as they occur. Originally developed for user-space applications, ASan has proven surprisingly effective in embedded environments when used with host-based testing, emulation, or even on-target with sufficient resources. This article explains how to integrate ASan into your embedded C workflow, understand its trade-offs, and leverage it to catch bugs early—before they become costly field failures.
Embedded firmware often runs without memory protection units (MPUs) or operates in bare-metal environments where a single stray write can overwrite critical data structures, interrupt vectors, or stack frames. Unlike desktop applications that benefit from virtual memory and process isolation, embedded systems frequently share a single address space between code, data, and peripherals. This makes memory bugs particularly devastating:
Consider a typical scenario: a firmware engineer implements a circular buffer for UART reception. Under high traffic, the buffer occasionally overflows, writing past its end and corrupting a nearby task control block. The system might crash hours later with a hard fault, but the root cause remains hidden in the noise.
Traditional approaches like manual code reviews, static analysis, or dynamic testing with limited instrumentation often miss these issues. Static analyzers can produce false positives and miss complex heap interactions, while debugging with breakpoints alters timing and may prevent the bug from occurring.
AddressSanitizer detects memory errors by instrumenting loads and stores in the compiled code and adding shadow memory that tracks the state of each application byte. At compile time, the compiler (GCC or Clang) inserts calls to ASan runtime functions before every memory access. These checks validate whether the accessed address is valid based on shadow memory state.
When a violation occurs—such as writing to a use-after-free region or overflowing a buffer—ASan prints a detailed diagnostic report that includes:
The instrumentation adds red zones (poisoned memory) around allocations and uses a shadow mapping scheme where each shadow byte represents the state of 8 application bytes. This design keeps overhead reasonable while catching a wide range of bugs.
To use ASan with embedded C toolchains, you need a compiler that supports the -fsanitize=address flag (GCC 4.8+ or Clang 3.1+). The steps are:
-fsanitize=address: This enables instrumentation and links against the ASan runtime.-lasan (provided by the compiler suite).Assume you’re using the GNU Arm Embedded Toolchain. A simple Makefile snippet:
CFLAGS += -O1 -g -fsanitize=addressLDFLAGS += -lasan# Ensure we link with libc that provides malloc/freeLIBS += -lc -lmall: firmware.elffirmware.elf: main.o$(CC) $(LDFLAGS) $^ -o $@ $(LIBS)clean:rm -f *.o *.elf
Note: Optimization level -O1 is recommended; higher levels may reduce ASan’s effectiveness due to aggressive inlining or dead code elimination that removes instrumentation.
For many embedded projects, the easiest approach is to abstract hardware dependencies and run the core logic on a POSIX host (Linux/macOS) using mocked hardware layers. This lets you leverage ASan’s full power without worrying about target resource constraints.
Example structure:
src/core.c # Hardware-independent logichal_stm32.c # STM32-specific hardware abstractionhal_mock.c # Mock implementation for host teststests/test_uart.c # Unit tests using mocked HAL
Compile the test binary with ASan and run it under Valgrind or directly—ASan will catch bugs in the core logic.
Running ASan directly on a microcontroller is possible but comes with constraints:
If resources are tight, consider using ASan only for unit tests or integration tests on a more capable host or emulator (e.g., QEMU modeling an STM32).
When ASan detects an error, it aborts the program and prints a diagnostic to stderr. Understanding this output is key to fixing bugs quickly.
===================================================================12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x60200000fed0 at pc 0x0000004005ef bp 0x7ffd8a3c7a50 sp 0x7ffd8a3c7a48READ of size 4 at 0x60200000fed0 thread T0#0 0x4005ee in process_packet src/core.c:42#1 0x4007a1 in main src/main.c:28#2 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)#3 0x400389 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x20890)0x60200000fed0 is located 0 bytes inside of 4-byte region [0x60200000fed0,0x60200000fed4)freed by thread T0 here:#0 0x400a3c in free (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x9c3cf)#1 0x4008f1 in release_buffer src/core.c:58#2 0x4005d5 in process_packet src/core.c:35#3 0x4007a1 in main src/main.c:28#4 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)previously allocated by thread T0 here:#0 0x4009b2 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x12c92)#1 0x400881 in allocate_buffer src/core.c:50#2 0x4005d5 in process_packet src/core.c:35#3 0x4007a1 in main src/main.c:28#4 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)SUMMARY: AddressSanitizer: heap-use-after-free src/core.c:42 in process_packet=================================================================
Breaking this down:
heap-use-after-free — the program read memory after it was freed.0x60200000fed0 — the exact byte accessed.process_packet at core.c:42.release_buffer at core.c:58).allocate_buffer at core.c:50).===================================================================12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd8a3c7a60 at pc 0x000000400621 bp 0x7ffd8a3c7a50 sp 0x7ffd8a3c7a48WRITE of size 8 at 0x7ffd8a3c7a60 thread T0#0 0x400620 in copy_data src/util.c:19#1 0x4007a1 in main src/main.c:28#2 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)#3 0x400389 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x20890)Address 0x7ffd8a3c7a60 is located in stack of thread T0 at offset 32 in frame#0 0x400620 in copy_data src/util.c:19This frame has 1 object(s):[32, 40) 'local_buf' <== Memory access at offset 32 overflows the variableHINT: this may be due to an incorrect memset parametersSUMMARY: AddressSanitizer: stack-buffer-overflow src/util.c:19 in copy_data=================================================================
Here, ASan identifies a write past the end of a stack array local_buf (expected size 8 bytes, accessed at offset 32). The hint points to a possible memset length error.
Let’s walk through a real-world scenario where ASan catches a subtle bug in STM32 HAL-based firmware.
A firmware module implements a double-buffered UART receiver to avoid overruns. Under heavy load, the system occasionally crashes with a hard fault. Manual debugging shows the fault occurs in an interrupt handler, but the stack looks corrupted.
#define UART_BUF_SIZE 256static uint8_t rx_buffer[UART_BUF_SIZE];static uint16_t rx_head = 0;static uint16_t rx_tail = 0;void UART_IRQHandler(void){if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF);rx_buffer[rx_head] = data; // Potential overflow if not checkedrx_head = (rx_head + 1) % UART_BUF_SIZE;}}
The bug: missing bounds check before writing to rx_buffer. If interrupts fire faster than the main loop consumes data, rx_head can wrap and overwrite unconsumed data, or worse, write past the buffer end if modulo operation is missed.
We compile the firmware for host execution with a mocked HAL:
// hal_mock.c#include "hal.h"void UART_IRQHandler(void){// Simulate interrupt by calling the same logicuint8_t data = mock_uart_getchar();rx_buffer[rx_head] = data; // ASan will catch overflow hererx_head = (rx_head + 1) % UART_BUF_SIZE;}
Running the test suite with ASan enabled produces:
===================================================================12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd8a3c7a60 at pc 0x000000400621 bp 0x7ffd8a3c7a50 sp 0x7ffd8a3c7a48WRITE of size 1 at 0x7ffd8a3c7a60 thread T0#0 0x400620 in UART_IRQHandler hal_mock.c:12#1 0x4007a1 in test_uart_rx_overload tests/test_uart.c:45#2 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)#3 0x400389 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x20890)Address 0x7ffd8a3c7a60 is located in stack of thread T0 at offset 32 in frame#0 0x400620 in UART_IRQHandler hal_mock.c:12This frame has 1 object(s):[0, 256) 'rx_buffer' <== Memory access at offset 32 is outside [0,256)SUMMARY: AddressSanitizer: stack-buffer-overflow hal_mock.c:12 in UART_IRQHandler
ASan pinpoints the exact line and shows that the write to rx_buffer is out of bounds. The fix is to add a check for buffer full condition:
void UART_IRQHandler(void){if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF);uint16_t next_head = (rx_head + 1) % UART_BUF_SIZE;if (next_head != rx_tail) { // Buffer not fullrx_buffer[rx_head] = data;rx_head = next_head;} else {// Handle overflow: set flag or drop datauart_overflow = true;}}}
After applying the fix, ASan reports no errors, confirming the buffer bounds are respected.
While ASan is powerful, it’s not a panacea for embedded systems. Understanding its limitations helps you apply it effectively.
ASan’s instrumentation increases code size significantly—often 2x to 3x—because each memory access is wrapped with checks. The shadow memory adds another 1/8th of the application’s memory usage (since 1 shadow byte tracks 8 application bytes), plus red zones around allocations. For a typical embedded binary of 64KB flash, expect 150KB-200KB with ASan.
RAM usage also grows due to:
On a Cortex-M0 with 8KB RAM, this overhead may leave little room for actual application data.
Each load/store instruction gains extra instructions for shadow memory lookup and red zone checks. Expect 2x-3x slowdown, which can affect real-time performance. This makes ASan unsuitable for timing-sensitive code paths unless you can isolate and test them separately.
ASan intercepts all memory accesses, including those to peripherals. Accessing a memory-mapped I/O register may trigger a false positive if the address falls within a red zone or shadow region. Workarounds include:
-fsanitize-address-use-after-scope doesn’t help; need to avoid instrumenting those files).Given the overhead, ASan is most practical when used with host-based unit tests, integration tests on Linux/macOS, or full-system emulation (QEMU, Renode). For pure bare-metal targets without an OS, porting the ASan runtime is non-trivial and may not be worth the effort unless you have significant resources.
To maximize benefits while minimizing drawbacks, follow these guidelines:
-O1 or -O2; avoid -O3 or -Ofast as they may interfere with instrumentation.ASan isn’t the only tool for memory safety. Consider these alternatives depending on your constraints:
-fcheck-array-bounds (Fortran) or -fbounds-check (some C compilers) offer lighter-weight checks but less comprehensive coverage.AddressSanitizer brings powerful memory error detection to embedded C development, helping engineers catch elusive bugs that traditional methods miss. By instrumenting memory accesses and tracking shadow state, ASan provides detailed reports that pinpoint the exact location and cause of violations—whether it’s a heap use-after-free, stack overflow, or global buffer overflow.
While the increased code size, RAM usage, and performance overhead make ASan less suitable for resource-constrained bare-metal targets, it shines when applied to host-based unit tests, integration tests on Linux/macOS, or emulation environments. The key is to isolate testable components, use moderate optimization, and treat every ASan report as a critical issue requiring immediate action.
For embedded teams striving for zero-defect firmware, integrating ASan into the continuous integration pipeline—especially for host-executable test suites—can significantly reduce escape rates of memory-related bugs. Combine it with good software architecture, rigorous code reviews, and other sanitizers to build a robust safety net that catches issues early in the development cycle.
The next time you encounter an intermittent crash or mysterious corruption in your firmware, consider compiling with -fsanitize=address. The few minutes of setup could save hours of debugging and prevent costly field failures.
The following diagram illustrates how AddressSanitizer uses shadow memory to track the state of application memory. Each shadow byte represents 8 application bytes, indicating whether they are valid, poisoned (red zone), freed, etc.
+---------------------+ +---------------------+| Application Memory | | Shadow Memory || (Actual RAM) | | (1 byte per 8 app B)|+---------------------+ +---------------------+| [Valid Data] | | 00 00 00 00 00 00 || [Red Zone]~~~~~~~~~| | 00 00 00 00 00 f1 || [Valid Data] | | 00 00 00 00 00 00 || [Freed Heap]XXXXXXXX| | 00 00 00 00 00 fd || [Stack Buffer] | | 00 00 00 00 00 00 || [Red Zone]~~~~~~~~~| | 00 00 00 00 00 f1 |+---------------------+ +---------------------+^ ^| || 8 app bytes -> 1 shadow byte|Legend:00 = valid addressf1 = stack red zonef2 = global red zonef3 = stack left red zonef4 = stack mid red zonef5 = stack right red zonef8 = invalid addressfd = freed heapfz = stack use after return
Quick Links
Legal Stuff





