
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. A basic C startup file declares every IRQ handler as a weak function:
/* basic_startup.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 — each .o file gets its own weak definition. The linker picks one arbitrarily. If uart.c also provides a strong definition, the strong one wins, but now you have an implicit dependency on link order. 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 an existing function:
void HardFault_Handler(void) { while (1); } /* the real definition */void HardFault_Handler_Alias(void) __attribute__((alias("HardFault_Handler")));
HardFault_Handler_Alias becomes another name for HardFault_Handler. Both resolve to identical machine code — same address, no thunk, no jump. The aliased target (HardFault_Handler) must be defined in the same translation unit.
STM32 HAL startup code uses this extensively:
/* C equivalent of startup_stm32f4xx.s */void Default_Handler(void) { while (1) { __asm("bkpt #0"); } }void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));void MemManage_Handler(void) __attribute__((weak, alias("Default_Handler")));void BusFault_Handler(void) __attribute__((weak, alias("Default_Handler")));void UsageFault_Handler(void) __attribute__((weak, alias("Default_Handler")));/* Peripheral IRQs */void WWDG_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));void PVD_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));/* ... 70+ more weak aliases ... */
One Default_Handler definition. Eighty weak aliases. The vector table entries all point to the same instruction. Code size: one function + 80 symbol table entries (negligible). Because they are weak aliases, the application can still override them. If they were strong aliases (without the weak attribute), application overrides would cause a “multiple definition” linker error.
When a HAL function is renamed but old code must compile:
/* New API — defined in hal_uart.c */HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart,uint8_t *pData, uint16_t Size) {/* ... implementation ... */}/* Old name — alias to new implementation (same TU) */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 | Weak 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);extern __attribute__((weak)) void HAL_GPIO_EXTI_Callback(uint16_t pin);/* hal_gpio_defaults.c */__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 fallback || v || +---------------------+ || | void Default_Handler| || | { while(1); } | || +---------------------+ || ^ || | weak 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 multiple files */__attribute__((weak)) int compute(int x) { return x * 2; }
If app.c includes this header and tries to provide a strong override, it will fail to compile with a redefinition of 'compute' error (because the compiler sees the weak definition from the header and the strong one in the .c file simultaneously). If the app doesn’t override it, multiple .o files will contain weak copies, and the linker arbitrarily picks one.
The fix: Declare extern __attribute__((weak)) in the header, and provide the weak definition 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 in this TU
GCC emits a compile-time error: 'bar' aliased to undefined symbol. The aliased target must be defined in the same translation unit.
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 override)# 08000244 T Default_Handler <- strong (alias target)# 08000244 W EXTI1_IRQHandler <- weak alias (same address)
The nm tool shows symbol types: uppercase T means global (strong) text symbol, lowercase t means local, and W means weak. An alias shares the same address as its target.
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/Common-Function-Attributes.htmlQuick Links
Legal Stuff





