问题现象:重写__weak函数未生效

在STM32 HAL库开发中,我们常通过重写__weak函数(如HAL_GPIO_EXTI_Callback)来实现自定义逻辑。但有时编译链接后,程序仍执行HAL库中的默认空函数,导致功能异常。例如:

// 用户代码中重写
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_0) {
        // 自定义处理
    }
}

但调试发现该函数从未被调用,中断触发后无反应。这通常不是代码错误,而是链接器选择了错误的函数定义。

原理:__weak与链接器行为

__weak是ARM编译器(AC6)的关键字,用于声明弱符号。链接器在解析符号时遵循以下规则:

  • 强符号优先于弱符号:若存在同名强符号,链接器选择强符号。
  • 多个强符号冲突:链接报错(重复定义)。
  • 仅弱符号:选择任意一个弱符号(通常为第一个遇到的)。

HAL库中,回调函数定义为__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin),属于弱符号。用户重写时未加__weak,属于强符号。理论上链接器应选择用户强符号。但若链接顺序不当,链接器可能先遇到弱符号并完成解析,导致强符号被忽略。

关键点:链接器按目标文件顺序处理符号。若包含弱符号的库文件(如stm32f1xx_hal_gpio.o)在用户目标文件之前被链接,且该弱符号未被其他强符号引用,链接器可能直接采用弱符号,后续强符号被视为重复定义而忽略(取决于链接器选项)。

排查步骤

1. 确认符号类型

使用arm-none-eabi-nm查看目标文件符号:

arm-none-eabi-nm -C build/main.o | grep HAL_GPIO_EXTI_Callback
# 输出应为 T(强符号)或 W(弱符号)

若用户函数显示为W,说明未正确声明强符号(可能误加__weak)。

2. 检查链接顺序

查看链接器命令或Makefile中的目标文件顺序。通常用户目标文件应位于HAL库之前。例如:

# 错误顺序:库在前
$(CC) $(LDFLAGS) -o $(TARGET) $(LIBRARIES) $(OBJECTS)

# 正确顺序:用户目标文件在前
$(CC) $(LDFLAGS) -o $(TARGET) $(OBJECTS) $(LIBRARIES)

在STM32CubeIDE中,链接顺序由.cproject管理,一般无需手动调整,但若使用自定义Makefile需注意。

3. 使用链接器选项强制选择

若无法调整顺序,可添加链接器选项--allow-multiple-definition(不推荐)或使用-Wl,--defsym强制指定符号地址。更规范的做法是使用__attribute__((used))确保用户函数被保留,并检查是否被优化掉。

4. 检查函数是否被内联或优化

若用户函数定义在头文件中且未加static inline,可能引发多重定义。确保函数定义在.c文件中,且声明为强符号。

完整代码示例

以下示例展示正确重写与链接配置:

user_exti.c

#include "main.h"

// 强符号重写,不加__weak
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_0) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

Makefile片段

# 用户目标文件在前,库在后
OBJECTS = main.o user_exti.o stm32f1xx_it.o
LIBRARIES = -lstm32f1xx_hal -lc -lm

$(TARGET): $(OBJECTS)
	$(CC) $(LDFLAGS) -o $@ $(OBJECTS) $(LIBRARIES)

链接脚本检查 确保.text段包含用户代码,且无/DISCARD/误删。

注意事项

  • 重写__weak函数时,不要加__weak关键字,否则仍为弱符号。
  • 避免在头文件中定义非static函数,防止多重定义。
  • 使用arm-none-eabi-nm定期检查符号类型,确保为T。
  • 若使用静态库(.a),链接器仅提取被引用的目标文件,可能跳过包含弱符号的文件,此时需用--whole-archive强制包含(谨慎使用)。
  • 在STM32CubeIDE中,检查“Linker”设置中的“Other options”是否包含异常选项。
  • 调试时可在重写函数内设断点,若未命中,结合反汇编确认实际调用地址。

通过以上排查,可解决绝大多数__weak函数重写失效问题。理解链接器符号解析机制,是嵌入式开发中避免此类陷阱的关键。