几个有用的gcc attribute介绍

简介:

作者:王智通

 

当已经不能依靠算法来提高系统的性能时, 操作系统内核原理,CPU体系架构,编译器技术才能体现出它们的价值。今天来聊聊gcc的attribute语法功能, 在大家平时写的钓丝代码中基本不会出现带有attribute属性的代码片断, 通常在像linux kernel这种高质量的软件中才能见到。gcc扩展了标准c的语法,如内嵌汇编代码,还有今天的主角attribute属性语法。一个attribute可以来修饰一个函数,变量和类型,gcc的attribute内容有很多, 即使是像kernel这种复杂的代码中也没有全部用到, 所以我们只聊聊几个常见的attribute, 这些内容足以让你的钓丝代码完成逆袭。

一、align属性

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

1、修饰变量.h>.h>

#include 
#include 

int a = 0;

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

用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字节对齐的内存地址上。

2、修饰类型

#include 
#include 

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

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

struct test aa;

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

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

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

3、修饰函数

#include 
#include 

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

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

int main(void)
{

}.h>.h>
test1.c:12: error: alignment may not be specified for 'test1'

在我的rhel5.4系统,使用的是4.1.2版本的gcc, 还没有能修饰函数的功能, 但在gcc 4.3.2
文档中已经说明可以修饰函数, 它不能减少编译器默认的align数值, 只能增大, 并且还要跟linker有关, 有的linker会限制align的大小。

二、always_inline属性

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

 

#include 
#include 

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();
}.h>.h>

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

+15>+14>+9>+4>+1>+0>

(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.+15>+14>+9>+4>+1>+0>

 

 

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

static inline void test2(void) __attribute__((always_inline));
加入always_inline属性后呢?+11>+10>+9>+4>+1>+0>

(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.
(gdb)+11>+10>+9>+4>+1>+0>

这次你会看到在调用完test1后, 直接把test2的代码, 也是nop语句加入在了test函数内。
三、constructor&destructor
很犀利的2个属性,用于修饰某个函数, 经过constructor属性修饰过的函数, 可以在main函数
运行前就可以先运行完毕, 同理destructor在进程exit之前执行。.h>.h>

 

#include 
#include 

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

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

int main(void)
{
}.h>.h>

 

root@localhost.localdomain # ./test
hehe.
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平台有效。.h>.h>

.h>.h>

#include 
#include 

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);
}.h>.h>

 

+19>+18>+15>+12>+9>+6>+3>+1>+0>

 

 

(gdb) disass test1
Dump of assembler code for function test1:
0x08048354 :   push   %ebp
0x08048355 :   mov    %esp,%ebp
0x08048357 :   sub    $0x8,%esp
0x0804835a :   mov    %ecx,-0x4(%ebp)
0x0804835d :   mov    %edx,-0x8(%ebp)
0x08048360 :  mov    -0x8(%ebp),%eax
0x08048363 :  add    -0x4(%ebp),%eax
0x08048366 :  leave
0x08048367 :  ret
End of assembler dump.
+19>+18>+15>+12>+9>+6>+3>+1>+0>

可以看到sub    $0×8,%esp在堆栈里先分配了8个字节的空间。
mov    %ecx,-0×4(%ebp),将ecx的值放入第一个变量里。
mov    %edx,-0×8(%ebp),将edx的值放入第二个变量里。

说明函数的第一个参数事先已经被保存咋ecx里,第二个参数保存在edx里。  +19>+18>+15>+12>+9>+6>+3>+1>+0>

 

(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.

+19>+18>+15>+12>+9>+6>+3>+1>+0>

test2函数一样。 +10>+9>+6>+3>+1>+0>

 

(gdb) disass test3
Dump of assembler code for function test3:
0x0804837c :   push   %ebp
0x0804837d :   mov    %esp,%ebp
0x0804837f :   mov    0xc(%ebp),%eax
0x08048382 :   add    0x8(%ebp),%eax
0x08048385 :   pop    %ebp
0x08048386 :  ret
End of assembler dump.
+10>+9>+6>+3>+1>+0>

而test3则使用原始的堆栈形式来传递参数,0×8(%ebp)保存第一个参数, 0xc(%ebp)保存第二个参数。

五、packed属性

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

#include 
#include 

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));
}.h>.h>

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

root@localhost.localdomain # ./test
8, 5

 
六、section属性

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

 

#include 
#include 

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);
}.h>.h>

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

root@localhost.localdomain # readelf -S test
  [12] .text             PROGBITS         0000000000400370  00000370
       00000000000001e8  0000000000000000  AX       0     0     16
  [13] TEST              PROGBITS         0000000000400558  00000558
       0000000000000012  0000000000000000  AX       0     0     1

文件多出了一个TEST section,它的起始地址为0×400558, 大小为0×12, 它的地址范围在
0×400558 – 0x40056a。

text section的起始地址为0×400370, 大小为0x1e8, 它的地址范围在0×400370 – 0×400558。

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

 

root@localhost.localdomain # readelf -s test|grep test1
    59: 0000000000400558    18 FUNC    GLOBAL DEFAULT   13 test1
root@localhost.localdomain # readelf -s test|grep test2
    63: 0000000000400448    18 FUNC    GLOBAL DEFAULT   12 test2
root@localhost.localdomain #

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

更多关于gcc attribute的介绍请看gcc手册:
http://gcc.gnu.org/onlinedocs/gcc-4.3.2//gcc/Variable-Attributes.html#Variable-Attributes

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
3月前
|
NoSQL 编译器 开发工具
006.gcc编译器
gcc是什么?
44 0
006.gcc编译器
|
4月前
|
存储 NoSQL 算法
从一个crash问题展开,探索gcc编译优化细节
问题分析的过程也正是技术成长之路,本文以一个gcc编译优化引发的crash为切入点,逐步展开对编译器优化细节的探索之路,在分析过程中打开了新世界的大门……
413 1
|
7天前
|
C语言
转载 - gcc/ld 动态连接库和静态连接库使用方法
本文介绍了如何在GCC中实现部分程序静态链接、部分动态链接。使用`-Wl`标志传递链接器参数,`-Bstatic`强制链接静态库,`-Bdynamic`强制链接动态库。
15 0
|
1月前
|
编译器 C语言 C++
列举gcc 常见和有用的编译警告选项
列举gcc 常见和有用的编译警告选项
11 0
|
1月前
|
编译器 C语言
gcc编译警告:warning: suggest parentheses around assignment used as truth value
gcc编译警告:warning: suggest parentheses around assignment used as truth value
16 0
|
1月前
|
编译器 Linux C语言
gcc编译器的使用方法
gcc编译器的使用方法
21 1
|
2月前
|
编译器 C语言
gcc/g++语法
gcc/g++语法
|
4月前
|
C语言
gcc静态编译/usr/bin/ld: cannot find -lc
gcc静态编译/usr/bin/ld: cannot find -lc
|
5月前
|
编译器 程序员 C语言
gcc的编译过程和gcc与g++的区别
gcc的编译过程和gcc与g++的区别
53 0
|
6月前
|
C语言
编译安装gcc
编译安装gcc