STM32 HAL库中__weak函数被用户重写后仍进入默认实现的链接脚本排查
问题现象
在STM32 HAL库工程中,我们经常重写HAL_UART_RxCpltCallback、HAL_GPIO_EXTI_Callback等__weak回调函数。但有时编译链接都通过,运行时却仍然进入HAL库的默认空实现,用户代码完全没被执行。
这类问题通常不是C语言层面的重定义错误(否则编译会报multiple definition),而是链接阶段符号解析或段回收导致的。
原理:__weak与链接器符号解析
HAL库中的回调定义形如:
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
}
__weak在ARM Compiler中展开为__attribute__((weak)),在GCC中同样如此。弱符号的规则是:
- 若存在同名的强符号,链接器选择强符号;
- 若只有弱符号,则使用弱符号;
- 多个弱符号时选择其中一个(通常第一个)。
因此用户重写的强符号理应覆盖弱符号。但以下情况会破坏这一规则:
-
用户函数被放在被
--gc-sections回收的段中,链接器认为它未被引用而丢弃; -
链接脚本中
KEEP缺失,导致中断向量表或回调表引用的符号被回收; - 用户函数被编译进静态库,而链接器在解析弱符号时不会主动从库中拉取该目标文件;
- 链接顺序问题:库在用户目标文件之前被扫描,弱符号先被解析。
排查步骤
1. 查看map文件确认符号来源
在工程中开启-Wl,-Map=output.map,搜索目标函数:
HAL_UART_RxCpltCallback
观察其地址与所属目标文件。如果显示来自stm32f4xx_hal_uart.o,说明用户实现未被选中。
2. 用objdump检查符号表
arm-none-eabi-nm -C build/app.elf | grep HAL_UART_RxCpltCallback
arm-none-eabi-objdump -t build/app.elf | grep HAL_UART_RxCpltCallback
若只看到一个W(weak)符号,说明用户强符号根本没进ELF;若有T和W两个,则需看最终地址绑定。
3. 检查用户函数所在段是否被回收
arm-none-eabi-objdump -h build/app.elf | grep -E "text|rodata"
若用户函数所在.text.user_callback段不存在,说明被--gc-sections回收。
根因与解决方案
情况A:--gc-sections回收了用户函数
当用户回调仅通过函数指针(如HAL内部回调表)被引用,而链接器无法静态追踪时,会被误回收。解决方式是在链接脚本中KEEP:
SECTIONS
{
.text :
{
KEEP(*(.isr_vector))
*(.text)
*(.text*)
KEEP(*(.text.HAL_UART_RxCpltCallback))
. = ALIGN(4);
} >FLASH
}
更通用的做法是给用户回调加used属性:
__attribute__((used))
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* user code */
}
情况B:用户实现位于静态库中
若用户回调被编译进libuser.a,链接器在扫描库时若尚未遇到未定义引用,就不会拉取该目标文件。解决方法:
- 将用户回调源文件直接加入工程源文件列表(不打包成库);
- 或使用
-Wl,--whole-archive强制链接整个库; - 或在链接脚本中用
EXTERN()声明符号,强制链接器保留。
EXTERN(HAL_UART_RxCpltCallback)
情况C:链接顺序错误
确保用户目标文件在HAL库之前参与链接。在Makefile中调整:
$(TARGET).elf: $(OBJS) $(USER_OBJS) $(LIBS)
$(CC) $(OBJS) $(USER_OBJS) -Wl,--start-group $(LIBS) -Wl,--end-group -T$(LDSCRIPT) -o $@
使用--start-group/--end-group可解决库间循环依赖。
完整示例:可复现的最小工程配置
/* user_callback.c */
#include "main.h"
__attribute__((used))
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
/* 用户处理逻辑 */
HAL_UART_Transmit(&huart1, (uint8_t*)"OK\r\n", 4, 100);
}
}
链接脚本关键片段:
ENTRY(Reset_Handler)
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} >FLASH
.text :
{
*(.text)
*(.text*)
KEEP(*(.init))
KEEP(*(.fini))
. = ALIGN(4);
} >FLASH
.rodata :
{
*(.rodata)
*(.rodata*)
} >FLASH
}
编译选项:
CFLAGS += -ffunction-sections -fdata-sections
LDFLAGS += -Wl,--gc-sections -Wl,-Map=$(BUILD_DIR)/$(TARGET).map
注意事项
-
-ffunction-sections与--gc-sections配合使用时,务必对中断向量、回调表、__attribute__((used))符号做KEEP保护; - 不要仅依赖
__weak覆盖,关键回调建议加used属性; - 静态库中的弱符号覆盖极易失效,优先把用户实现放在应用目标文件中;
- 每次修改链接脚本后,用
nm和map文件双重确认符号最终绑定; - 若使用IAR/Keil,对应机制为
__weak与keep/--keep,排查思路一致。
通过以上链接脚本与符号表层面的排查,可以稳定定位并解决“重写__weak函数却仍进入默认实现”的问题。
暂无评论,来抢沙发~