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:

  1. Multiple Strong Symbols: If two translation units define the same strong symbol with identical names, the linker halts and throws a "multiple definition" error.
  2. 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.
  3. Multiple Weak Symbols: If only weak symbols exist, the linker arbitrarily selects one of the weak definitions.
  4. 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
}

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.c

The 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 as malloc or pthread stubs) to allow user-defined wrappers or alternative memory allocators to replace standard implementations.
  • Optional Features: Functions can be declared weak and checked against NULL prior to execution. If a supporting library containing the strong symbol is linked, the function runs; otherwise, the program bypasses it.