
Weak symbols and function aliasing are two GCC linker features that fundamentally change how embedded firmware handles default implementations, interrupt vectors, and hardware abstraction layers. Used correctly, they eliminate boilerplate, enable clean HAL overrides, and make vector tables maintainable. Used carelessly, they create silent bugs where the wrong function executes.
This article covers the mechanics, use cases, and pitfalls of both features with concrete ARM Cortex-M examples.
A weak symbol tells the linker: “Use this definition unless a strong one exists elsewhere.” The syntax:
__attribute__((weak)) void Default_Handler(void) {while (1) { __asm("bkpt #0"); }}
If the application defines void Default_Handler(void) without the weak attribute, the linker binds all references to the strong definition. The weak version vanishes from the final binary — no code size penalty, no runtime indirection.
The canonical use case is the Cortex-M vector table. The startup file (typically startup_stm32xxxx.s or a C equivalent) declares every IRQ handler as weak:
/* startup_stm32f4xx.c */__attribute__((weak)) void NMI_Handler(void) { Default_Handler(); }__attribute__((weak)) void HardFault_Handler(void) { Default_Handler(); }__attribute__((weak)) void MemManage_Handler(void) { Default_Handler(); }__attribute__((weak)) void BusFault_Handler(void) { Default_Handler(); }__attribute__((weak)) void UsageFault_Handler(void) { Default_Handler(); }__attribute__((weak)) void SVC_Handler(void) { Default_Handler(); }__attribute__((weak)) void DebugMon_Handler(void) { Default_Handler(); }__attribute__((weak)) void PendSV_Handler(void) { Default_Handler(); }__attribute__((weak)) void SysTick_Handler(void) { Default_Handler(); }/* External interrupts */__attribute__((weak)) void EXTI0_IRQHandler(void) { Default_Handler(); }__attribute__((weak)) void EXTI1_IRQHandler(void) { Default_Handler(); }/* ... 80+ more ... */
The application overrides only what it needs:
/* main.c or stm32f4xx_it.c */void EXTI0_IRQHandler(void) {if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);button_callback();}}
No vector table editing. No weak alias boilerplate. The linker resolves EXTI0_IRQHandler to the application version; all unhandled IRQs fall through to Default_Handler.
Weak variables work the same way. A library provides a default configuration struct; the application overrides it:
/* driver_config.c — library default */__attribute__((weak)) const DriverConfig_t default_config = {.baudrate = 115200,.parity = PARITY_NONE,.flow = FLOW_NONE};
/* application_config.c — overrides */const DriverConfig_t default_config = {.baudrate = 921600,.parity = PARITY_EVEN,.flow = FLOW_RTS_CTS};
The linker picks the application version. Zero runtime cost, no function pointer indirection.
A common mistake is defining a weak function in a header:
/* bad.h */__attribute__((weak)) void log_putc(char c) { /* default UART */ }
Included in main.c, uart.c, spi.c → three weak definitions. The linker picks one arbitrarily (first seen). If uart.c also provides a strong definition, which weak copy wins is undefined. The fix: declare in header, define once in a .c file.
/* log.h */extern __attribute__((weak)) void log_putc(char c);/* log_default.c */__attribute__((weak)) void log_putc(char c) { uart_write(c); }/* log_custom.c — application override */void log_putc(char c) { usb_cdc_write(c); }
The alias attribute creates a second name for the same function:
void Default_Handler(void) __attribute__((alias("HardFault_Handler")));
Both Default_Handler and HardFault_Handler resolve to identical machine code. No thunk, no jump — same address.
STM32 HAL startup code uses this extensively:
/* startup_stm32f4xx.c */void Default_Handler(void) { while (1) { __asm("bkpt #0"); } }void NMI_Handler(void) __attribute__((alias("Default_Handler")));void HardFault_Handler(void) __attribute__((alias("Default_Handler")));void MemManage_Handler(void) __attribute__((alias("Default_Handler")));void BusFault_Handler(void) __attribute__((alias("Default_Handler")));void UsageFault_Handler(void) __attribute__((alias("Default_Handler")));/* Peripheral IRQs */void WWDG_IRQHandler(void) __attribute__((alias("Default_Handler")));void PVD_IRQHandler(void) __attribute__((alias("Default_Handler")));/* ... 70+ more aliases ... */
One Default_Handler definition. Eighty aliases. The vector table entries all point to the same instruction. Code size: one function + 80 symbol table entries (negligible).
When a HAL function is renamed but old code must compile:
/* New API */HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart,uint8_t *pData, uint16_t Size);/* Old name — alias to new implementation */HAL_StatusTypeDef HAL_UART_Transmit_DMA_IT(UART_HandleTypeDef *huart,uint8_t *pData, uint16_t Size)__attribute__((alias("HAL_UART_Transmit_DMA")));
Both names work. No wrapper function overhead.
| Scenario | Mechanism | Reason |
|---|---|---|
| Default IRQ handler | Weak | App overrides selectively |
| Vector table default | Alias | 80+ IRQs → one handler, zero overhead |
| HAL default config | Weak data | App overrides struct once |
| Deprecated API name | Alias | Same code, two entry points |
| Optional callback | Weak function | App provides if needed |
| Multiple ISR names → one handler | Alias | DMA stream aliases, EXTI line groups |
Rule of thumb: Weak = “override me if you want.” Alias = “this is another name for the same thing.”
A clean HAL design uses weak symbols for overridable defaults and aliases for vector table density.
/* hal_gpio.h */typedef void (*gpio_irq_cb_t)(uint16_t pin);__attribute__((weak)) void HAL_GPIO_EXTI_Callback(uint16_t pin) {(void)pin; /* Default: no-op */}/* hal_gpio.c */void EXTI0_IRQHandler(void) {if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);HAL_GPIO_EXTI_Callback(0);}}/* Vector table uses aliases for density */void EXTI1_IRQHandler(void) __attribute__((alias("EXTI0_IRQHandler")));void EXTI2_IRQHandler(void) __attribute__((alias("EXTI0_IRQHandler")));/* ... */void EXTI15_10_IRQHandler(void) {uint16_t pins = __HAL_GPIO_EXTI_GET_IT(0xFC00);__HAL_GPIO_EXTI_CLEAR_IT(0xFC00);for (int i = 10; i <= 15; i++) {if (pins & (1 << i)) HAL_GPIO_EXTI_Callback(i);}}
The application implements HAL_GPIO_EXTI_Callback once. All 16 EXTI lines route through it. The vector table has 16 entries pointing to 2 handler functions (one for 0-9 via alias, one for 10-15 with loop).
+------------------------------------------------------------------+| LINKER SYMBOL RESOLUTION |+------------------------------------------------------------------+| || STARTUP CODE (.c/.s) APPLICATION CODE (.c) || ┌─────────────────────┐ ┌─────────────────────┐ || │ weak void │ │ void EXTI0_IRQHandler() │ ← STRONG| │ EXTI0_IRQHandler()│ ──X──│ { ...custom code... } │ wins| │ { Default_Handler }│ └─────────────────────┘ || └─────────────────────┘ || │ || │ weak || ▼ || ┌─────────────────────┐ || │ void Default_Handler│ || │ { while(1); } │ || └─────────────────────┘ || ▲ || │ alias || ┌─────────────────────┐ || │ EXTI1_IRQHandler │ ── alias → Default_Handler || │ EXTI2_IRQHandler │ ── alias → Default_Handler || │ ... │ || │ EXTI15_IRQHandler │ ── alias → Default_Handler || └─────────────────────┘ || || RESULTING VECTOR TABLE: || ┌──────────────┬────────────────────────────────────────────┐ || │ Vector # │ Address │ || ├──────────────┼────────────────────────────────────────────┤ || │ EXTI0 │ 0x0800_0240 → app EXTI0_IRQHandler() │ || │ EXTI1 │ 0x0800_0244 → Default_Handler (alias) │ || │ EXTI2 │ 0x0800_0248 → Default_Handler (alias) │ || │ ... │ ... │ || │ EXTI15 │ 0x0800_027C → Default_Handler (alias) │ || └──────────────┴────────────────────────────────────────────┘ || |+------------------------------------------------------------------+
/* lib.h — included by lib.c and app.c */__attribute__((weak)) int compute(int x) { return x * 2; }/* lib.c */int compute(int x) { return x * 2; } // strong? no, still weak!/* app.c */int compute(int x) { return x * 3; } // strong
If lib.c doesn’t redeclare as strong, the library’s own calls to compute() may bind to the app’s version — or the weak one. Declare weak in header, define strong in exactly one .c file.
// log.hextern "C" __attribute__((weak)) void log_putc(char c); // OK: declaration// log.cppextern "C" __attribute__((weak)) void log_putc(char c) { uart_write(c); } // OK: one def// main.cpp#include "log.h"extern "C" void log_putc(char c) { usb_write(c); } // OK: strong def
But if log.h contained the definition (not just declaration), every TU including it gets a weak definition → ODR violation in C++. Keep definitions out of headers.
void foo(void) __attribute__((alias("bar"))); // bar not defined anywhere
Linker error: undefined reference to 'bar'. The aliased target must exist.
If a weak symbol is defined in a static library (.a), and the application provides a strong definition, the linker may still pull the weak version from the archive if the archive member is needed for another symbol. Fix: put weak defaults in object files linked directly, not in static libraries, or use --whole-archive.
# After linking, inspect the symbol tablearm-none-eabi-nm -n firmware.elf | grep -E '(EXTI0|Default_Handler)'# Output example:# 08000240 T EXTI0_IRQHandler ← strong app version# 08000244 T Default_Handler ← weak/alias target# 08000244 T EXTI1_IRQHandler ← alias to Default_Handler
Or use objdump -t to see binding type (w = weak, W = weak undefined, u = unique global).
extern weak in headers, define once in a .c file.nm or objdump to confirm which definition won.These mechanisms cost zero runtime cycles. They shift complexity to link time where the toolchain resolves it once, correctly, for every build.
weak, alias — https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.htmlQuick Links
Legal Stuff




