Compiler Attributes
看源码时时常会看到一些使用一些编译器提供的属性
__attribute__
,这里对一些常见的编译器属性做个汇总:
我们写的代码会经过编译器的处理,这就是这就意味着它可以做很多的事情,例如:
- 参数检查
- 指令优化
- 指令重排
- 插入指令
- 修改symbol
- ...
编译器把这些特性以类似“代码标注”的方式提供出来;比如这文章要讲的一些常见的属性__attribute__
,可以用来去“标注”函数、变量和类型。这些被标注的代码,编译器处理的时候会特殊对待,根据对应的功能去处理代码。记住,它不是标准语言的一部分,是编译器提供的,不同的编译器还会有些许差异。可能出现某个特性LLVM的clang提供了,GCC没有提供,比如overloadable
类似高级语言里面的函数重载。
文档:
常见函数属性列表:
alias ("target")
:别名constructor
、destructor
:构造、析构deprecated (msg)
:过时、废弃format (archetype, string-index, first-to-check)
:参数格式检查format_arg (string-index)
noreturn
:函数不返回section ("section-name")
:段visibility ("visibility_type")
:可见性weakref
:弱引用weak
:弱符号nonnull(arg-index, …)
:非空
alias ("target")属性
给一个符号(函数/全局变量)起别名
#include <stdio.h>
void __f() {
printf("foo\n");
}
__attribute__((weak, alias("__f"))) void f();
int main(int argc, char* argv[]) {
f();
return 0;
}
给 f
函数起一个别名__f
;当调用 f 函数时会,则会去调用__f
输出:
$ cc main.c && ./a.out
foo
constructor、destructor 属性
constructor
可以在main函数之前调用,而destructor
可以在main函数之后调用;这里的另外一个问题是return并不能结束程序,只能代表函数调用的结束;
#include <stdio.h>
__attribute__((constructor)) void init(void) {
printf("constructor\n");
}
int main(int argc, char* argv[]) {
printf("main\n");
return 0;
}
__attribute__((destructor)) void release(void) {
printf("destructor\n");
}
输出:
$ cc main.c && ./a.out
constructor
main
destructor
golang里面有个init函数也是类似,在main函数之前执行。
优先级:constructor (priority)
和destructor (priority)
可以加优先级去修饰函数,优先级用来控制函数调用的顺序:
#include <stdio.h>
__attribute__((constructor(1))) void init(void) {
printf("constructor 1\n");
}
__attribute__((constructor(2))) void foo1(void) {
printf("constructor 2\n");
}
int main(int argc, char* argv[]) {
printf("main\n");
return 0;
}
__attribute__((destructor(1))) void release(void) {
printf("destructor 1\n");
}
__attribute__((destructor(2))) void bar1(void) {
printf("destructor 2\n");
}
输出:
$ cc main.c && ./a.out
constructor 1
constructor 2
main
destructor 2
destructor 1
deprecated (msg)属性
用来标记函数过时,可携带相应信息:
#include <stdio.h>
int old_fn() __attribute__((deprecated("for some reason")));
int old_fn();
int (*fn_ptr)() = old_fn;
int main(int argc, char* argv[]) {
old_fn();
return 0;
}
VSCode + clangd + Error Lens
提示截图:
![](/assets/image-20220830103307039_PM.0FrV6EZj.webp)
编译输出提示:
$ cc main.c && ./a.out
main.c:5:1: warning: ‘old_fn’ is deprecated: for some reason [-Wdeprecated-declarations]
5 | int (*fn_ptr)() = old_fn;
| ^~~
main.c:4:5: note: declared here
4 | int old_fn();
| ^~~~~~
main.c: In function ‘main’:
format (archetype, string-index, first-to-check)属性
format 属性指定函数采用 printf、scanf、strftime 或 strfmon 样式参数,这些参数应根据格式字符串进行类型检查。
#include <stdio.h>
#include <stdarg.h>
__attribute__((format(printf, 2, 3))) extern void my_printf(void* my_object, const char* my_format, ...) {
printf("%s: ", (char*)my_object);
va_list args;
va_start(args, my_format);
vprintf(my_format, args);
va_end(args);
}
int main(int argc, char* argv[]) {
my_printf("some flag", "hello %s\n", "format");
return 0;
}
format(printf, 2, 3)
解释:
- 把
my_printf
的函数参数用printf
一样的格式去检查, format
中的2表示:my_printf
函数第二个参数是 printf 的第一个参数格式format
中的3表示:从my_printf
函数的三个参数开始检查
输出:
$ cc main.c && ./a.out
some flag: hello format
noreturn属性
#include <stdio.h>
#include <stdlib.h>
__attribute__((noreturn)) void fatal(/* … */) {
/* … */
/* Print error message. */
/* … */
exit(1);
}
int main(int argc, char* argv[]) {
fatal();
return 0;
}
表示这个函数不会返回
section ("section-name")属性
把相应的函数放到对应的section段
#include <stdio.h>
__attribute__((section("foo"))) void foobar(void) {
}
int main(int argc, char* argv[]) {
return 0;
}
输出:程序文件会多一个foo的section(Nr列是15)
$ cc main.c
$ readelf -SW a.out
There are 30 section headers, starting at offset 0x3678:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 0000000000000318 000318 00001c 00 A 0 0 1
[ 2] .note.gnu.property NOTE 0000000000000338 000338 000030 00 A 0 0 8
[ 3] .note.gnu.build-id NOTE 0000000000000368 000368 000024 00 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000038c 00038c 000020 00 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003b0 0003b0 000024 00 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003d8 0003d8 000090 18 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000468 000468 000088 00 A 0 0 1
[ 8] .gnu.version VERSYM 00000000000004f0 0004f0 00000c 02 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000500 000500 000030 00 A 7 1 8
[10] .rela.dyn RELA 0000000000000530 000530 0000c0 18 A 6 0 8
[11] .init PROGBITS 0000000000001000 001000 00001b 00 AX 0 0 4
[12] .plt PROGBITS 0000000000001020 001020 000010 10 AX 0 0 16
[13] .plt.got PROGBITS 0000000000001030 001030 000010 10 AX 0 0 16
[14] .text PROGBITS 0000000000001040 001040 0000ff 00 AX 0 0 16
[15] foo PROGBITS 000000000000113f 00113f 00000b 00 AX 0 0 1
[16] .fini PROGBITS 000000000000114c 00114c 00000d 00 AX 0 0 4
[17] .rodata PROGBITS 0000000000002000 002000 000004 04 AM 0 0 4
[18] .eh_frame_hdr PROGBITS 0000000000002004 002004 000034 00 A 0 0 4
[19] .eh_frame PROGBITS 0000000000002038 002038 0000b4 00 A 0 0 8
[20] .init_array INIT_ARRAY 0000000000003df0 002df0 000008 08 WA 0 0 8
[21] .fini_array FINI_ARRAY 0000000000003df8 002df8 000008 08 WA 0 0 8
[22] .dynamic DYNAMIC 0000000000003e00 002e00 0001c0 10 WA 7 0 8
[23] .got PROGBITS 0000000000003fc0 002fc0 000040 08 WA 0 0 8
[24] .data PROGBITS 0000000000004000 003000 000010 00 WA 0 0 8
[25] .bss NOBITS 0000000000004010 003010 000008 00 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 003010 000026 01 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 003038 000360 18 28 18 8
[28] .strtab STRTAB 0000000000000000 003398 0001d0 00 0 0 1
[29] .shstrtab STRTAB 0000000000000000 003568 000110 00 0 0 1
visibility ("visibility_type")属性
可以用来修饰函数、变量和类型。当一个库用来给别人调用的时候,当前库对外暴露的函数、变量和类型就有可见性的问题;比如说这个想对外暴露那些接口,影藏哪些接口;一个好的最佳实践是:尽可能少地输出符号。动态库装载和识别的符号越少,程序启动和运行的速度就越快。导出所有符号会减慢程序速度,并耗用大量内存。
visibility_type
取值:
default
:在 ELF 上,默认可见性意味着声明对其他模块可见,并且在共享库中,意味着声明的实体可以被覆盖。hidden
:隐藏可见性表明声明的实体具有一种新的链接形式,我们称之为“隐藏链接”。如果两个具有隐藏链接的对象声明在同一个共享对象中,则它们引用同一个对象。internal
:内部可见性类似于隐藏可见性,但具有额外的处理器特定语义。protected
:受保护的可见性类似于默认可见性,只是它指示定义模块中的引用绑定到该模块中的定义。也就是说,声明的实体不能被另一个模块覆盖。
cJSON和dbus中的使用示例:
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
#else
#define CJSON_PUBLIC(type) type
#endif
#if defined(__GNUC__) && __GNUC__ >= 4
#define DBUS_EXPORT __attribute__ ((__visibility__ ("default")))
#else
#define DBUS_EXPORT
#endif
weak属性
对于编译器来说,(全局)变量和函数名都是一个符号,符号分为强符号和弱符号。weak可以将一个强符号转为弱符号。
- 强符号:函数名,初始化的全局变量名
- 弱符号:未初始化的全局变量名
当编译一个程序时定义了两个同名的函数(不同文件中),编译则会报错,因为他不知道去使用哪一个。这时候可以把其中的一个函数转成弱符号,则程序可以正常的编译和运行,运行时会调用强符号的函数。可以用来做兼容、做自定义实现,比如,在 SDK 中提供的是弱符号的实现函数,你可以用 SDK 中默认的实现函数;如果你想用自己的实现,则可以重新去实现这个函数(强符号),实际运行会调用你自己实现的强符号的函数。比如:实现 libc 的 musl 库的线程相关的函数是弱符号的:
// features.h
#define weak_alias(old, new) extern __typeof(old) new __attribute__((__weak__, __alias__(#old)))
// pthread_create.c
int __pthread_create(pthread_t* restrict res, const pthread_attr_t* restrict attrp, void* (*entry)(void*), void* restrict arg){
// ...
}
weak_alias(__pthread_create, pthread_create);
给__pthread_create
函数起一个别名pthread_create
;当使用pthread_create
时,实际就会去调用__pthread_create
函数。
nonnull(arg-index, …) 属性
编译器对函数参数进行检测:参数不能为空。在编译期发现问题并解决,总比在运行期发现问题在去解决好的多
#include <stdio.h>
__attribute__((nonnull(1, 2)))
int my_memcpy(void* dest, const void* src, size_t len) {
// do something
return 0;
}
int main(int argc, char* argv[]) {
my_memcpy(NULL, "foo", 3);
return 0;
}
输出:
cc demo_nonnull.c
demo_nonnull.c: In function ‘main’:
demo_nonnull.c:10:5: warning: argument 1 null where non-null expected [-Wnonnull]
10 | my_memcpy(NULL, "foo", 3);
| ^~~~~~~~~
demo_nonnull.c:4:5: note: in a call to function ‘my_memcpy’ declared ‘nonnull’
4 | int my_memcpy(void* dest, const void* src, size_t len) {
| ^~~~~~~~~
常见变量属性列表:
aligned (alignment)
cleanup (cleanup_function)
deprecated (msg)
同上section ("section-name")
同上visibility ("visibility_type")
同上weak
同上
aligned (alignment)属性
内存按指定字节大小对齐
#include <stdio.h>
char c1 = 'a';
char c2 __attribute__((aligned(16))) = 'b';
char c3 = 'c';
int main(int argc, char* argv[]) {
printf("c1: %p\n", &c1);
printf("c2: %p\n", &c2);
printf("c3: %p\n", &c3);
return 0;
}
输出:
$ cc main.c && ./a.out
c1: 0x10b08e000
c2: 0x10b08e010
c3: 0x10b08e011
cleanup (cleanup_function)属性
用来修饰变量,当变量超出作用域范围后,调用该函数
#include <stdio.h>
void func_cleanup(int* value) {
printf("func cleanup\n");
printf("value: %d\n", *value);
}
int main(int argc, char** argv) {
printf("enter scope ~~~\n");
{
int value __attribute__((__cleanup__(func_cleanup))) = 1;
value = 5;
printf("leave scope ~~~\n");
}
printf("out of scope ~~~\n");
return 0;
}
输出:
$ cc main.c && ./a.out
enter scope ~~~
leave scope ~~~
func cleanup
value: 5
out of scope ~~~
deprecated (msg) 属性(同上)
section ("section-name") 属性(同上)
visibility ("visibility_type") 属性(同上)
weak 属性(同上)
常见类型属性列表:
aligned (alignment)
deprecated (msg)
同上packed
visibility
aligned (alignment) 属性(同上)
deprecated (msg) 属性(同上)
packed 属性
修饰struct
或union
,该属性可以使得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,减小对象占用的空间
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct foo {
char a;
int x[2];
};
struct bar {
char a;
int x[2] __attribute__((packed));
};
struct my_unpacked_struct {
char c;
int i;
};
struct __attribute__((__packed__)) my_packed_struct {
char c;
int i;
struct my_unpacked_struct s;
};
int main(int argc, char* argv[]) {
printf("sizeof(char): %lu\n", sizeof(char));
printf("sizeof(int): %lu\n", sizeof(int));
printf("-----------------------\n");
printf("sizeof(struct foo): %lu\n", sizeof(struct foo));
printf("sizeof(struct bar): %lu\n", sizeof(struct bar));
printf("-----------------------\n");
printf("sizeof(struct my_unpacked_struct): %lu\n", sizeof(struct my_unpacked_struct));
printf("sizeof(struct my_packed_struct): %lu\n", sizeof(struct my_packed_struct));
return 0;
}
输出:
$ cc demo_packed.c && ./a.out
sizeof(char): 1
sizeof(int): 4
-----------------------
sizeof(struct foo): 12
sizeof(struct bar): 9
-----------------------
sizeof(struct my_unpacked_struct): 8
sizeof(struct my_packed_struct): 13
visibility 属性(同上)
__builtin内建函数
编译器提供了其他选项,可以执行C语言常规能力范围之外的操作,又不必借 助于内联汇编。
__builtin_return_address
:查看返回地址__builtin_frame_address
:查看栈帧地址__builtin_expect
:编译器分支优化__builtin_unreachable
:
__builtin_return_address 函数
文档:https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html
函数原型:
void * __builtin_return_address (unsigned int level)
__builtin_return_address(0)
获得函数的返回地址,即函数结束时控制流将定位到的目标地址。参数指定了该__builtin
函数应该在活动记录中向上多少层。
- 0表示当前运行函数的返回地址
- 1表示调用当前函数的函数将返回的地址
- etc
__builtin_frame_address 函数
函数原型:
void * __builtin_frame_address (unsigned int level)
和上面的__builtin_return_address
函数类似,__builtin_frame_address(0)
获得函数的栈帧地址,即函数结束时控制流将定位到的目标地址。函数调用的过程中,有一个栈帧的概念。函数每调用一次,都会将函数的现场(返回值、寄存器、临时变量等)保存在栈中,每一层函数调用都会将各自现场的信息保存在各自的栈中。这个栈就是当前函数的栈帧,每一个栈帧都有起始地址和结束地址,多层函数调用就会有多个栈帧,每一个栈帧都会保存上一层栈帧的起始地址,这样各个栈帧就形成了一个调用链。参数指定了该__builtin
函数应该在活动记录中向上多少层。
- 0表示当前运行函数的栈帧地址
- 1表示上一级函数的栈帧地址
- etc
__builtin_expect 函数
函数原型:
long __builtin_expect (long exp, long c)
允许程序员将最有可能执行的分支告诉编译器,来帮助编译器优化分支预测。比如,有些服务程序代码抽象后,可能是下面这样,一个死循环然后处理数据;下一问题是:如果99%的情况是true,这时候有没有办法去优化它
#include <stdio.h>
#include <stdbool.h>
static bool cond = false;
int main(int argc, char* argv[]) {
// ...
while (true) {
if (cond) {
// handle ...
} else {
// init ...
cond = true;
}
}
return 0;
}
使用__builtin_expect
改过后:
#include <stdio.h>
#include <stdbool.h>
static bool cond = false;
#define likely(x) __builtin_expect(!!(x), 1) // x很可能为真
#define unlikely(x) __builtin_expect(!!(x), 0) // x很可能为假
int main(int argc, char* argv[]) {
// ...
while (true) {
if (likely(cond)) {
// handle ...
} else {
// init ...
cond = true;
}
}
return 0;
}
代码解释:
if(likely(cond)) // 等价于 if(cond)
if(unlikely(cond)) // 等价于 if(cond)
__builtin_expect()
是 GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降。
__builtin_expect((x),1)
表示 x 的值为真的可能性更大;__builtin_expect((x),0)
表示 x 的值为假的可能性更大。
也就是说,使用likely()
,执行 if 后面的语句的机会更大,使用 unlikely()
,执行 else 后面的语句的机会更大。通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着起面的代码,从而减少指令跳转带来的性能上的下降 。
Linux内核定义了以下两个宏,来标识代码中很可能和不太可能的分支: <compiler.h>
文件中
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
__builtin_unreachable 函数
表示程序执行流不应该执行到这里,执行到这里就会触发 UB;用来去检测一些程序不应该出现的情况,在这点上有点类似 assert
。
int foobar(void* ptr) {
if (ptr == NULL) {
__builtin_unreachable();
}
// do something with ptr
return 0;
}
LLVM
和其他 GCC 特性一样,Clang 支持了 __attribute__
, 还加入了一小部分扩展特性。
overloadable
overloadable属性
修饰函数,没错,这就是高级语言里面的函数重载。示例
#include <stdio.h>
__attribute__((overloadable)) void foo(int x) {
printf("foo_with_int\n");
}
__attribute__((overloadable)) void foo(float x) {
printf("foo_with_float\n");
}
__attribute__((overloadable)) void foo(double x) {
printf("foo_with_double\n");
}
int main(int argc, char* argv[]) {
foo(10);
foo(10.1f);
foo(10.1);
return 0;
}
输出:
$ cc main.c && ./a.out
foo_with_int
foo_with_float
foo_with_double