0%

几个有用的gcc attribute

align属性

align属性不仅可以修饰变量,类型, 还可以修饰函数, 举个例子:

修饰变量

int a = 0;

int main(void)
{
    printf("a = %d\n", a);
}

用gdb看下变量a的地址:

(gdb) p/x &a
$1 = 0x60087c

变量a地址是编译器随意生成的, 这个例子碰巧分配到了8字节对齐的位置上。
用align属性修饰下:

int a __attribute__((aligned(8))) = 0;

(gdb) p/x &a
$1 = 0x600880

编译器会把变量a生成在8字节对齐的内存地址上。

修饰类型

int a __attribute__((aligned(8))) = 0;

struct test {
    int a;
} __attribute__((aligned(8)));

struct test aa;

int main(void)
{
    printf("a = %d\n", a);
}

经过align修饰后,struct test数据结构定义的所有变量都会出现在8字节对齐的内存上。

(gdb) p/x &aa
$1 = 0x600888

修饰函数

void test1(void) __attribute__((aligned(2)));

void  test1(void)
{
    printf("hello, world.\n");
}

int main(void)
{
    test1();
}

经过align修饰后,函数出现在8字节对齐的内存上。

(gdb) p test1 
$1 = {<text variable, no debug info>} 0x400538 <test1>

always_inline属性

gcc有个inline关键字,可以将一个函数定义为内嵌形式:

static inline void test2(void);

void test1(void)
{
    printf("hehe.\n");
}

void test2(void)
{
    asm("nop");
}

void test(void)
{
    test1();
    test2();
}

int main(void)
{
    test();
}

按照inline的定义, test在调用test2函数的时候, 会直接将test2函数的代码展开在test函数内部,
这样就减少了一次函数调用过程, 加快了代码的执行速度, 用gdb反汇编看下:

(gdb) disass test
Dump of assembler code for function test:
0x00000000004004a8 :    push   %rbp
0x00000000004004a9 :    mov    %rsp,%rbp
0x00000000004004ac :    callq  0x400498 
0x00000000004004b1 :    callq  0x4004b8 
0x00000000004004b6 :   leaveq
0x00000000004004b7 :   retq
End of assembler dump.

你可以看到,gcc并没有把test2函数展开,明明已经使用inline修饰了。
因为虽然用inline做了修饰,但是gcc会根据代码的逻辑来优化到底有没有必要使用inline代码。
像这种简单的代码,用inline代码效果不大,因此gcc并没有按照inline要求来生成代码。

static inline void test2(void) __attribute__((always_inline));
加入always_inline属性后呢?
(gdb) disass test
Dump of assembler code for function test:
0x00000000004004a8 :    push   %rbp
0x00000000004004a9 :    mov    %rsp,%rbp
0x00000000004004ac :    callq  0x400498 
0x00000000004004b1 :    nop
0x00000000004004b2 :   leaveq
0x00000000004004b3 :   retq
End of assembler dump.

这次你会看到在调用完test1后, 直接把test2的代码, 也是nop语句加入在了test函数内。

constructor & destructor

很犀利的2个属性,用于修饰某个函数,经过constructor属性修饰过的函数,
可以在main函数运行前就可以先运行完毕,同理destructor在进程exit之前执行。

#include<stdio.h>

void test1(void) __attribute__((aligned(8)));

void  test1(void)
{
    printf("hello, world.\n");
}

void __attribute__((constructor)) test2(void)
{
    printf("hehe.\n");
}

void __attribute__((destructor)) test3(void)
{
    printf("haha.\n");
}

int main(void)
{
    test1();
}

执行输出如下:

➜  /data/test/1  > ./main 
hehe.
hello, world.
haha.

fastcall & regparm属性

在c语言中,通过函数传递参数通常使用堆栈的方式,如:
test(a, b, c);
参数从右到左依次压入堆栈c, b, a。
函数执行完后,还要把这3个参数从堆栈中弹出来,如果一个函数每秒钟有上万次调用,
这将非常耗时,为了加快代码运行速度,gcc扩展了fastcall和regparm2个属性,
对于fastcall属性,一个函数的前2个参数分别通过ecx和edx来传递,剩下的则是使用堆栈来传递。
对于regparm,它的用法如下regparm(n), 函数的1到n个参数,分别通过eax, edx, ecx来传递,最多就使用3个寄存器,
其余参数通过堆栈来传递。注意这2个属性只在x86平台有效。很对编译平台fastcall默认开启。

