问题现象:重写无效,中断仍进默认死循环

在使用STM32 HAL库开发时,我们经常通过重写__weak函数来实现自定义中断处理逻辑。例如:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == KEY_Pin) {
        // 用户自定义逻辑
    }
}

但有时编译下载后,中断触发时程序却跳转到了HAL库默认的__weak函数中——通常是一个空的while(1)死循环。更诡异的是,用调试器查看符号表,发现用户函数确实存在,但中断向量表却指向了默认实现。

原理:__weak与链接脚本的协作机制

__weak的本质

__weak是ARM编译器(AC6/AC5)的扩展关键字,用于声明弱符号。当链接器发现强符号(用户定义)和弱符号(库中定义)同名时,会优先选择强符号。

__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    UNUSED(GPIO_Pin);
    while(1); // 默认死循环
}

链接脚本的关键作用

链接脚本(.ld文件)决定了各个段(section)在内存中的布局。HAL库的弱函数通常被放置在.text段中,而用户函数可能被链接器放置在不同的段(如.text.user或.text.*)。

关键点:链接器在选择强符号时,会优先从同一输入段中查找。如果用户函数被链接到不同的段,且链接脚本没有正确合并这些段,链接器可能仍然选择弱符号。

排查步骤:定位链接脚本问题

步骤1:确认符号是否被正确链接

使用arm-none-eabi-nm查看最终ELF文件:

arm-none-eabi-nm -C build/Project.elf | grep HAL_GPIO_EXTI_Callback
  • 如果输出显示T(全局文本符号)且地址与用户函数一致,说明链接正确。
  • 如果显示W(弱符号)或地址指向库函数,说明链接器选择了弱符号。

步骤2:检查链接脚本中的段合并规则

打开.ld文件,查看.text段的定义:

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

常见问题:如果用户函数被编译器放入.text.user段(通过__attribute__((section(".text.user")))),而链接脚本只写了*(.text)和*(.text*),则.text.user可能被遗漏,导致用户函数未被链接到最终镜像中。

步骤3:检查编译器优化与段属性

某些编译器优化(如-ffunction-sections)会将每个函数放入独立的段(如.text.HAL_GPIO_EXTI_Callback)。此时链接脚本必须包含*(.text*)通配符。

// 检查编译选项
// 如果使用了 -ffunction-sections,确保链接脚本有 *(.text*)

步骤4:使用map文件分析

在编译选项中添加-Wl,-Map=output.map,然后查看map文件:

.text.HAL_GPIO_EXTI_Callback
    0x08001234    0x20  build/main.o

如果用户函数出现在map文件中,但中断向量表仍指向弱符号,说明链接器在符号解析阶段选择了弱符号。

完整解决方案

方案1:修改链接脚本(推荐)

确保.text段包含所有可能的用户函数段:

.text :
{
    *(.text)
    *(.text*)
    *(.text.user)      /* 显式包含用户自定义段 */
    *(.glue_7)
    *(.glue_7t)
    . = ALIGN(4);
    _etext = .;
} > FLASH

方案2:使用KEEP指令防止段被丢弃

如果用户函数被链接到独立段且未被引用,链接器可能将其丢弃。使用KEEP保留:

.text :
{
    KEEP(*(.text.HAL_GPIO_EXTI_Callback))
    *(.text*)
} > FLASH

方案3:强制使用强符号属性

在用户函数定义前添加__attribute__((used)),防止被优化掉:

__attribute__((used))
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    // 用户逻辑
}

方案4:检查启动文件中的向量表

确保启动文件(startup_stm32xxxx.s)中的中断向量表引用了正确的符号。例如:

.word HAL_GPIO_EXTI_Callback  /* 应指向用户函数 */

注意事项

  • 不要随意修改HAL库源文件:直接修改库中的__weak函数会破坏可移植性,应通过重写方式实现。
  • 链接脚本的段顺序很重要:将用户自定义段放在库段之前,确保链接器优先选择用户符号。
  • 使用-ffunction-sections时务必配合--gc-sections:否则未使用的函数段会占用Flash空间。
  • 调试时优先查看map文件:map文件是链接过程的“黑匣子”,能快速定位符号解析问题。
  • 不同编译器行为差异:AC5与AC6对__weak的处理略有不同,AC6更严格,建议统一使用AC6并检查链接脚本兼容性。

通过以上排查与修复,可彻底解决“重写__weak函数后中断仍进默认死循环”的问题,确保自定义中断逻辑正确执行。