閱讀244 返回首頁    go 阿裏雲 go 技術社區[雲棲]


幾個有用的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

  上一篇:go 從 JavaScript 數組去重談性能優化
  下一篇:go JAVA輕量級文件監控