幾個有用的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手冊:
https://gcc.gnu.org/onlinedocs/gcc-4.3.2//gcc/Variable-Attributes.html
最後更新:2017-04-03 07:57:03