一、问题现象与背景

在STM32 HAL库项目中,我们经常重定义__weak函数来实现自定义逻辑,例如:

/* 用户代码 */
void HAL_InitTick(uint32_t TickPriority) {
    // 自定义SysTick初始化
}

但有时发现自定义函数未被调用,或者同一份代码在不同工程(如IAR、Keil、GCC)下行为不一致。典型现象:

  • 调试时断点无法进入用户函数,实际执行的是HAL库中的弱定义。
  • 添加/删除某个无关文件后,行为突然改变。
  • 使用__weak重定义HAL_UART_RxCpltCallback,但串口接收回调仍走默认空函数。

根本原因在于:链接器对弱符号与强符号的解析规则,以及目标文件在链接命令行中的顺序。

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

2.1 弱符号与强符号

  • 强符号:普通函数定义、已初始化的全局变量。
  • 弱符号:用__weak(或__attribute__((weak)))修饰的函数/变量,或未初始化的全局变量。

链接器规则:

  1. 不允许两个强符号同名(重复定义报错)。
  2. 强符号优先于弱符号:若同时存在强符号和弱符号,链接器选择强符号。
  3. 多个弱符号同名:链接器选择第一个遇到的弱符号(取决于目标文件顺序)。

2.2 HAL库中的__weak函数

HAL库大量使用__weak定义默认回调函数,例如:

__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    /* 默认空实现 */
}

用户重定义时,相当于提供了强符号,理论上应覆盖弱符号。但若链接器未正确识别强符号(例如用户函数被编译为弱符号,或目标文件顺序导致弱符号先被选中),则行为异常。

2.3 链接顺序的影响

以GCC链接为例,链接器按命令行中目标文件/库的顺序依次处理。若用户目标文件在HAL库之后,且HAL库以静态库形式提供,则:

  • 链接器先处理HAL库,遇到未定义符号时从库中提取弱定义目标文件,该弱符号被加入符号表。
  • 后续用户目标文件提供强符号,但链接器已解析该符号,可能忽略强符号(取决于链接器实现,通常强符号会覆盖,但某些场景下若弱符号已被引用则不会重新解析)。

更常见的是:用户代码与HAL库源文件同时编译,链接顺序由IDE自动生成,若用户文件排在HAL库文件之后,且HAL库中弱符号先被引用,则可能选中弱符号。

三、排查步骤与实战

3.1 确认符号类型

使用nm工具查看目标文件符号类型:

arm-none-eabi-nm -C build/main.o | grep HAL_InitTick

输出示例:

00000000 T HAL_InitTick   // T: 强符号(用户定义)
00000000 W HAL_InitTick   // W: 弱符号(HAL库定义)

若用户函数显示为W,说明重定义未生效(可能误加了__weak)。

3.2 检查链接映射文件

在Keil/IAR/GCC中生成.map文件,搜索目标函数,查看最终链接地址来自哪个目标文件。例如GCC的-Wl,-Map=output.map。

在map文件中查找:

HAL_InitTick   0x08001234  0x20  build/stm32f1xx_hal.o

若地址指向HAL库目标文件,则弱符号被选中。

3.3 调整链接顺序

方法一:将用户目标文件放在链接命令行前面(GCC):

# 错误顺序:库在前,用户文件在后
$(CC) -o app.elf $(HAL_LIB) main.o user_hal.o

# 正确顺序:用户文件在前,库在后
$(CC) -o app.elf main.o user_hal.o $(HAL_LIB)

方法二:使用--whole-archive强制包含库中所有目标文件(不推荐,增大体积)。

方法三:在IDE中调整文件编译顺序(Keil:Options for Target -> Linker -> 取消“Use Memory Layout from Target Dialog”,手动调整;IAR:Linker -> Config -> 调整文件顺序)。

3.4 完整代码示例

HAL库弱定义(stm32f1xx_hal.c):

__weak void HAL_InitTick(uint32_t TickPriority) {
    // 默认使用SysTick
    HAL_SYSTICK_Config(SystemCoreClock / 1000U);
}

用户强定义(user_tick.c):

#include "stm32f1xx_hal.h"

void HAL_InitTick(uint32_t TickPriority) {
    // 自定义:使用TIM6作为时基
    TIM6_Init();
    HAL_NVIC_SetPriority(TIM6_IRQn, TickPriority, 0);
    HAL_NVIC_EnableIRQ(TIM6_IRQn);
}

链接脚本/命令(GCC Makefile片段):

SRCS = main.c user_tick.c stm32f1xx_hal.c ...
OBJS = $(SRCS:.c=.o)

app.elf: $(OBJS)
	$(CC) $(CFLAGS) -T stm32f1.ld -Wl,-Map=app.map -o $@ $(OBJS) $(LIBS)

确保user_tick.o在stm32f1xx_hal.o之前。

3.5 验证方法

  • 在用户函数入口设置断点,运行确认是否命中。
  • 使用nm确认最终elf中符号类型为T。
  • 查看map文件确认地址归属。

四、注意事项与最佳实践

  • 不要给用户重定义函数加__weak:否则可能被其他弱符号覆盖。
  • 避免在头文件中声明__weak:弱属性应仅出现在定义处。
  • 统一使用强符号覆盖:若需多个可选实现,可用函数指针或宏开关,而非依赖弱符号顺序。
  • 注意库的打包方式:静态库(.a)中弱符号的提取规则更复杂,建议将用户重定义文件直接加入工程源文件列表,而非库。
  • 跨编译器差异:IAR的__weak与GCC的__attribute__((weak))行为基本一致,但链接器对顺序的敏感度不同,务必查看map文件确认。
  • 调试技巧:在弱函数内加入__asm volatile("nop")或翻转GPIO,便于逻辑分析仪捕捉实际执行路径。

通过以上系统化排查,可快速定位并解决__weak函数重定义失效问题,确保嵌入式系统行为一致可靠。