In embedded firmware development, configuration is often controlled through compiler macros passed from the build system. This keeps the codebase flexible and allows easy customization for different products or hardware variants.
In this blog post, we will explore how a macro can introduce a subtle bug, often referred to as a C macro override bug that can sometimes have severe and unexpected consequences.
I encountered this issue while working on a display driver. To better understand it, let’s walk through a simple example using UART, as it is widely used and commonly understood in embedded systems.
For example, you might configure a UART buffer size like this:
gcc -DUART_RX_BUFFER_SIZE=128 main.c
or through a build system:
envLocal.Append(CCFLAGS='-DUART_RX_BUFFER_SIZE=128')
This configuration ensures that your firmware uses a 128-byte receive buffer, which is essential for handling incoming data streams efficiently and avoiding data loss.
At first glance, everything appears correct.
But then comes the subtle bug.
Somewhere deep in your codebase, perhaps inside a driver header or a legacy file, you find the following line:
#define UART_RX_BUFFER_SIZE 64
This line might have been introduced during testing, copied from an older project, or simply left behind by mistake. However, its impact is significant. Let’s understand the impact of an innocent #define.
The Hidden Problem:
This seemingly harmless #define silently overrides the configuration provided by the build system.
Even though you explicitly passed:
-DUART_RX_BUFFER_SIZE=128
Now you expect here UART_RX_BUFFER_SIZE = 128
But your firmware ends up using:
UART_RX_BUFFER_SIZE = 64
What actually happened?
In C, the preprocessor processes code sequentially, from top to bottom, before the actual compilation step. If a macro is defined multiple times, the most recent definition replaces the previous one for all code that follows.
As a result, a carefully configured build-time parameter can be silently overridden later in the source, leading to behavior that’s difficult to trace.
How it happens:
The C preprocessor follows a simple rule:
So, the sequence looks like this:
#define UART_RX_BUFFER_SIZE 128 // Defined by compiler or build system #define UART_RX_BUFFER_SIZE 64 // Redefined in source file buffer[UART_RX_BUFFER_SIZE]; // Uses 64
No runtime error. No crash. Just incorrect behavior.
Why this Bug Is Hard to Catch:
This issue is particularly dangerous because it does not behave like a typical bug.
- The code compiles successfully: No errors. No warnings (in most cases). The build passes cleanly.
- No immediate failure: The firmware boots and runs exactly as expected during initial testing.
- Behavior depends on runtime conditions: The issue only appears under specific scenarios, often tied to data rate or timing.
- Fails under heavy load: Problems like data loss or buffer overflow show up only when the system is stressed.
How to Prevent from this Bug:
Here are some important practices that help you avoid this kind of issue:
1. Never Redefine Build Macros in Source
If a macro is meant to be configured via the build system (e.g., -D flags), don’t redefine it inside source files.
#define UART_RX_BUFFER_SIZE 64 // Avoid this
This silently overrides external configuration and defeats the purpose of build-time control.
2. Use the Safe Default Pattern
Provide a default only if the macro hasn’t already been defined:
#ifndef UART_RX_BUFFER_SIZE #define UART_RX_BUFFER_SIZE 64
3. Add Compile-Time Validation
Catch invalid configurations early:
#if UART_RX_BUFFER_SIZE < 128 #error "UART buffer too small!" #endif
4. Enable Compiler Warnings
Turn on warnings to catch macro-related problems:
-Wall -Wextra
For stricter enforcement:
-Werror
Recommended Post:
To strengthen your understanding of C programming concepts, explore the following articles: