HomeAbout UsContact Us

Weak Symbols and Function Aliasing in Embedded C

By Jithin Tom
Published in Embedded C/C++
July 20, 2026
3 min read
Weak Symbols and Function Aliasing in Embedded C

Table Of Contents

01
Weak Symbols: Linker-Time Override Mechanism
02
Function Aliasing: Multiple Names, One Address
03
Combining Both: The HAL Override Pattern
04
ASCII Art: Vector Table Resolution
05
Common Pitfalls
06
Verification: Check What the Linker Chose
07
Summary
08
Related Reading
09
References
10
Frequently Asked Questions

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.

Weak Symbols: Linker-Time Override Mechanism

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.

Default Interrupt Handlers

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 Data Symbols for Configuration Defaults

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.

Pitfall: Weak Definitions in Headers

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); }

Function Aliasing: Multiple Names, One Address

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.

Vector Table Compression

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

Aliasing for Backward Compatibility

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.

Aliasing vs. Weak: When to Use Which

ScenarioMechanismReason
Default IRQ handlerWeakApp overrides selectively
Vector table defaultAlias80+ IRQs → one handler, zero overhead
HAL default configWeak dataApp overrides struct once
Deprecated API nameAliasSame code, two entry points
Optional callbackWeak functionApp provides if needed
Multiple ISR names → one handlerAliasDMA stream aliases, EXTI line groups

Rule of thumb: Weak = “override me if you want.” Alias = “this is another name for the same thing.”

Combining Both: The HAL Override Pattern

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

ASCII Art: Vector Table Resolution

+------------------------------------------------------------------+
| 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) │ |
| └──────────────┴────────────────────────────────────────────┘ |
| |
+------------------------------------------------------------------+

Common Pitfalls

1. Weak Function in Header Included by Library and App

/* 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.

2. C++ ODR Violation

// log.h
extern "C" __attribute__((weak)) void log_putc(char c); // OK: declaration
// log.cpp
extern "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.

3. Alias to Non-Existent Function

void foo(void) __attribute__((alias("bar"))); // bar not defined anywhere

Linker error: undefined reference to 'bar'. The aliased target must exist.

4. Weak Symbol Hidden by Static Library

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.

Verification: Check What the Linker Chose

# After linking, inspect the symbol table
arm-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).

Summary

  • Weak symbols provide link-time overridable defaults. Use for default ISRs, configuration structs, optional callbacks.
  • Function aliases create multiple names for one function body. Use for vector table density, API backward compatibility, grouped interrupt handlers.
  • Never define weak functions in headers — declare extern weak in headers, define once in a .c file.
  • In C++, weak definitions in headers violate ODR. Keep weak declarations in headers, definitions in one TU.
  • Verify linker output with nm or objdump to confirm which definition won.
  • Combine both: aliases compress the vector table; weak symbols let the app hook in.

These mechanisms cost zero runtime cycles. They shift complexity to link time where the toolchain resolves it once, correctly, for every build.

  • Interrupt Handling and ISRs in Embedded Systems
  • Linker Scripts and Memory Layout in Embedded C
  • Fixing I2C Clock Stretching Timeouts on STM32

References

  1. GCC Documentation: Function Attributes — weak, aliashttps://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html
  2. ARM Cortex-M Programming Guide to Memory Barriers — https://developer.arm.com/documentation/102448/0100
  3. STM32F4xx HAL Driver Source (startup_stm32f4xx.s) — STMicroelectronics
  4. “Linkers and Loaders” by John Levine, Chapter 3: Symbol Resolution — ISBN 978-1558604964
  5. C++ Core Guidelines: ODR and Weak Symbols — https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
  6. Embedded Artistry: Weak Functions in Embedded C — https://embeddedartistry.com/fieldatlas/enabling-users-to-override-functions-in-c-c/

Frequently Asked Questions

What is a weak symbol in C?

A weak symbol is a symbol (function or variable) that the linker will use only if no strong definition exists. If a strong definition is found, the weak one is silently discarded. This allows libraries to provide default implementations that applications can override without linker errors.

How does function aliasing differ from weak symbols?

Function aliasing creates an alternative name for the same function address using the `alias` attribute. Both names resolve to identical code. Weak symbols provide a fallback that can be overridden; aliases provide multiple entry points to the same implementation. Aliases are commonly used for vector table entries (e.g., `Default_Handler` aliased to multiple IRQ handlers).

When should I use weak symbols vs. function pointers for HAL design?

Use weak symbols for default handlers that most applications will never override (interrupt vectors, default ISRs). Use function pointers in structs for runtime-configurable callbacks and driver operations that vary per-instance. Weak symbols are resolved at link time with zero runtime overhead; function pointers add indirection but enable runtime polymorphism.

What happens if I define a weak function in a header file?

Defining a weak function in a header included by multiple translation units creates multiple weak definitions. The linker picks one arbitrarily (first seen). This works but is fragile. Best practice: declare weak symbols in headers with `extern __attribute__((weak))`, provide the default definition in exactly one .c file, and let applications override in their own .c files.

Can weak symbols cause ODR violations in C++?

Yes. C++ has stricter One Definition Rule (ODR) semantics. A weak function defined in a header violates ODR if included in multiple TUs, even with `weak`. In C++, prefer inline functions or explicit template specializations for header-only defaults. For C-compatible weak symbols, declare `extern "C" __attribute__((weak))` in headers and define in a single .cpp file.

Tags

embedded-cgccweak-symbolsfunction-aliasinglinkerhalstm32

Share


Previous Article
Zero-Copy DMA Patterns on ARM Cortex-M
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Zero-Copy DMA Patterns on ARM Cortex-M
Zero-Copy DMA Patterns on ARM Cortex-M
July 19, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media