相信阅读嵌入式代码的老铁经常看到一些类型定义、变量、函数有 attribute 标识符,这个标识符号到底是做什么的?有哪些用法,咱们今天就来聊一聊。
attribute 可以指定编译时的细节。其可作用于变量、函数、结构体、结构体成员。
值的注意的是,Attributes 机制并不是 C标准 的一部分。因此,使用 Attributes 的程序有时候不可跨编译器移植。但是,目前市面上多数的 C/C++ IDE使用的编译工具链都是 GCC,包括很多的商业 IDE 的编译工具链也是基于 GCC 优化过的版本。因此 GNU C的__attribute__ 一般是可以兼容的,在 uboot 和 Linux 源码中会常用到此__attribute__机制。
__attribute__ ((attribute-list))
例子:
extern void die(const char *format,...) __attribute__( (noreturn) ); // 只含一个属性 noreturn
extern void die(const char *format,...) __attribute__( (noreturn, format(printf, 1, 2)) ); // 含两个属性:noreturn, format(printf, 1, 2);两个属性以逗号分隔
示例:用于版本管理,声明该函数将被丢弃,在编译时会发出提示,提醒开发人员使用最新版本的函数或变量。
int spi_cal_clock(int fapb, int hz, int duty_cycle, uint32_t* reg_o) __attribute__((deprecated));
编译时会提示:attribute_deprecated.c:33: warning: ‘ spi_cal_clock()’ is deprecated。
在 内联函数(inline 函数)详解 中我们介绍了内联函数与非内联函数的区别。attribute((always_inline)) 与 attribute ((noinline)) 均作用于编译器,在编译阶段试图强制使得对应的函数内联或者非内联。
示例:
__attribute__((always_inline)) inline int32_t add(int32_t x, int32_t y) {
return (x+y);
}
上面的函数有两个 inline。后面的 inline 可能被编译器忽略(比如 c89 不支持该关键字,C99 支持该关键字)。但前面的 attribute((always_inline)) (GCC 编译器支持的属性) 不会被忽略,否则会报错。
允许一个函数不执行到 return 语句。
static void __ubsan_default_handler(struct source_location *loc, const char *func)
{
char msg[60] = {};
(void) strlcat(msg, "Undefined behavior of type ", sizeof(msg));
(void) strlcat(msg, func + strlen("__ubsan_handle_"), sizeof(msg));
esp_system_abort(msg);
}
直接编译上述语句可能触发 warning: this function may return with or without a value。
应使用下述声明说明该函数最终不返回,因为直接触发了 abort 异常,设备会重启,因此不必返回。
static void __ubsan_default_handler(struct source_location *loc, const char *func) __attribute__((noreturn));
声明支持重载。用于c语言函数,可以定义若干个名称相同,但传递的参数不同的函数,调用时编译器会自动根据参数选择函数原型。
__attribute__((overloadable)) void print(NSString *string)
__attribute__((overloadable)) void print(Int *int)
用于设置函数别名。
int __centon()
{
printf("in %s\n",__FUNCTION__);
return 0;
}
void centon() __attribute__((alias("__centon")));//设置函数别名,原函数是__cencon, 别名是centon. 后面可以使用 centon 代替 __centon.
用于声明一个函数的指定参数不能为 null
extern void *
my_memcpy (void *dest, const void *src, size_t len)
__attribute__((nonnull (1, 2))); // 参数 dest、src 不能为 null,否则编译器会发出警告
构造属性(constructors)和析构属性(destructors)属性,可带优先级(PRIORITY)。可以作用于函数和全局变量对象(或静态变量),这里以函数为例。
带有"构造"属性的函数将在main()函数之前被执行,而声明为"析构"属性的函数则将在main()退出时执行。主要用于在 main() 函数执行前后执行很多的前处理动作或者是后处理动作。
void main_enter1() __attribute__((constructor(99)));//main_enter函数在进入main函数前调用
void main_enter2() __attribute__((constructor(100)));//main_enter函数在进入main函数前调用
void main_exit() __attribute__((destructor));//main_exit函数在main函数返回后调用
上述函数,在进入 main() 之前,先调用 main_enter1(),因为它的优先级高为 99,然后调用 main_enter2(),因为它的优先级次高 100。最后在 main() 执行后,将调用main_exit() 。
对齐属性可以用于指定内存的对齐方式。对齐值必须是 2 的整数幂。
如,int8_t foo; 可以被存储在 0x100\0x101\0x102\0x103 任意位置。
但是通过 int8_t foo attribute((aligned(4))); 指定其对齐方式为 4 bytes。然后其对齐就变成 0x100\0x104\0x108 了。
示例,请计算下面四种类型的变量的长度:
typedef struct {
int8_t var1;
int32_t var2;
int8_t var3;
} custom_type1;
typedef struct {
int8_t var1 __attribute__((aligned(4)));
int8_t var2 __attribute__((aligned(4)));
int8_t var3 __attribute__((aligned(4)));
}custom_type2;
typedef struct {
int8_t var1;
int32_t var2;
int8_t var3;
}custom_type3 __attribute__((aligned)); // 自动按字对齐,如果aligned 后面不紧跟一个指定的数字值,那么编译器将依据你的目标机器情况使用最大最有益的对齐方式。ligned 属性使被设置的对象占用更多的空间,相反的,使用packed 可以减小对象占用的空间。需要注意的是,attribute 属性的效力与你的连接器也有关,如果你的连接器最大只支持16 字节对齐,那么你此时定义32 字节对齐也是无济于事的。ligned 属性使被设置的对象占用更多的空间,相反的,使用packed 可以减小对象占用的空间。
typedef struct {
int8_t var1;
int32_t var2;
int8_t var3;
} custom_type4 __attribute__((packed)); // 不自动对齐。此时 sizeof( custom_type4) 应该是 6bytes. 使用该属性可以使得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐。使用该属性对struct或者union类型进行定义,设定其类型的每一个变量的内存约束。当用在enum类型定义时,暗示了应该使用最小完整的类型。告诉(不是强制)编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。
探究不同对齐方式下,最终结构体的大小。答案在文末。
section控制变量或函数在编译时的段名。在嵌入式软件开发时用的非常多,比如有外扩 Flash 或 RAM 时,需要将变量或函数放置到外扩存储空间,可以在链接脚本中指定段名来操作。在使用MPU(存储保护)的MCU编程时,需要对存储器划分区域,将变量或代码放置到对应的区域,通常也是通过段操作来实现。
提到section,就得说代码存放的基本区域了。在编译器编译之后,代码被划分为不同的段,RO Section(ReadOnly)中存放代码段和常量,RW Section(ReadWrite)中存放可读写静态变量和全局变量,ZI Section(ZeroInit)是存放在RW段中初始化为0的变量。
attribute((section(“section_name”))),其作用是将作用的函数或数据放入指定名为"section_name"对应的段中。
const int identifier[3] __attribute__ ((section ("ident"))) = { 1,2,3 };
void myfunction (void) __attribute__ ((section ("ext_function")))
上述代码分别在编译后,数组和函数所在的段分别为“indent”和“ext_function”而不是通常的 text 段中。
在C程序中,如果定义了一个静态函数\变量a,而没有去使用,编译时会有一个告警:‘a’ defined but not used [-Wunused-function]。
uint16_t acl_length __attribute__((unused));
__attribute__((unused)) static uint32_t get_cause(void)
{
uint32_t wakeup_cause = REG_GET_FIELD(RTC_CNTL_WAKEUP_STATE_REG, \
RTC_CNTL_WAKEUP_CAUSE);
return wakeup_cause;
}
意味着函数或变量很可能未被使用,此时编译器根据__attribute__((unused))不会针对这个函数产生警告。
也可以将其声明在函数实现中没有使用过的参数上,例如:
int main(int argc __attribute__((unused)), char **argv)
attribute((used)) 的作用是告诉编译器,我声明的这个符号是需要保留的。被used修饰以后,意味着即使函数没有被引用,在编译链接时下也不会被优化。如果不加这个修饰,那么链接器可能会去掉没有被引用的段。
若两个或两个以上全局符号名字一样,而其中之一声明为weak symbol(弱符号),另一个没有添加 weak 修饰符,则这些同名的全局符号不会引发重定义错误。当没有添加 weak 修饰符的对象存在时,链接器会忽略掉弱符号修饰的那个,如果不存在添加 weak 修饰符的对象,则使用 weak 修饰的那个对象。
感兴趣的可以参考我前述的博客:详解 C 语言中的弱符号与弱引用。
该属性只能用于带有数值类型参数的函数上。当重复调用带有数值参数的函数时,由于返回值是相同的,所以此时编译器可以进行优化处理,除第一次需要运算外, 其它只需要返回第一次的结果就可以了,进而可以提高效率。该属性主要适用于没有静态状态(static state)和副作用的一些函数,并且返回值仅仅依赖输入的参数。
__attribute__((const)) int f() {
return 1;
}
更多的__attribute__属性可以参考GCC手册,在我们需要使用到编译器一些高级特性的时候,可以在手册中查找。
custom_type1 len = 12 // 编译器按照默认自动对齐
custom_type2 len = 12 // 每个成员都 4bytes 对齐
custom_type3 len = 12 // 32 位编译器默认 4bytes 对齐
custom_type4 len = 12 // 因为我的编译器禁止这种 packed 优化,因此最终会在编译时提出警告,但仍以最优长度进行对齐
(感谢收藏与点赞,您的支持是我持续创作的动力)