__ attribute __是GNU C 编译器用于声明函数、变量或类型属性的关键字。
为什么要声明属性呢?
主要用于告诉编译器在编译程序时需要进行哪些方面的优化或代码检查。
__ attrabute __ 的用法:
在被定义的函数、变量或者类型名前/后按如下方式添加属性即可:
__attribute__ ((ATTRIBUTE))
注意:
__ attribute __后面的具体属性是被两对小括号括起来的
属性(ATTRIBUTE)有十几种,常用的有以下几种:
section:自定义段
aligned:对齐
packed:对齐
format:检查函数变参格式
weak:弱声明
alias:函数起别名
noinline:无内联
always_inline:内联函数总是展开
…
对一个变量可以添加一个或多个属性。
如果有多个属性的声明,各个属性之间用逗号隔开。
char c __attribute__((packed, algined(4))) = 4;
section属性
可执行文件主要由代码段,数据段、BSS 段构成,还可以包含一些其他自定义段。
代码段(.text):用来存放编译生成的可执行指令,比如函数定义、程序语句
数据段(.data):用来存放初始化的全局变量、初始化的静态局部变量
BSS段(.bss):用来存放未初始化的全局变量,未初始化的静态局部变量
section 属性作用:编译时,将一个函数或者变量放到指定的段
(在链接文件中会指定可执行程序中各种段的组织方式或排列顺序)
使用 __ attribute __ ((section(“xxx”))),修改段的属性
int unint_val __attribute__((section(".data")));
对齐属性
GNU C 通过attribute声明 变量或类型的对齐方式的属性有两种:
aligned 和 packed(有啥不同,且看下文讲解)
aligned 属性
显式指定变量或类型在内存中的地址对齐方式
aligned (x),参数x表示按几个字节对齐,该参数必须是 2 的幂次方,否则编译就会报错。
疑问:对齐字节数为啥必须是2的幂次方呢?
待查资料搞清楚
char c1;char c2 __attribute__((aligned(4)));
思考:为啥需要指定地址对齐方式呢?
地址对齐是为了配合计算机硬件设计,可以简化CPU和内存RAM之间的接口和硬件设计。
例如,32位系统中CPU读写RAM时,硬件只支持4字节及其整数倍数对齐的地址访问,一个机器周期可以读写4字节数据,如果把一个int型数据就放在4字节对齐的地址上,那么CPU就可以在一个机器周期内完成数据的读写操作,否则可能需要两个机器周期才能完成。可以提高计算机的整体性能
结构体对齐
结构体作为一种复杂的数据类型,编译器在给一个结构体变量分配存储空间时,不仅要考虑结构体内各个成员的对齐,还要考虑结构体整体的对齐。为了结构体各成员对齐,编译器可能会在结构体内填充一些字节。为了结构体的整体对齐,编译器可能会在结构体的末尾一些空间。
结构体成员顺序不同,所占大小有可能不同:
struct data {char a;int b;short c;};
四字节对齐:占12字节
struct data {char a;short b;int c;};四字节对齐:占8字节
显式的指定成员的对齐方式:
struct data {char a;short b __attribute__((aligned(4)));int c;};
四字节对齐:占12字节
显式的指定结构体对齐方式:
struct data {char a;short b;int c;} __attribute__((aligned(16)));
16字节对齐,末尾填充8字节:占16字节
编译器一定会按照 aligend 指定的方式对齐吗?
aligend属性声明,其实只是建议编译器按照这种大小地址对齐,但是不能超过编译器允许的最大值。编译器对每个基本的数据类型都有默认的最大边界对齐字节数,如果超过了,则编译器只能按照它规定的最大对齐字节数来对变量分配地址。
packed 属性
aligned 对齐一般会增大变量的地址,变量或结构体成员变量之间地址对齐可能会造成一定的内存空洞(内存空间浪费),而packed对齐则正好相反,一般用来减少变量间的空洞,使变量间紧挨着,指定变量或类型使用最可能小的地址对齐方式。
使用packed属性显式的声明结构体成员
struct data {char a;short b __attribute__((packed));int c __attribute__((packed));};
使用最小一字节对齐
使用packed属性显式的声明整个结构体
struct data {char a;short b;int c;}__attribute__((packed));
内核中的packed、aligned 声明
在内核源码中,我们经常看到aligned 和 packed 一起使用,即对一个变量或者类型同时使用packed 和 aligned 属性声明。这样做的好处是即避免了结构体各成员间地址对齐产生的内存空洞,又指定了整个结构体的对齐方式。
这样可以在性能(时间)和空间(内存)间取得较好的平衡
struct data {char a;short b;int c;} __attribute__((packed, aligned(8)));
format属性
format 属性用来指定变参函数的参数格式检查。
使用方法:
__attribute__((format (archetype, string-index, frist-to-check)))
示例:
void LOG(const char *fmt, ...) __attribute__((format(printf,1,2)));LOG("hello world ,i am %d ages \n", age); /* 前者表示格式字符串,后者表示所有的参数*/
属性format(printf,1,2) 有3个参数,
第一个参数pritnf 是告诉编译器,按照printf的标准来检查;
第二个参数表示LOG()函数所有的参数列表中格式字符串的位置索引,即第一个参数"hello world ,i am %d ages \n";
第三个参数是告诉编译器要检查的参数的起始位置,即从第二个参数age开始检查。
weak属性
GNU C 通过 weak 属性声明,将一个强符号,转换为弱符号。
使用方法如下:
void __attribute__((weak)) func(void);int num __attribute__((weak));
以编译器视角看,变量名/函数名仅仅是个符号而已,符号分为强符号和弱符号。
个人理解:变量/函数本质是内存中一块空间的起始地址,编译器会把符号和对应的地址做映射。
强符号:函数名,初始化的全局变量名
弱符号:未初始化的全局变量名
在工程中,对于相同的全局变量名/函数名,可以归结为以下3种场景:
强符号 + 强符号
强符号 + 弱符号
弱符号 + 弱符号
强符号和弱符号主要用来解决在程序链接过程中,出现多个同名全局变量或同名函数的冲突问题,一般遵循以下3个原则:
一山不容二虎(不能同时存在两个强符号)
强弱可以共处
体积大者胜出(可以存在两个弱符号)
一个工程中如果同时存在两个强符号,那么链接器在链接时就会报重定义错误。
一个工程中允许强符号和弱符号同时存在,比如可以定义一个初始化的全局变量和一个未初始化的全局变量,这种写法在编译时是可以编过的。
编译器对这种同名符号冲突时,在做符号决议时,一般会选择强符号,丢掉弱符号。
一个工程中存在多个弱符号时,那么编译器该选择哪个呢?谁在内存中存储空间大,就选谁。
结论:
a.一般不建议在一个工程中定义多个不同类型的同名弱符号,编译时可能会出现各种各样的问题。
b.也不能同时定义两个同名的强符号,否则会报重定义错误。我们可以使用GNU C 的扩展 weak 属性,将一个强符号转换为弱符号。
弱符号的用途
在一个源文件中引用一个编号或者函数,当编译器只看到声明,而没看到其定义时,一般编译时不会报错。在链接阶段,链接器会到其他文件中找到这些符号的定义,若未找到,则报未定义错误。
当函数被声明一个弱符号时,会有一个奇特地方:当链接器找不到这个函数的定义时,也不会报错。编译器会将这个函数名,即弱符号,设置为0或者一个特殊值。只有当程序运行时,调用到这个函数,跳转到零地址或者一个特殊的地址才会报错误,产生一个内存错误。
如果我们在使用函数前,判断这个函数地址是否为0,即可避免段错误。你会发现,即使函数未定义也可以正常编过。
弱符号的这个特性在库函数开发设计中应用十分广泛,如果在开发一个库时,基础功能已经实现,有些高级功能还未实现,那么你就可以将这些函数通过weak 属性声明转换为一个弱符号。
多模块存在依赖接口时,如果A模块依赖B模块的C接口,此时C接口还没有实现,为了不阻塞A模块代码的调试,可以先将C接口通过weak属性声明转换为一个弱符号,供A模块调试;在后期B模块实现C接口时,实现为强符号,就可以覆盖掉之前弱符号(该做法其实就是打桩)。
alias属性
alias 属性主要用来给函数定义一个别名
示例
void __f(void){printf("__f\n");}void f(void) __attribute__((alias("__f")));int main(void){f();return 0;}
在Linux 内核中你会发现alias有时候会和weak属性一起使用。如有些接口随着内核版本升级,函数接口发生了变化,我们可以通过alias属性对旧的接口名字进行封装,重新起一个接口名字。 用于版本升级后对旧版本的兼容
//f.cvoid __f(void){printf("__f\n");}void f() __attribute__((weak, alias("__f")));//main.cvoid __attribute__((weak)) f(void);void f(void){printf("f\n");}int main(){f();return 0;}
如果main.c 中定义了f()函数,那么main 函数调用f()会调用新定义的函数(强符号),否则调用__f()函数(弱符号)
noinline 和 always_inline属性
内联函数
说起内联函数,就不得不说起函数调用开销。
一个函数在执行过程中,如果要调用其他函数,则一般会执行以下过程:
保存当前函数现场。
跳到调用函数执行。
恢复当前函数现场。
继续执行当前函数。
对于一些短小精悍,并且调用频繁的函数,调用开销大,这个时候我们可以将函数声明为内联函数。
编译器遇到内联函数会像宏一样将内联函数在调用处展开,这样做就减少了函数调用的开销。
内联函数与宏
与宏相比,内联函数有以下优势:
a.参数类型检查:内联函数本质上还是一个函数,在编译过程中编译器会对齐进行参数检查,而宏不具备这个特性。
b.便于调试:函数支持的调试功能有断点、单步等。
c.返回值:内联函数有返回值。这个优势是相对于ANSI C 说的。
因为现在的宏也有返回值和类型了,如使用语句表达式定义的宏
d.接口封装:有些内联函数可以用来封装一个接口,而宏不具备这个特性。
编译器对内联函数的处理
编译器不一定会展开内联函数,编译器根据实际情况进行评估,权衡展开和不展开的利弊,并最终决定要不要展开。
内联函数缺点:
会增大程序的体积
满足以下几点的函数适合被声明为内联函数:
a.函数体积小;
b.函数体内无指针赋值、递归、循环语句等;
c.调用频繁。
当一个函数满足以上三点,应做内联展开时,可以使用static inline 关键字修饰它,但是编译器不一定会内联展开。
如果想明确告诉编译器一定要展开,或者不展开就可以使用 noinline 和 always_inline 对函数的属性做一个声明。
函数使用 noinline 或 always_inline 属性声明的形式如下:
static inline __attribute__((always_inline)) void f()
内联函数为什么定义在头文件中?
在Linux 内核中,存在大量的内联函数被定义在头文件中,而且常常使用static关键字修饰。
为什么定义在头文件中呢?
因为它是一个内联函数,可以像宏一样使用,在任何想使用内联函数的源文件中,都不必亲自在定义一遍,直接包含这个头文件即可。
为什么还要用static 修饰呢?
因为使用inline关键字定义的内联函数,编译器不一定会内联展开,那么当一个工程中多个头文件包含这个内联函数的定义时,编译时就可能报重复定义的错误。使用satic 关键字修饰,则可以限定这个函数的作用域在各自的源文件内,避免重复定义的发生。
参考wiki:
嵌入式 C 代码属性怎么定义?
C语言中头文件中的 static inline 函数以及attribute((always_inline)) 强制内联展开