问题现象:重写__weak函数为何不生效?

在STM32 HAL库中,许多回调函数(如HAL_UART_RxCpltCallback、HAL_GPIO_EXTI_Callback)被定义为__weak,允许用户重写。但开发者常遇到:明明在main.c或自定义文件中重写了函数,运行时却仍执行HAL库中的空实现。

例如:

// 用户代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 自定义逻辑
    HAL_UART_Transmit(&huart2, (uint8_t*)"OK\r\n", 4, 100);
}

但串口并未输出"OK",说明回调未被执行。

原理:弱符号与强符号的链接规则

  • 弱符号(weak symbol):用__weak修饰的函数或变量,链接器允许被同名强符号覆盖。
  • 强符号(strong symbol):普通定义的函数或变量,优先级高于弱符号。
  • 链接器行为:当同时存在弱符号和强符号时,链接器选择强符号;若只有弱符号,则使用弱符号。

HAL库中__weak函数编译后成为弱符号,用户重写后生成强符号。理论上链接器应选择强符号,但实际可能因链接顺序或库文件包含方式导致弱符号被优先选中。

关键原因:链接顺序与库文件包含

1. 链接顺序的影响

链接器按命令行中目标文件和库文件的顺序处理符号。若用户目标文件(含强符号)在库文件(含弱符号)之前被链接,链接器先看到强符号,后续弱符号被忽略,正确。反之,若库文件在前,链接器先解析弱符号,且用户目标文件中的强符号可能因“符号已定义”而被丢弃(取决于链接器策略)。

  • GNU ld默认行为:对于静态库,链接器只提取未定义符号所在的库成员。若弱符号已满足引用,库成员不会被提取,但用户目标文件中的强符号仍可能被忽略?实际上,GNU ld会优先选择强符号,但前提是强符号在弱符号之前被看到。若弱符号先被看到,链接器可能认为符号已定义,不再搜索后续强符号。

2. 库文件包含方式

  • 直接编译HAL库源文件:所有源文件与用户代码一起编译链接,符号顺序取决于文件列表顺序。通常用户文件在前,HAL库文件在后,强符号优先,问题较少。
  • 使用预编译的HAL库(.a文件):链接器按顺序处理。若.a文件在用户目标文件之前,弱符号先被解析,用户强符号可能被忽略。

排查步骤

步骤1:检查链接顺序

查看链接器命令行(如Keil的.map文件或GCC的-Wl,--verbose输出),确认用户目标文件是否在HAL库文件之前。

  • Keil MDK:在Options for Target → Linker → Misc controls中查看链接顺序,或打开.map文件搜索函数名。
  • STM32CubeIDE:在Project Properties → C/C++ Build → Settings → Tool Settings → MCU/MPU Linker中查看Linker flags,或检查.map文件。

步骤2:使用nm或objdump查看符号

对编译后的.o文件和.a文件执行:

arm-none-eabi-nm -C user_file.o | grep HAL_UART_RxCpltCallback
arm-none-eabi-nm -C libHAL.a | grep HAL_UART_RxCpltCallback
  • 用户文件应显示T(强符号),库文件显示W(弱符号)。
  • 若用户文件显示W,说明重写未生效(可能函数签名不匹配或未编译)。

步骤3:检查.map文件

在.map文件中搜索函数名,查看最终链接地址来自哪个目标文件。若地址来自HAL库文件,则强符号未被选中。

解决方案

方案1:调整链接顺序

确保用户目标文件在HAL库文件之前。

  • Keil:在Options for Target → Linker → Use Memory Layout from Target Dialog取消勾选,手动编辑.sct文件,将用户目标文件放在前面。
  • GCC:在链接命令中将用户.o文件放在.a文件之前,例如:
arm-none-eabi-gcc -o firmware.elf main.o user_callback.o -lHAL

方案2:强制链接用户符号

使用链接器选项--undefined强制引用符号,确保用户强符号被提取。

-Wl,--undefined=HAL_UART_RxCpltCallback

或在Keil的Misc controls中添加--undefined=HAL_UART_RxCpltCallback。

方案3:避免使用预编译库

将HAL库源文件直接加入工程编译,确保所有符号在同一个链接单元中处理,链接器自然选择强符号。

方案4:使用__attribute__((used))

在用户重写函数前添加__attribute__((used)),防止被优化掉,并确保符号被保留。

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

注意事项

  • 函数签名必须完全一致:包括参数类型和返回类型,否则编译器视为不同函数,不会覆盖。
  • 避免在头文件中定义函数:可能导致多重定义或弱符号冲突。
  • 检查编译优化等级:高优化等级可能内联或删除未使用的弱函数,但通常不影响覆盖。
  • 使用-ffunction-sections和--gc-sections:可能移除未引用的强符号,需配合__attribute__((used))或KEEP段。
  • 调试时查看反汇编:确认最终调用的函数地址是否指向用户代码。

总结

__weak函数重写失效的根源在于链接器对符号的解析顺序。通过理解弱符号与强符号的链接规则,检查链接顺序、使用nm工具验证符号类型、调整工程配置,即可彻底解决。推荐优先采用直接编译HAL源文件的方式,避免预编译库带来的顺序问题。