STM32 HAL库中__weak函数被用户重写后仍进入默认实现的链接脚本排查

问题现象

在STM32 HAL库工程中,我们经常重写HAL_UART_RxCpltCallback、HAL_GPIO_EXTI_Callback等__weak回调函数。但有时编译链接都通过,运行时却仍然进入HAL库的默认空实现,用户代码完全没被执行。

这类问题通常不是C语言层面的重定义错误(否则编译会报multiple definition),而是链接阶段符号解析或段回收导致的。

原理:__weak与链接器符号解析

HAL库中的回调定义形如:

__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);
}

__weak在ARM Compiler中展开为__attribute__((weak)),在GCC中同样如此。弱符号的规则是:

  • 若存在同名的强符号,链接器选择强符号;
  • 若只有弱符号,则使用弱符号;
  • 多个弱符号时选择其中一个(通常第一个)。

因此用户重写的强符号理应覆盖弱符号。但以下情况会破坏这一规则:

  1. 用户函数被放在被--gc-sections回收的段中,链接器认为它未被引用而丢弃;
  2. 链接脚本中KEEP缺失,导致中断向量表或回调表引用的符号被回收;
  3. 用户函数被编译进静态库,而链接器在解析弱符号时不会主动从库中拉取该目标文件;
  4. 链接顺序问题:库在用户目标文件之前被扫描,弱符号先被解析。

排查步骤

1. 查看map文件确认符号来源

在工程中开启-Wl,-Map=output.map,搜索目标函数:

HAL_UART_RxCpltCallback

观察其地址与所属目标文件。如果显示来自stm32f4xx_hal_uart.o,说明用户实现未被选中。

2. 用objdump检查符号表

arm-none-eabi-nm -C build/app.elf | grep HAL_UART_RxCpltCallback
arm-none-eabi-objdump -t build/app.elf | grep HAL_UART_RxCpltCallback

若只看到一个W(weak)符号,说明用户强符号根本没进ELF;若有T和W两个,则需看最终地址绑定。

3. 检查用户函数所在段是否被回收

arm-none-eabi-objdump -h build/app.elf | grep -E "text|rodata"

若用户函数所在.text.user_callback段不存在,说明被--gc-sections回收。

根因与解决方案

情况A:--gc-sections回收了用户函数

当用户回调仅通过函数指针(如HAL内部回调表)被引用,而链接器无法静态追踪时,会被误回收。解决方式是在链接脚本中KEEP:

SECTIONS
{
  .text :
  {
    KEEP(*(.isr_vector))
    *(.text)
    *(.text*)
    KEEP(*(.text.HAL_UART_RxCpltCallback))
    . = ALIGN(4);
  } >FLASH
}

更通用的做法是给用户回调加used属性:

__attribute__((used))
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* user code */
}

情况B:用户实现位于静态库中

若用户回调被编译进libuser.a,链接器在扫描库时若尚未遇到未定义引用,就不会拉取该目标文件。解决方法:

  • 将用户回调源文件直接加入工程源文件列表(不打包成库);
  • 或使用-Wl,--whole-archive强制链接整个库;
  • 或在链接脚本中用EXTERN()声明符号,强制链接器保留。
EXTERN(HAL_UART_RxCpltCallback)

情况C:链接顺序错误

确保用户目标文件在HAL库之前参与链接。在Makefile中调整:

$(TARGET).elf: $(OBJS) $(USER_OBJS) $(LIBS)
	$(CC) $(OBJS) $(USER_OBJS) -Wl,--start-group $(LIBS) -Wl,--end-group -T$(LDSCRIPT) -o $@

使用--start-group/--end-group可解决库间循环依赖。

完整示例:可复现的最小工程配置

/* user_callback.c */
#include "main.h"

__attribute__((used))
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    /* 用户处理逻辑 */
    HAL_UART_Transmit(&huart1, (uint8_t*)"OK\r\n", 4, 100);
  }
}

链接脚本关键片段:

ENTRY(Reset_Handler)

SECTIONS
{
  .isr_vector :
  {
    KEEP(*(.isr_vector))
  } >FLASH

  .text :
  {
    *(.text)
    *(.text*)
    KEEP(*(.init))
    KEEP(*(.fini))
    . = ALIGN(4);
  } >FLASH

  .rodata :
  {
    *(.rodata)
    *(.rodata*)
  } >FLASH
}

编译选项:

CFLAGS += -ffunction-sections -fdata-sections
LDFLAGS += -Wl,--gc-sections -Wl,-Map=$(BUILD_DIR)/$(TARGET).map

注意事项

  • -ffunction-sections与--gc-sections配合使用时,务必对中断向量、回调表、__attribute__((used))符号做KEEP保护;
  • 不要仅依赖__weak覆盖,关键回调建议加used属性;
  • 静态库中的弱符号覆盖极易失效,优先把用户实现放在应用目标文件中;
  • 每次修改链接脚本后,用nm和map文件双重确认符号最终绑定;
  • 若使用IAR/Keil,对应机制为__weak与keep/--keep,排查思路一致。

通过以上链接脚本与符号表层面的排查,可以稳定定位并解决“重写__weak函数却仍进入默认实现”的问题。