11 Apr 2025 - tsp
Last update 11 Apr 2025
11 mins
When developing embedded libraries for microcontrollers, you often want to provide default behavior that users can easily override in their application code. This is where weak linkage comes into play—a GCC extension that allows you to define functions or variables in a way that they can be replaced at link time.
This article is mainly a quick note to remember how this works and shows how to use weak linkage for:
This short blog article walks through examples, shows how it works under the hood, and wraps up with a Makefile-friendly AVR demo project.
In GCC (used by avr-gcc
), you can mark a symbol as “weak” so that it will only be used if no strong symbol of the same name is found. By default, all symbols are treated as “strong” unless explicitly marked otherwise. This means the application can override the default without any special tricks.
__attribute__((weak)) void my_hook_function(void) {
// Default implementation
}
If the application defines my_hook_function
without __attribute__((weak))
, the linker will use the application’s version instead. This substitution happens during the linking phase, which occurs after the compiler has built all individual object files. That means you can compile and cache your library object files with weak symbols and then override selected parts later in application code without needing to rebuild the library.
However, it’s important to understand how the linker treats unused weak symbols. If the weakly defined function or variable is not referenced by any other code in the binary, it may be omitted entirely by the linker—particularly when using --gc-sections
in the linker and -ffunction-sections
and -fdata-sections
during compilation.
These flags work together to enable fine-grained section-level control:
-ffunction-sections
and -fdata-sections
instruct the compiler to place each function and data object into its own uniquely named section in the .o
file (e.g., .text.myfunc
, .data.myvar
) instead of placing all functions in .text
and all data in .data
.--gc-sections
then allows the linker to discard entire sections that are not used. Crucially, this means that if nothing in a section is referenced—no functions, no variables, no labels—the entire section can be safely removed from the final binary.This behavior is what makes weak linkage especially powerful in embedded development: you can provide many default implementations without inflating binary size, as unused defaults will not be linked unless actually referenced.
In this context, “actually referenced” means that the weak symbol is used in a way that causes its section to be retained during linking—either because it is directly called or accessed, or because it is not overridden by a strong symbol in the application and is needed as the default implementation.
This means that even if a weak symbol is defined in a library, it won’t end up in the binary unless something references it—either directly or indirectly. This is ideal for libraries that want to stay lean and only include what’s used.
To summarize:
Therefore, a well-structured library should:
This pattern enables powerful and modular application behavior while minimizing binary size and avoiding code bloat.
Let’s consider a typical example in embedded development: your library implements UART communication, and it includes a handler function that gets called whenever a character is received via an interrupt. You want to provide a default implementation (e.g., echoing the character), but let the user of the library override it with custom logic.
A example situation might look like this:
ISR(USART_RX_vect) {
char c = UDR0; // Read received character
uart_on_rx(c); // Call hook function to handle it
}
In this example, the interrupt service routine (ISR) calls a handler function uart_on_rx
, which is defined as a weak symbol in the library. If the user doesn’t provide their own implementation, the default will be used.
Example Problem: You’re writing a logging library for a small AVR-based sensor node. The node uses UART to send messages to a serial console. By default, every character received should be echoed back. However, in one application, the user wants to count how many characters were received instead. You don’t want them to touch the library source code—just override a hook.
// libuart.c
__attribute__((weak)) void uart_on_rx(char c) {
// Default: echo
printf("LIB: received char: %c\n", c);
}
// main.c
void uart_on_rx(char c) {
printf("APP: got %c from UART!\n", c);
}
The uart_on_rx
function in the application replaces the weak one from the library.
Sometimes you want to provide configurable parameters in your library—such as buffer sizes, feature enable flags or other compile-time constants—that can be changed by the application using your library. Weak global variables are perfect for this use case.
Example Situation: Imagine your library defines the size of a circular receive buffer for UART input. Most applications are fine with 64 bytes, but some need 128 or more to avoid overflows when data comes in bursts. Rather than forcing users to modify the library or add complex configuration headers, you simply define the buffer size as a weak global variable. This allows the application to override it by defining the same symbol.
This approach is simple and does not require function hooks or pointers. It also works nicely when paired with initialization logic that allocates buffers or configures peripherals based on the variable’s value.
// libuart.c
__attribute__((weak)) uint16_t uart_rx_buffer_size = 64;
uint16_t uart_rx_buffer_size = 128;
The application’s uart_rx_buffer_size
definition will take precedence over the weak default, allowing the user to tune the system without modifying the library.
Sometimes you want to let the application decide the behavior of a handler at runtime—not by overriding a symbol at link time, but by assigning function pointers during initialization, swapping them at runtime or even exchange them depending on the current state of the system. In these cases, traditional function pointers are the better solution.
Example Situation: You’re building a modular library where different UART channels or modes might use different handlers for received characters. Instead of forcing the user to override a fixed symbol, you define a function pointer in your library and allow the user to assign any function they like to it.
This approach is more flexible and dynamic than weak linkage and doesn’t rely on linker behavior. It also avoids symbol conflicts or the need to match names exactly.
void default_hook(char c) {
// Default behavior
printf("Default received: %c
", c);
}
void (*uart_rx_hook)(char) = default_hook;
void uart_received_char(char c) {
if (uart_rx_hook) uart_rx_hook(c);
}
void my_uart_rx_handler(char c) {
// Custom logic
printf("Handled by app: %c
", c);
}
int main(void) {
uart_rx_hook = my_uart_rx_handler;
uart_received_char('B');
return 0;
}
To bring all the concepts together—weak functions, weak variables, and modularity—we’ll walk through a complete build system example using a Makefile. This project builds a static library from the libuart.c
file with appropriate compiler and linker flags that support section-level optimization. The final application links against this library, overriding weak symbols as needed.
This setup mimics what you would do in a real AVR embedded project: compile a reusable library once, cache the object or static archive, and allow the application layer to customize behavior through overrides or assignment—without touching the library source code.
project/
├── Makefile
├── lib/
│ ├── libuart.c
│ └── libuart.h
└── main.c
libuart.h
#ifndef LIBUART_H
#define LIBUART_H
#include <stdint.h>
extern uint32_t uart_baudrate;
void uart_on_rx(char c);
void uart_init(void);
void uart_simulate_receive(char c);
#endif
libuart.c
#include "libuart.h"
#include <stdio.h>
__attribute__((weak)) uint32_t uart_baudrate = 9600;
__attribute__((weak)) void uart_on_rx(char c) {
printf("LIB: received char: %c\n", c);
}
void uart_init(void) {
printf("LIB: init UART at %d baud\n", uart_baudrate);
}
void uart_simulate_receive(char c) {
uart_on_rx(c);
}
main.c
#include "lib/libuart.h"
#include <stdio.h>
uint32_t uart_baudrate = 19200;
void uart_on_rx(char c) {
printf("APP: got %lu from UART!\n", c);
}
int main(void) {
uart_init();
uart_simulate_receive('A');
}
Makefile
MCU = atmega328p
CC = avr-gcc
AR = avr-ar
OBJCOPY = avr-objcopy
CFLAGS = -mmcu=$(MCU) -Os -Wall -ffunction-sections -fdata-sections
LDFLAGS = -Wl,--gc-sections
LIBDIR = lib
LIBSRC = $(LIBDIR)/libuart.c
LIBOBJ = $(LIBDIR)/libuart.o
LIB = $(LIBDIR)/libuart.a
TARGET = main
all: $(TARGET).hex
$(LIB): $(LIBOBJ)
$(AR) rcs $@ $^
$(LIBOBJ): $(LIBSRC)
$(CC) $(CFLAGS) -c $< -o $@
$(TARGET).elf: main.c $(LIB)
$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@
%.hex: %.elf
$(OBJCOPY) -O ihex $< $@
clean:
rm -f $(LIBDIR)/*.o $(LIBDIR)/*.a *.elf *.hex
This akefile compiles the libuart.c
source into a static library (libuart.a
) with -ffunction-sections
and -fdata-sections
enabled. The application then links against the library using --gc-sections
to ensure unused sections (e.g., unused weak symbols) are discarded.
Symbol Type | Use Case | Overridable? | Notes |
---|---|---|---|
Function | User callbacks/hooks | ✅ Yes | Most common use |
Global Variable | Configuration options | ✅ Yes | Use in lib, override in app |
Function Pointer | Late-bound custom behavior | ✅ Yes | More dynamic, use with care |
Using weak linkage is a powerful idiom for embedded C, especially in the AVR world. It lets you build clean, reusable libraries that users can customize without forking or patching. From error handlers to baud rates to interrupt hooks, this technique gives your libraries flexibility while keeping the design clean and robust.
This article is tagged:
Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)
This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/