Skip to content
Published at:

Compiler Attributes

看源码时时常会看到一些使用一些编译器提供的属性__attribute__,这里对一些常见的编译器属性做个汇总:

我们写的代码会经过编译器的处理,这就是这就意味着它可以做很多的事情,例如:

  • 参数检查
  • 指令优化
  • 指令重排
  • 插入指令
  • 修改symbol
  • ...

编译器把这些特性以类似“代码标注”的方式提供出来;比如这文章要讲的一些常见的属性__attribute__,可以用来去“标注”函数、变量和类型。这些被标注的代码,编译器处理的时候会特殊对待,根据对应的功能去处理代码。记住,它不是标准语言的一部分,是编译器提供的,不同的编译器还会有些许差异。可能出现某个特性LLVM的clang提供了,GCC没有提供,比如overloadable 类似高级语言里面的函数重载。

文档:

常见函数属性列表:

  • alias ("target"):别名
  • constructordestructor:构造、析构
  • 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")属性

给一个符号(函数/全局变量)起别名

c
#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

输出:

c
$ cc main.c && ./a.out
foo

constructor、destructor 属性

constructor可以在main函数之前调用,而destructor可以在main函数之后调用;这里的另外一个问题是return并不能结束程序,只能代表函数调用的结束;

c
#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");
}

输出:

bash
$ cc main.c && ./a.out
constructor
main
destructor

golang里面有个init函数也是类似,在main函数之前执行。

优先级constructor (priority)destructor (priority)可以加优先级去修饰函数,优先级用来控制函数调用的顺序:

c
#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");
}

输出:

bash
$ cc main.c && ./a.out
constructor 1
constructor 2
main
destructor 2
destructor 1

deprecated (msg)属性

用来标记函数过时,可携带相应信息:

c
#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 提示截图:

编译输出提示:

bash
$ 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 样式参数,这些参数应根据格式字符串进行类型检查

c
#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函数的三个参数开始检查

输出:

bash
$ cc main.c && ./a.out
some flag: hello format

noreturn属性

c
#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段

c
#include <stdio.h>

__attribute__((section("foo"))) void foobar(void) {
}

int main(int argc, char* argv[]) {
    return 0;
}

输出:程序文件会多一个foo的section(Nr列是15)

bash
$ 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中的使用示例:

c
#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 库的线程相关的函数是弱符号的:

c
// 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, …) 属性

编译器对函数参数进行检测:参数不能为空。在编译期发现问题并解决,总比在运行期发现问题在去解决好的多

c
#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;
}

输出:

bash
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)属性

内存按指定字节大小对齐

c
#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;
}

输出:

bash
$ cc main.c && ./a.out
c1: 0x10b08e000
c2: 0x10b08e010
c3: 0x10b08e011

cleanup (cleanup_function)属性

用来修饰变量,当变量超出作用域范围后,调用该函数

c
#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;
}

输出:

bash
$ 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 属性

修饰structunion,该属性可以使得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,减小对象占用的空间

c
#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;
}

输出:

bash
$ 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

函数原型:

c
void * __builtin_return_address (unsigned int level)

__builtin_return_address(0)获得函数的返回地址,即函数结束时控制流将定位到的目标地址。参数指定了该__builtin函数应该在活动记录中向上多少层。

  • 0表示当前运行函数的返回地址
  • 1表示调用当前函数的函数将返回的地址
  • etc

__builtin_frame_address 函数

函数原型:

c
void * __builtin_frame_address (unsigned int level)

和上面的__builtin_return_address函数类似,__builtin_frame_address(0)获得函数的栈帧地址,即函数结束时控制流将定位到的目标地址。函数调用的过程中,有一个栈帧的概念。函数每调用一次,都会将函数的现场(返回值、寄存器、临时变量等)保存在栈中,每一层函数调用都会将各自现场的信息保存在各自的栈中。这个栈就是当前函数的栈帧,每一个栈帧都有起始地址和结束地址,多层函数调用就会有多个栈帧,每一个栈帧都会保存上一层栈帧的起始地址,这样各个栈帧就形成了一个调用链。参数指定了该__builtin函数应该在活动记录中向上多少层。

  • 0表示当前运行函数的栈帧地址
  • 1表示上一级函数的栈帧地址
  • etc

__builtin_expect 函数

函数原型:

c
long __builtin_expect (long exp, long c)

允许程序员将最有可能执行的分支告诉编译器,来帮助编译器优化分支预测。比如,有些服务程序代码抽象后,可能是下面这样,一个死循环然后处理数据;下一问题是:如果99%的情况是true,这时候有没有办法去优化它

c
#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 改过后:

c
#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;
}

代码解释:

c
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>文件中

c
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

__builtin_unreachable 函数

表示程序执行流不应该执行到这里,执行到这里就会触发 UB;用来去检测一些程序不应该出现的情况,在这点上有点类似 assert

c
int foobar(void* ptr) {
    if (ptr == NULL) {
        __builtin_unreachable();
    }

    // do something with ptr
    return 0;
}

LLVM

和其他 GCC 特性一样,Clang 支持了 __attribute__, 还加入了一小部分扩展特性。

  • overloadable

overloadable属性

修饰函数,没错,这就是高级语言里面的函数重载。示例

c
#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;
}

输出:

bash
$ cc main.c && ./a.out
foo_with_int
foo_with_float
foo_with_double

Ref:

Updated at: