STM32 HAL库中__weak函数被用户重写后中断服务程序不执行的链接脚本排查
一、问题现象与背景
在STM32 HAL库工程中,我们经常重写HAL_GPIO_EXTI_Callback、HAL_UART_RxCpltCallback等__weak函数来实现业务逻辑。但有时代码编译通过、下载正常,中断却始终不进入回调。常见现象包括:
- 断点打在用户重写的回调函数中,永远不命中;
- 中断标志位被清除,但业务逻辑未执行;
- 换一个工程或换一个编译优化等级后问题消失或出现。
这类问题往往不是寄存器配置错误,而是链接期符号解析问题。本文聚焦“__weak函数被用户重写后中断服务程序不执行”的链接脚本排查方法。
二、原理讲解:__weak、向量表与链接器
2.1 __weak 的本质
__weak是编译器扩展属性(ARMCC/AC6为__weak,GCC为__attribute__((weak)))。它告诉链接器:该符号是弱符号,若存在同名强符号,则强符号覆盖弱符号。HAL库中大量回调函数定义为__weak,正是为了让用户可以在不修改库文件的情况下重写。
2.2 中断服务程序与向量表
Cortex-M的向量表位于启动文件(如startup_stm32f103xb.s)中,表项指向中断服务函数名,例如:
DCD EXTI0_IRQHandler
EXTI0_IRQHandler在HAL库中通常定义为__weak,内部调用HAL_GPIO_EXTI_IRQHandler,后者再调用HAL_GPIO_EXTI_Callback。因此,用户重写回调函数能否生效,取决于链接器最终选择的符号地址是否被正确放入向量表或调用链。
2.3 链接脚本的作用
链接脚本(.ld或.sct)决定各段(.text、.data、.isr_vector等)的放置位置。若用户重写函数被放入一个未被链接脚本包含的段,或链接器因--gc-sections将其回收,就会出现“函数存在但未生效”的现象。
三、排查步骤
3.1 确认符号是否被正确解析
生成.map文件(GCC加-Wl,-Map=output.map),搜索用户函数名:
HAL_GPIO_EXTI_Callback 0x08001234 Thumb Code 12 main.o(.text.HAL_GPIO_EXTI_Callback)
若看到的是库文件(如stm32f1xx_hal_gpio.o)中的弱符号地址,说明用户强符号未被选中。
3.2 检查函数签名与链接属性
用户重写函数必须与弱符号完全同名、同签名,且不能加static。若加了static,链接器看不到全局符号,弱符号继续生效。
3.3 检查 --gc-sections 与 KEEP
若使用-ffunction-sections -fdata-sections -Wl,--gc-sections,未被引用的函数会被回收。用户回调函数若只被弱符号间接引用,可能被误回收。可在链接脚本中加:
KEEP(*(.text.HAL_GPIO_EXTI_Callback))
或对回调函数加__attribute__((used))。
3.4 检查向量表段是否被正确放置
确认.isr_vector段被放在Flash起始地址,且没有被用户自定义段覆盖。若使用自定义链接脚本,务必保留:
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
3.5 检查启动文件中的弱别名
部分启动文件用.weak声明中断向量,若用户同时重写EXTI0_IRQHandler和回调,需确认没有重复定义导致链接器选择异常。
四、完整代码示例
4.1 用户重写回调(正确写法)
/* main.c */
#include "main.h"
/* 必须与HAL弱符号同名同签名,不能加static */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}
4.2 防止被回收的属性
__attribute__((used))
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* 业务逻辑 */
}
4.3 链接脚本关键片段
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
.text :
{
*(.text*)
*(.rodata*)
KEEP(*(.text.HAL_GPIO_EXTI_Callback))
} >FLASH
}
4.4 编译命令示例
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \
-ffunction-sections -fdata-sections \
-Wl,--gc-sections -Wl,-Map=output.map \
-T stm32f103.ld -o app.elf main.o startup.o
五、注意事项
- 重写
__weak函数时不要加static,否则链接器无法用强符号覆盖弱符号。 - 函数签名必须与HAL库声明完全一致,包括参数类型和返回值。
- 开启
--gc-sections时,对仅被弱引用调用的回调使用__attribute__((used))或链接脚本KEEP。 - 每次修改链接脚本后,务必查看
.map文件确认符号地址落在预期段内。 - 若同时重写
xxx_IRQHandler和回调,注意避免重复定义导致链接器报错或选择异常。 - 不同工具链(AC5/AC6/GCC)的弱符号语法不同,迁移工程时需同步检查。
- 调试时可在回调函数入口加
__NOP()或翻转IO,便于用逻辑分析仪确认是否真正进入。
通过以上原理分析与链接脚本排查步骤,可以系统性地定位“__weak函数被重写后中断不执行”的链接期根因,避免在寄存器配置上反复试错。