int __attribute__((fastcall)) test1(int a, int b)
{
    return a + b;
}

int __attribute__((regparm(2))) test2(int a, int b)
{
    return a + b;
}

int test3(int a, int b)
{
    return a + b;
}

int main(void)
{
    test1(1, 2);
    test2(1, 2);
    test3(1, 2);
}

(gdb) disass test1
Dump of assembler code for function test1:
0x08048354 :   push   %ebp
0x08048355 :   mov    %esp,%ebp
0x08048357 :   sub    $0x8,%esp 在堆栈里先分配了8个字节的空间
0x0804835a :   mov    %ecx,-0x4(%ebp) 将ecx的值放入第一个变量里
0x0804835d :   mov    %edx,-0x8(%ebp) 将edx的值放入第二个变量里
0x08048360 :  mov    -0x8(%ebp),%eax
0x08048363 :  add    -0x4(%ebp),%eax
0x08048366 :  leave
0x08048367 :  ret
End of assembler dump.

(gdb) disass test2
Dump of assembler code for function test2:
0x08048368 :   push   %ebp
0x08048369 :   mov    %esp,%ebp
0x0804836b :   sub    $0x8,%esp
0x0804836e :   mov    %eax,-0x4(%ebp)
0x08048371 :   mov    %edx,-0x8(%ebp)
0x08048374 :  mov    -0x8(%ebp),%eax
0x08048377 :  add    -0x4(%ebp),%eax
0x0804837a :  leave
0x0804837b :  ret
End of assembler dump.

(gdb) disass test3
Dump of assembler code for function test3:
0x0804837c :   push   %ebp
0x0804837d :   mov    %esp,%ebp
0x0804837f :   mov    0xc(%ebp),%eax 堆栈形式来传递参数,0×c(%ebp)保存第二个参数
0x08048382 :   add    0x8(%ebp),%eax 堆栈形式来传递参数,0×8(%ebp)保存第一个参数
0x08048385 :   pop    %ebp
0x08048386 :  ret
End of assembler dump.

packed属性

用于修饰struct, union, enum数据结构,看如下的例子:

struct test {
    char a;
    int b;
};

struct test1 {
    char a;
    int b;
}__attribute__((packed));

int main(void)
{
    printf("%d, %d\n", sizeof(struct test), sizeof(struct test1));
}

struct test结构,理论来说一共有1+4=5字节的大小,但是gcc默认编译出来的大小是8,也就是说char是按照4字节来分配空间的。
加上packed修饰后,就会按照实际的类型大小来计算。

➜  /data/test/1  > ./main 
8, 5

section属性

gcc编译后的二进制文件为elf格式,代码中的函数部分会默认的链接到elf文件的text section中,
变量则会链接到bss和data section中。如果想把代码或变量放到特定的section中,就可以使用section属性来修饰。

int __attribute__((section("TEST"))) test1(int a, int b)
{
    return a + b;
}

int test2(int a, int b)
{
    return a + b;
}

int main(void)
{
    test1(1, 2);
    test2(1, 2);
}

使用readelf来观察下test二进制格式。

➜  /data/test/1  > readelf -S main
[13] .text             PROGBITS         0000000000400400  00000400
00000000000001a2  0000000000000000  AX       0     0     16
[14] TEST              PROGBITS         00000000004005a2  000005a2
0000000000000014  0000000000000000  AX       0     0     1

文件多出了一个TEST section,它的起始地址为0x4005a2, 大小为0x14, 它的地址范围在 0x4005a2 – 0x4005b6。
text section的起始地址为0x400400, 大小为0x1a2, 它的地址范围在0x400400 – 0x4005a2。

在来看下test1, test2符号表的地址:

➜  /data/test/1  > readelf -s main
57: 00000000004005a2    20 FUNC    GLOBAL DEFAULT   14 test1
68: 00000000004004f6    20 FUNC    GLOBAL DEFAULT   13 test2

可以看到test2确实被链接在text section中, 而test1链接在TEST section中。

PS

更多关于gcc attribute的介绍请看gcc手册