__attribute__ 是 GCC 编译器提供的一套**"给编译器看的注释"**,让你能精确控制:
-
代码怎么生成
-
变量怎么放
-
函数怎么调用
-
链接怎么处理
在嵌入式底层开发中几乎是必备技能。
基本语法:
__attribute__((ATTRIBUTE_NAME))
// 或带参数
__attribute__((ATTRIBUTE_NAME(parameter)))
第一类:内存布局控制
| 命令 | 作用 | 典型场景 |
|---|---|---|
packed |
取消结构体对齐填充,紧凑排列 | 协议帧解析、EEPROM存储结构 |
aligned(n) |
强制按 n 字节对齐 | DMA缓冲区、Cache对齐 |
section("name") |
放入指定链接段 | 自定义内存布局、Bootloader跳转表 |
packed 示例:
cpp
struct __attribute__((packed)) ModbusFrame {
uint8_t slave_addr; // offset 0
uint8_t func_code; // offset 1
uint16_t reg_addr; // offset 2
uint16_t reg_count; // offset 4
};
// sizeof = 6 字节,不加 packed 会变成 8 或 12 字节
aligned 示例:
cpp
uint8_t __attribute__((aligned(32))) dma_buf[1024];
// 保证 DMA 缓冲区首地址是 32 字节对齐
section 示例:
cpp
const char __attribute__((section(".version"))) fw_ver[] = "V1.2.3";
// 把版本字符串放到 .version 段,配合链接脚本固定地址
第二类:链接与符号控制
| 命令 | 作用 | 典型场景 |
|---|---|---|
weak |
声明弱符号,强定义可覆盖 | SDK默认实现、中断默认处理 |
alias("target") |
给函数/变量取别名 | API版本兼容 |
used |
强制保留符号,不被优化掉 | 中断向量表引用的函数 |
unused |
抑制未使用警告(确实没调用) | 预留接口、形参占位 |
weak 示例:
cpp
// SDK 提供默认实现
__attribute__((weak)) void APP_IdleHook(void) {
// 默认空实现
}
// 用户应用里可以覆盖
void APP_IdleHook(void) {
// 用户自己的低功耗处理
}
alias 示例:
cpp
void new_i2c_write(uint8_t addr, uint8_t *data, uint32_t len) {
// 真实实现
}
// 旧函数名作为别名,兼容老代码
void __attribute__((alias("new_i2c_write"))) i2c_send(uint8_t addr, uint8_t *data, uint32_t len);
used 示例:
cpp
// 强制保留,即使编译器认为没被调用
void __attribute__((used)) HardFault_Handler(void) {
while(1);
}
// 中断向量表通过地址引用它,编译器看不到调用关系
unused 示例:
cpp
// 告诉编译器:这个参数是故意不用的,不要报警告
void callback(void __attribute__((unused)) *arg) {
// 接口要求有这个参数,但当前实现用不上
}
第三类:优化控制
| 命令 | 作用 | 典型场景 |
|---|---|---|
always_inline |
强制内联展开 | 高频调用的微小函数 |
noinline |
禁止内联 | 调试时需要保留调用栈 |
optimize("O0") |
指定优化等级 | 特定函数不开优化,方便调试 |
always_inline 示例:
cpp
// 强制内联:GPIO 位操作(高频调用,消除函数调用开销)
static inline __attribute__((always_inline)) void gpio_toggle(void) {
GPIOA->ODR ^= 1 << 5;
}
这里同时使用了inline 和 always_inline
1.C语言标准(C99/C11)把 inline 定义为"建议"------告诉编译器"这个函数很短,调用开销大,建议你在这里展开"。
编译器可以完全无视它,尤其在以下情况:
-
你开了
-O0(不优化)------编译器根本懒得展开。 -
函数体太大(递归、循环等)------编译器认为展开后代码臃肿,自动拒绝。
-
为了优化代码尺寸(
-Os),编译器宁愿保持函数调用以节省 Flash 空间。
所以,如果只写 static inline,在调试模式(-O0)下,gpio_toggle 依然会被编译成 "调用+返回" 指令,存在函数调用开销。
__attribute__((always_inline))是 GCC 的扩展属性,它明确强制编译器进行内联操作。GCC 会无视优化等级,在调用处直接展开函数体。
- 那为什么还要同时写
static inline?
__attribute__ 是 GCC 专属 的。如果你换用 Keil(ARMCC) 或 IAR ,它们不认识这个语法,直接编译报错。而 static inline 是 C 标准语法,所有 C 编译器都认识。在非 GCC 环境下,它至少还能作为"建议"生效(或正常编译通过)。
noinline 示例:
cpp
// 强制不内联:中断处理,调试时需要看完整调用栈
void __attribute__((noinline)) HardFault_Handler(void) {
save_context();
}
optimize("O0") 示例:
cpp
// 调试专用:强制关优化,方便在调试器中逐行观察变量
void __attribute__((optimize("O0"))) debug_i2c_read(void) {
uint32_t timeout = 0xFFFF; // O2 下可能被优化掉,调试器看不到
volatile uint8_t status = 0;
while (timeout--) {
status = I2C_GetFlag(); // 可以逐行打断点
if (status == READY) break;
}
}
// 调试完必须删除此属性,否则 Release 版本性能严重下降
第四类:生命周期控制
| 命令 | 作用 | 典型场景 |
|---|---|---|
constructor |
main 之前自动执行 |
早期硬件初始化 |
destructor |
main 退出后自动执行 |
资源释放、数据存盘 |
deprecated |
标记已废弃,编译时警告 | API平滑升级 |
constructor 示例:
cpp
// main 之前自动执行,用于早期硬件初始化
void __attribute__((constructor)) system_pre_init(void) {
SystemClock_Config();
HAL_Init();
}
destructor 示例:
cpp
// main 退出后执行(嵌入式 main 几乎不退出,实际使用极少)
void __attribute__((destructor)) system_cleanup(void) {
save_data_to_flash();
}
// 嵌入式环境更推荐用 atexit(),但同样极少用
deprecated 示例:
cpp
// 标记旧接口已废弃,编译时产生警告
void __attribute__((deprecated("请改用 new_api()"))) old_api(void) {
// 仍有实现,但编译会报警告
}