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手册