Using Weak Symbols to Override Functions in C
In the C programming language, weak symbols provide a mechanism to
define default function implementations that can be replaced at link
time without triggering duplicate symbol errors. By annotating a
function with compiler-specific attributes like GCC’s
__attribute__((weak)), developers can designate a fallback
routine. During the linking phase, the linker prioritizes normal,
"strong" symbols over weak ones, allowing user code, hardware
abstraction layers, or custom modules to override library-provided
defaults seamlessly.
Understanding Strong vs. Weak Symbols
In compiled C code, functions and global variables are converted into
symbols within object files (.o). By default, all defined
functions and initialized global variables are treated as strong
symbols.
The linker applies specific rules when resolving symbols across compilation units:
- Multiple Strong Symbols: If two translation units define the same strong symbol with identical names, the linker halts and throws a "multiple definition" error.
- Strong and Weak Symbols: If a symbol exists as both strong and weak, the linker silently discards the weak symbol and binds all calls to the strong implementation.
- Multiple Weak Symbols: If only weak symbols exist, the linker arbitrarily selects one of the weak definitions.
- Undefined Weak Symbols: If a weak symbol is
declared but has no definition, the linker resolves it to address
0(null) rather than reporting an undefined reference.
Declaring and Defining Weak Symbols
In GCC and Clang, the weak attribute is specified using compiler extensions:
// Declaring a weak function prototype
void hardware_init(void) __attribute__((weak));
// Defining a weak function with default behavior
__attribute__((weak)) void hardware_init(void) {
// Default fallback initialization
}How Overriding Works at Link Time
Consider a scenario involving a library and a user application.
1. The Default Implementation (Library Side)
The library provides a base behavior for an event hook:
/* logger.c */
#include <stdio.h>
__attribute__((weak)) void custom_logger(const char *msg) {
// Default implementation: output to standard error
fprintf(stderr, "[DEFAULT LOG]: %s\n", msg);
}
void process_data(void) {
custom_logger("Processing complete.");
}If the program is compiled using only logger.c, calls to
custom_logger invoke this default handler.
2. The Overriding Implementation (Application Side)
The application developer wants to redirect these log messages to a dedicated logging framework or file:
/* main.c */
#include <stdio.h>
void process_data(void);
// Strong definition: overrides the weak default in logger.c
void custom_logger(const char *msg) {
printf("[CUSTOM LOG]: %s\n", msg);
}
int main(void) {
process_data();
return 0;
}3. Linker Resolution
When compiling both files together:
gcc -o app main.c logger.cThe linker parses the symbol table of logger.o, marking
custom_logger as weak. When inspecting main.o,
it encounters custom_logger as a strong symbol. Following
ELF linker rules, the strong definition in main.o
supersedes the weak definition in logger.o. At runtime,
process_data() calls the custom implementation,
outputting:
[CUSTOM LOG]: Processing complete.
Common Applications
- Embedded Systems and Interrupt Service Routines
(ISRs): Microcontroller startup files frequently define weak
default interrupt handlers that simply loop indefinitely. Developers can
then define a strong handler in their application code for specific
interrupts (e.g.,
USART1_IRQHandler) without modifying the vendor startup files. - System Library Hooks: The GNU C Library
(
glibc) uses weak symbols for threading and memory functions (such asmallocorpthreadstubs) to allow user-defined wrappers or alternative memory allocators to replace standard implementations. - Optional Features: Functions can be declared weak
and checked against
NULLprior to execution. If a supporting library containing the strong symbol is linked, the function runs; otherwise, the program bypasses it.