STM32 HAL库中__weak函数被用户重定义后链接脚本仍保留原符号的排查方法

一、问题现象与背景

在STM32 HAL库开发中,我们经常重定义__weak函数来实现自定义回调,例如:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 用户代码
}

但有时会发现:用户函数明明已经定义,运行时却仍进入HAL库的默认空实现。用arm-none-eabi-nm查看ELF,发现两个符号同时存在,或者链接脚本(.ld)中保留了原弱符号。这类问题通常与__weak机制、链接器符号解析顺序、链接脚本的KEEP/EXCLUDE规则有关。

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

2.1 __weak的本质

__weak是编译器属性(GCC下为__attribute__((weak))),它告诉链接器:该符号是弱符号。当存在同名强符号时,链接器优先选择强符号;若只有弱符号,则使用弱符号。

HAL库中大量回调函数定义为:

__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    UNUSED(huart);
}

2.2 链接器符号解析规则

  • 强符号只能定义一次,重复定义会报错。
  • 弱符号可被强符号覆盖。
  • 若多个弱符号同名,链接器通常选择第一个遇到的。
  • 链接脚本中的KEEP、EXCLUDE_FILE、PROVIDE等指令会影响符号的保留与解析。

2.3 常见误判原因

  1. 用户函数未加__weak,但被放在不同编译单元,链接顺序导致弱符号先被解析。
  2. 链接脚本中使用了KEEP(*(.text.HAL_UART_RxCpltCallback)),强制保留库中弱符号。
  3. 使用了-ffunction-sections和--gc-sections,但链接脚本未正确丢弃弱符号段。
  4. 用户函数被编译器优化掉(未调用或static)。
  5. 多个弱符号同名,链接器选择了库中的版本。

三、排查步骤

步骤1:确认符号表

使用arm-none-eabi-nm查看符号类型:

arm-none-eabi-nm -C build/Project.elf | grep HAL_UART_RxCpltCallback
  • W表示弱符号,T表示强符号。若同时出现W和T,说明强符号存在,但需确认最终地址。

步骤2:分析map文件

在链接选项中添加-Wl,-Map=output.map,然后搜索符号:

grep -n "HAL_UART_RxCpltCallback" output.map

观察该符号被分配到哪个段、来自哪个目标文件。若来自stm32f4xx_hal_uart.o,说明弱符号被保留。

步骤3:反汇编确认调用地址

arm-none-eabi-objdump -d build/Project.elf | grep -A5 "HAL_UART_RxCpltCallback"

查看中断处理函数中调用的地址是否指向用户函数。

步骤4:检查链接脚本

重点检查:

  • 是否有KEEP指令强制保留库函数段。
  • 是否使用了EXCLUDE_FILE排除用户目标文件。
  • PROVIDE是否覆盖了符号。

示例问题链接脚本片段:

.text :
{
    KEEP(*(.text.HAL_UART_RxCpltCallback))
    *(.text*)
}

上述KEEP会强制保留弱符号,导致用户强符号无法覆盖。

步骤5:检查编译选项

  • 确保用户文件参与编译且未被--gc-sections丢弃。
  • 确保用户函数非static,且声明与HAL库一致。
  • 检查-ffunction-sections是否开启,若开启需配合--gc-sections。

四、完整代码示例

4.1 用户重定义回调

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

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

4.2 正确的链接脚本片段

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

避免使用KEEP强制保留HAL库弱符号段。

4.3 编译与链接命令示例

arm-none-eabi-gcc -c -ffunction-sections -fdata-sections user_callback.c -o user_callback.o
arm-none-eabi-gcc -Wl,--gc-sections -Wl,-Map=output.map -T stm32f4.ld *.o -o Project.elf

五、注意事项

  • 不要对HAL库弱符号使用KEEP,除非你明确需要保留默认实现。
  • 用户重定义函数必须与HAL库原型完全一致(参数、返回值、调用约定)。
  • 若使用C++,注意extern "C"避免名称修饰。
  • 开启--gc-sections时,确保用户函数被引用,否则可能被丢弃。
  • 使用__attribute__((used))可防止函数被优化掉。
  • 定期检查map文件,确认符号来源,是排查链接问题的黄金手段。

通过以上方法,可系统性地定位并解决__weak函数重定义后链接脚本仍保留原符号的问题,确保回调按预期生效。