问题现象与背景

在STM32 HAL库项目中,我们常通过重定义__weak函数来定制中断回调。例如,重定义HAL_GPIO_EXTI_Callback以处理外部中断。但有时编译链接均无报错,运行时中断却仍进入HAL库默认的while(1)死循环。

典型场景:

  • 用户在新文件(如user_exti.c)中定义了void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)。
  • 编译链接成功,但触发中断后程序卡在stm32f1xx_hal_gpio.c的默认回调中。
  • 使用调试器查看符号地址,发现用户函数未被链接到中断向量表或调用点。

__weak机制与链接器行为

弱符号与强符号

  • __weak在GCC中通过__attribute__((weak))实现,将函数标记为弱符号。
  • 链接器规则:若存在同名强符号,则强符号覆盖弱符号;否则使用弱符号。
  • HAL库中大量回调函数定义为__weak,正是为了允许用户重定义。

常见误判

  • 用户函数未加__weak,理论上是强符号,应覆盖弱符号。
  • 但若用户函数所在目标文件未被链接器纳入最终镜像,强符号根本不存在,链接器只能选择弱符号。
  • 链接脚本的段放置、KEEP指令、--gc-sections等均可能影响目标文件是否被链接。

链接脚本排查步骤

步骤1:确认用户函数是否被链接

使用arm-none-eabi-nm查看最终ELF符号表:

arm-none-eabi-nm -C build/your_project.elf | grep HAL_GPIO_EXTI_Callback
  • 若输出为W(弱符号)且地址在HAL库区域,说明用户强符号未链接。
  • 若输出为T(强符号)且地址在用户区域,则链接正确。

步骤2:检查链接脚本的段收集

典型STM32链接脚本(.ld)中,.text段收集所有代码:

.text :
{
  *(.text)
  *(.text*)
  *(.glue_7)
  *(.glue_7t)
  . = ALIGN(4);
  _etext = .;
} >FLASH
  • 若用户函数被放置到自定义段(如__attribute__((section(".user_text")))),而链接脚本未收集该段,则函数被丢弃。
  • 检查是否有*(.user_text)或*(.text*)覆盖自定义段。

步骤3:检查--gc-sections与KEEP

  • 若启用-ffunction-sections -fdata-sections -Wl,--gc-sections,链接器会删除未引用的段。
  • 用户回调函数若仅被弱符号引用(如HAL库中通过函数指针调用),可能被误判为未引用而删除。
  • 解决方案:在链接脚本中用KEEP保留用户段,或使用__attribute__((used))标记函数。
__attribute__((used)) void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    // 用户代码
}

步骤4:检查中断向量表与调用路径

  • 确认中断服务函数(如EXTI0_IRQHandler)调用了HAL_GPIO_EXTI_IRQHandler,后者再调用HAL_GPIO_EXTI_Callback。
  • 若用户重定义了EXTI0_IRQHandler但未调用HAL处理函数,则回调不会执行。
  • 使用调试器在HAL_GPIO_EXTI_Callback处设断点,观察实际跳转地址。

完整代码示例

用户回调定义(user_exti.c)

#include "stm32f1xx_hal.h"

// 使用used属性防止被gc-sections删除
__attribute__((used)) void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_0) {
        // 用户处理逻辑
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    }
}

链接脚本片段(STM32F103C8Tx_FLASH.ld)

.text :
{
  . = ALIGN(4);
  *(.text)
  *(.text*)
  *(.glue_7)
  *(.glue_7t)
  *(.eh_frame)
  . = ALIGN(4);
  _etext = .;
} >FLASH

/* 确保用户自定义段被保留 */
.user_text :
{
  . = ALIGN(4);
  KEEP(*(.user_text))
  . = ALIGN(4);
} >FLASH

编译选项检查

CFLAGS += -ffunction-sections -fdata-sections
LDFLAGS += -Wl,--gc-sections -Wl,-Map=output.map

注意事项

  • 符号可见性:确保用户函数与HAL库声明完全一致(参数类型、返回值),否则视为不同符号。
  • 链接顺序:目标文件链接顺序不影响强弱符号覆盖,但影响未定义符号解析。
  • Map文件分析:使用-Wl,-Map=output.map生成映射文件,搜索函数名确认其所在段与地址。
  • 调试器验证:在回调函数入口设断点,若未命中,检查反汇编确认调用指令目标地址。
  • 避免重复定义:若用户函数与HAL库函数在同一目标文件,可能引发多重定义错误,需分离文件。
  • 启动文件影响:中断向量表在启动文件中定义,确保未修改向量表导致跳转错误。

通过以上排查,可系统解决__weak函数重定义后中断仍进默认死循环的问题,核心在于确认强符号是否真正被链接器纳入最终镜像。