嵌入式高手都在偷偷用的“第12条”:用 __attribute__((weak)) 写出“可覆盖”的函数,让框架优雅地留出后门
2026/6/30 3:29:11 网站建设 项目流程

该文章同步至OneChan

你有没有遇到过:用了官方的 HAL 库,想在自己的代码里“拦截”某个中断回调,结果发现要么改库源码,要么只能眼巴巴看着默认的空函数被执行?

这是资深工程师压箱底的编程技巧系列第十二篇。前面我们学会了编译期状态机、毒死危险标识符、编译拦截危险调用。今天这一招,是嵌入式领域“框架设计”的灵魂技巧——让你提供的函数可以被人静默替换,而不用改你一行代码。

它就是几乎所有嵌入式编译器都支持的关键属性:

__attribute__((weak))

在很多芯片原厂提供的 HAL 库里,你会看到大量__weak修饰的空函数。很多初学者觉得“这不就是占个坑吗”,但实际上,这个“坑”挖得极其精妙。今天我们就来彻底搞懂它,并学会如何在自己的代码里用这个特性,构建出优雅、可扩展的驱动框架。


一、这东西到底是干什么用的?

简单说:__attribute__((weak))把一个函数或变量标记为“弱符号”。链接时,如果出现了另一个同名但未标记为weak的“强符号”,链接器会选择强符号,静默丢弃弱符号。

这句话信息量很大,我们拆开看:

在嵌入式开发中,这个特性最常见的应用就是中断回调的默认处理。芯片厂商的 HAL 库通常会这样写:

// 在库文件 stm32f4xx_hal_uart.c 里__weakvoidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){/* 空函数,什么都不做 */}

你作为用户,在自己的main.capp_uart.c里写一个同名的普通函数(不加weak):

voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){// 你的处理逻辑:把收到的数据放入队列UART_DataReady=1;}

编译、链接时,链接器发现两个同名函数,一个弱一个强,自动保留你的版本,丢弃库里的空版本。你的代码无缝注入到库的执行流程中,而不需要修改库源文件,也不需要注册任何函数指针。

这就是“静默覆盖”的精髓:框架提供默认行为,用户可以在不修改框架的前提下,有选择地替换关键节点。


二、上硬菜,直接看怎么用

Step 1:构建你自己的带“钩子”的驱动框架

假设你在写一个通用的传感器轮询模块,希望在数据更新时通知应用层。你可以这样设计:

// sensor_driver.c (你的驱动框架)__attribute__((weak))voidSensor_OnDataReady(floatvalue){/* 默认什么都不做,应用层可以覆盖 */}voidSensor_Update(void){floatnew_val=ADC_Read();Sensor_OnDataReady(new_val);// 调用“可能被覆盖”的回调}

现在任何使用你驱动的开发者,只需要在自己的代码文件里写:

// app_main.c (应用层)voidSensor_OnDataReady(floatvalue){if(value>THRESHOLD){Alarm_Trigger();}}

编译时,链接器会用应用层提供的强符号替换驱动里的弱符号。驱动框架完全不知道也不关心谁覆盖了它,回调就这样静默发生了。

Step 2:提供多个可覆盖的弱回调,构建完整的扩展点

一个成熟的模块通常会提供多个弱回调:

// modem.c__weakvoidModem_OnConnected(void){}__weakvoidModem_OnDisconnected(void){}__weakvoidModem_OnDataReceived(uint8_t*data,uint16_tlen){}__weakvoidModem_OnError(uint8_terror_code){}

应用层只需要覆盖它关心的那一个事件,其他的保持默认空函数:

// 我只关心收到数据时干什么voidModem_OnDataReceived(uint8_t*data,uint16_tlen){ParsePacket(data,len);}

这种方式比函数指针注册表更省 RAM(不需要存储指针),而且链接后调用是直接跳转,运行效率更高。

Step 3:弱符号也可以用于变量——提供默认配置

不仅是函数,变量也可以标记为weak。你可以提供一个默认的配置结构体,允许用户在链接时整体替换:

// config.c__attribute__((weak))structSystemConfig{intbaudrate;inttimeout_ms;}g_system_config={.baudrate=115200,.timeout_ms=500};

如果用户觉得 500ms 超时不够,可以在自己的代码里定义同名变量覆盖它:

// user_config.cstructSystemConfigg_system_config={.baudrate=115200,.timeout_ms=2000// 改成 2 秒};

注意:覆盖变量时必须类型完全一致,否则链接行为未定义。这种用法在嵌入式 SDK 中不常见,但在一些灵活的框架(如 Zephyr RTOS 的设备树配置覆盖)中有类似思想。


三、举一反三,组合技展现真正威力

1. 与链接脚本配合,实现“默认空实现”的自动收集

如果你提供了一整套弱回调函数,想让用户通过链接脚本的KEEPPROVIDE来组织它们,可以在链接脚本中为弱符号所在段做特殊处理。虽然 weak 本身不依赖段,但你可以把弱函数统一放到一个特殊的输入段,然后在链接脚本中PROVIDE一个默认值。这可以让你的弱函数同时支持“覆盖”和“未覆盖时使用完全不同的策略”。

2. 利用弱符号实现“可选功能模块”

假如你的固件支持两种显示屏:OLED 和 TFT,但用户只选一种编译进项目。你可以这样设计:

// display_core.c__weakvoidDisplay_Init(void){// 默认不初始化任何屏幕}__weakvoidDisplay_DrawPixel(intx,inty,uint16_tcolor){}

然后在oled.c中提供强实现:

voidDisplay_Init(void){OLED_Init();}voidDisplay_DrawPixel(intx,inty,uint16_tcolor){OLED_DrawPixel(x,y,color);}

应用层代码只调用Display_Init(),链接时自动绑定到实际存在的模块。如果用户忘了链接任何屏幕驱动,弱符号保证链接不会报“未定义符号”错误,程序能继续运行(只是屏幕不亮)。这是非常优雅的“可插拔架构”。

3. 弱符号 +__attribute__((used))防止被优化掉

弱函数如果从未被调用,且链接器开启了--gc-sections,可能会被垃圾回收掉。如果你希望弱函数本身总保留(以便调试或作为占位符),可以加上__attribute__((used))

__attribute__((weak,used))voidDefaultHandler(void){while(1);}

这在 Cortex-M 中断向量表的默认处理器中非常常见。

4. 注意:弱符号不能内联

如果你把弱函数写在.h文件并标记为static inline __attribute__((weak)),行为是未定义的。弱符号必须拥有全局作用域和外部链接属性,所以弱函数必须放在.c文件中,并在.h中用extern声明。这是很多新手踩过的坑。


四、留两个问题给你思考

现在请你停下来,思考这两个实际设计问题:

  1. 如果应用层定义了两个同名的强符号函数(比如在两个不同的.c文件里都写了void HAL_UART_RxCpltCallback),链接器会怎么办?弱符号此时还能起作用吗?
  2. 如果你想在弱函数里调用用户可能覆盖的另一个弱函数,例如Modem_OnConnected()里调用Log_Print("Connected"),而Log_Print本身也是弱符号,这样的设计有什么风险?应该怎么规避?

思考清楚了,你在设计可扩展框架时就能避开那些隐藏很深的链接陷阱。


五、总结与思考题回答

核心总结:


思考题回答

问题1:多个强符号同名会怎样?

这是典型的符号重定义错误。链接器发现多个同名的强符号时,不知道选哪一个,会直接报错(multiple definition of ...)。此时弱符号已经完全被忽略——因为连两个强符号之间都无法抉择,更轮不到弱符号。这要求框架设计者必须在文档中明确告知用户:每个弱钩子只能在一个地方覆盖。如果用户的代码是模块化开发的,建议使用统一的user_hooks.c集中管理所有覆盖实现,或者通过条件编译宏来控制。

问题2:弱函数调用弱函数有什么风险?

风险在于调用链的不确定性。如果一个弱函数内部调用了另一个弱函数,而用户只覆盖了其中一个,默认行为可能不符合预期。更糟的是,如果用户覆盖了被调用的弱函数,那么第一个弱函数的行为会间接改变。这会破坏封装性。规避方法:


好了,第 12 招我们就彻底吃透了。下次设计驱动框架时,别再让用户去修改你的库源码了,用__attribute__((weak))优雅地给他们留一个“后门”。

如果今天的内容让你对“弱符号”三个字有了全新的认识,欢迎转发和点赞。下一篇我们继续挖:__attribute__((alias))为函数创建同名弱别名或兼容别名。咱们不见不散!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询