作者:王智通
当已经不能依靠算法来提高系统的性能时, 操作系统内核原理,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