嵌入式開發,離不開 C 語言,C語言中有很多語法會直接或間接影響你代碼的質量,下面就來講講__attribute__ 關鍵字的用法。
1. 什么是 __attribute__
GNU C 編譯器增加了一個 __attribute__ 關鍵字用來聲明一個函數、變量或類型的特殊屬性。申明這些屬性主要用途就是指導編譯程序進行特定方面的優化或代碼檢查。
__attrabute__ 的用法非常簡單,當我們定義一個一個函數、變量或者類型時,直接在他名字旁邊添加如下屬性即可:
?
__attribute__?((ATTRIBUTE))
?
需要注意的是,__attribute__ 后面是兩對小括號,不能圖方便只寫一對,否則會編譯報錯。括號里的 ATTRIUBTE 表示要聲明的屬性,目前支持十幾種屬性聲明:
section:自定義段
aligned:對齊
packed:對齊
format:檢查函數變參格式
weak:弱聲明
alias:函數起別名
noinline:無內聯
always_inline:內聯函數總是展開
......
比如:
?
char?c?__attribute__((algined(8)))?=?4; int?global_val?__attribute__?((section(".data")));
?
當然,我們對一個變量也可以同時添加多個屬性。在定義變量前,各個屬性之間用逗號隔開。以下三種聲明方式是沒有問題的。
?
char?c??__attribute__((packed,?algined(4))); char?c??__attribute__((packed,?algined(4)))?=?4; __attribute__((packed,?algined(4)))?char?c?=?4;
?
2. 屬性聲明:section
section 屬性的主要作用是:在程序編譯時,將一個函數或者變量放到指定的段,即指定的section 中。
一個可執行文件注意由代碼段,數據段、BSS 段構成。代碼段主要用來存放編譯生成的可執行指令代碼、數據段和BSS段用來存放全局變量和未初始化的全局變量。
除了這三個段,可執行文件還包含一些其他的段。我們可以用 readelf 去查看一個可執行文件各個section信息。
不同的 section 及說明
section | 組成 |
---|---|
代碼段(.text) | 函數定義、程序語句 |
數據段 (.data) | 初始化的全局變量、初始化的靜態局部變量 |
BSS 段(.bss) | 未初始化的全局變量,未初始化的靜態局部變量 |
?
int?global_val?=?8; int?unint_val; int?main(void) {??? ???return?0; }
?
我們使用gcc 編譯這個程序
?
gcc?-m32?-o?a.out?gnu.c
?
查看符表號信息
?
????#readelf?-s?a.out ????Num:????Value??????????Size?Type????Bind??????Vis??????????Ndx?Name ????44:???????0804c020?????4??OBJECT???GLOBAL?DEFAULT???24?unint_val ????45:???????08049090?????4??FUNC?????GLOBAL?HIDDEN????13?__x86.get_pc_thunk.bx ????46:???????0804c010?????0??NOTYPE??WEAK???DEFAULT???23?data_start ????47:???????0804c01c?????0??NOTYPE???GLOBAL?DEFAULT???23?_edata ????48:???????080491c4?????0??FUNC?????GLOBAL?HIDDEN????14?_fini ????49:???????0804c018?????4??OBJECT???GLOBAL?DEFAULT???23?global_val ????50:???????0804c010?????0??NOTYPE??GLOBAL?DEFAULT???23?__data_start ????51:???????00000000?????0??NOTYPE??WEAK???DEFAULT??UND?__gmon_start__ ????52:???????0804c014?????0??OBJECT???GLOBAL?HIDDEN????23?__dso_handle ????53:???????0804a004?????4??OBJECT???GLOBAL?DEFAULT???15?_IO_stdin_used ????54:???????00000000?????0??FUNC?????GLOBAL?DEFAULT??UND?__libc_start_main@@GLIBC_ ????55:???????08049160????85?FUNC??????GLOBAL?DEFAULT???13?__libc_csu_init ????56:???????0804c024?????0??NOTYPE???GLOBAL?DEFAULT???24?_end ????57:???????08049080?????1??FUNC?????GLOBAL?HIDDEN????13?_dl_relocate_static_pie ????58:???????08049040????55?FUNC?????GLOBAL?DEFAULT???13?_start ????59:???????0804a000?????4??OBJECT??GLOBAL?DEFAULT???15?_fp_hw ????60:???????0804c01c?????0??NOTYPE??GLOBAL?DEFAULT???24?__bss_start ????61:???????08049152????10?FUNC?????GLOBAL?DEFAULT???13?main
?
查看 section 信息
?
#?readelf?-S?a.out
?
使用 __attribute__ ((section("xxx"))),修改段的屬性。
?
int?global_val?=?0; int?unint_val?__attribute__((section(".data"))); int?main() { ????return?0; }
?
可以看到 unint_val 這個變量,已經被編譯器放在數據段中。當然也可以自定義段的名稱。
3. 屬性聲明:aligned
GNU C 通過 __attribute__ 來聲明 aligned 和 packed 屬性,指定一個變量或類型的對齊方式。
通過 aligned 屬性,我們可以顯示地指定變量 a 在內存中的地址對齊方式。aligned 有一個參數,表示要按幾個字節對齊,使用時要注意,地址對齊的字節數必須是 2 的冪次方,否則編譯就會報錯。
3.1 地址對齊
?
#include?int?a?=?1; int?b?=?2; char?c1?=?3; char?c2?=?4; int?main() { ????printf("a?=?%p ",?&a); ????printf("b?=?%p ",?&b); ????printf("c1?=?%p ",?&c1); ????printf("c2?=?%p ",?&c2); ???? ????return?0; }
?
可以看到,char 占一個字節,c2的地址緊挨著 c1
?
a?=?0x404030 b?=?0x404034 c1?=?0x404038 c2?=?0x404039
?
使用 aligned 地址對齊
?
#include?int?a?=?1; int?b?=?2; char?c1?=?3; char?c2?\__attribute__((aligned(4)))?=?4; int?main() { ????printf("a?=?%p ",?&a); ????printf("b?=?%p ",?&b); ????printf("c1?=?%p ",?&c1); ????printf("c2?=?%p ",?&c2); ???? ????return?0; }
?
可以看到,c2 的地址是按照4字節對齊
?
a?=?0x404030 b?=?0x404034 c1?=?0x404038 c2?=?0x40403c
?
通過 aligned 屬性聲明,雖然可以顯示的指定變量地址的對齊方式,但是也會因為邊界對齊造成一定的內存空間浪費。
地址對齊的好處是,為了配合計算機硬件設計,可以簡化CPU和內存RAM之間的接口和硬件設計。
例如,一個32位的計算機操作系統,在CPU讀取內存時,硬件設計上可能只支持4字節或者4字節倍數對齊地址訪問,CPU 每次向 RAM 讀寫數據時,一個周期可以讀寫4字節。如果我們把一個int型數據就放在4字節對齊的地址上,那么CPU就可以一次性把數據讀取完畢,否則可能需要讀取兩次。
3.2 結構體對齊
結構體作為一種復雜的數據類型,編譯器在給一個結構體變量分配存儲空間時,不僅要考慮結構體內各個成員的對齊,還要考慮結構體整體的對齊。為了結構體各成員對齊,編譯器可能會在結構體內填充一些字節。為了結構體的整體對齊,編譯器可能會在結構體的末尾一些空間。
?
#include?struct?data?{ ????char?a; ????int?b; ????short?c; }; int?main() { ????struct?data?s; ????printf("size?=?%d ",?sizeof(s)); ????printf("a?=?%p ",?&s.a); ????printf("b?=?%p ",?&s.b); ????printf("c?=?%p ",?&s.c); ???? ????return?0; }
?
四字節對齊:占12字節
?
size?=?12 a?=?0xffb6c374 b?=?0xffb6c378 c?=?0xffb6c37c
?
結構體成員順序不同,所占大小有可能不同:?
?
#include?struct?data?{ ????char?a; ????short?b; ????int?c; }; int?main() { ????struct?data?s; ????printf("size?=?%d ",?sizeof(s)); ????printf("a?=?%p ",?&s.a); ????printf("b?=?%p ",?&s.b); ????printf("c?=?%p ",?&s.c); ???? ????return?0; }
?
四字節對齊:占8字節
?
size?=?8 a?=?0xffa2d9f8 b?=?0xffa2d9fa c?=?0xffa2d9fc
?
顯示的指定成員的對齊方式:
?
#include?struct?data?{ ????char?a; ????short?b?__attribute__((aligned(4))); ????int?c; }; int?main() { ????struct?data?s; ????printf("size?=?%d ",?sizeof(s)); ????printf("a?=?%p ",?&s.a); ????printf("b?=?%p ",?&s.b); ????printf("c?=?%p ",?&s.c); ???? ????return?0; }
?
四字節對齊:占12字節
?
size?=?12 a?=?0xffb6c374 b?=?0xffb6c378 c?=?0xffb6c37c
?
顯示指定結構體對齊方式:
?
#include?struct?data?{ ????char?a; ????short?b; ????int?c; }?__attribute__((aligned(16))); int?main() { ????struct?data?s; ????printf("size?=?%d ",?sizeof(s)); ????printf("a?=?%p ",?&s.a); ????printf("b?=?%p ",?&s.b); ????printf("c?=?%p ",?&s.c); ???? ????return?0; }
?
16字節對齊,末尾填充8字節:占16字節
?
size?=?16 a?=?0xffa2d9f8 b?=?0xffa2d9fa c?=?0xffa2d9fc
?
3.3 編譯器一定會按照 aligend 指定的方式對齊嗎?
我們通過這個屬性聲明,其實只是建議編譯器按照這種大小地址對齊,但是不能超過編譯器允許的最大值。一個編譯器,對每個基本的數據類型都有默認的最大邊界對齊字節數,如果超過了,則編譯器只能按照它規定的最大對齊字節數來對變量分配地址。
4. 屬性聲明:packed
aligned 屬性一般用來增大變量的地址對齊,元素之間地址對齊會造成一定的內存空洞,而packed屬性則正好相反,一般用來減少地址對齊,指定變量或類型使用最可能小的地址對齊方式。
顯示的對結構體成員使用packed
?
#include?struct?data?{ ????char?a; ????short?b?__attribute__((packed)); ????int?c?__attribute__((packed));????????????????????????????????????????????? }; int?main() { ????struct?data?s; ????printf("size?=?%d ",?sizeof(s)); ????printf("a?=?%p ",?&s.a); ????printf("b?=?%p ",?&s.b); ????printf("c?=?%p ",?&s.c); ???? ????return?0; }
?
使用最小一字節對齊
?
size?=?7 a?=?0xfff38fb9 b?=?0xfff38fba c?=?0xfff38fbc
?
對整個結構體添加packed屬性。
?
struct?data?{ ????char?a; ????short?b; ????int?c; }__attribute__((packed));
?
內核中的packed、aligned 聲明
在內核源碼中,我們經常看到aligned 和 packed 一起使用,即對一個變量或者類型同時使用packed 和 aligned 屬性聲明。這樣做的好處是即避免了結構體各成員間地址對齊產生的內存空洞,又指定了整個結構體的對齊方式。
?
struct?data?{ ????char?a; ????short?b; ????int?c; }?__attribute__((packed,?aligned(8)));
?
5. 屬性聲明:format
GNU 通過 __attribute__ 擴展的 format 屬性,來指定變參函數的參數格式檢查。
它的使用方法如下:
?
__attribute__((format?(archetype,?string-index,?frist-to-check))) void?LOG(const?char?*fmt,?...)?__attribute__((format(printf,1,2)));
?
屬性format(printf,1,2) 有3各參數,第一個參數pritnf 是告訴編譯器,按照printf的標準來檢查;第二個參數表示LOG()函數所有的參數列表中格式字符串的位置索引,第三個參數是告訴編譯器要檢查的參數的起始位置。
?
LOG("hello?world?,i?am?%d?ages? ",?age);?/*?前者表示格式字符串,后者表示所有的參數*/
?
6. 屬性聲明:weak
GNU C 通過 weak 屬性聲明,可以將一個強符號,轉換為弱符號。使用方法如下:
?
void?__attribute__((weak))?func(void); int?num?__attribute__((weak));
?
在一個程序中,無論是變量名,還是函數名,在編譯器眼里,就是一個符號而已,符號可以分為強符號和弱符號。
強符號:函數名,初始化的全局變量名
弱符號:未初始化的全局變量名。
在一個工程項目中,對于相同的全局變量名、函數名,我們一般可以歸結為以下3種場景:
強符號 + 強符號
強符號 + 弱符號
弱符號 + 弱符號
強符號和弱符號主要用來解決在程序鏈接過程中,出現多個同名全局變量、同名函數的沖突問題,一般我們遵循以下3個原則:
一山不容二虎
強弱可以共處
體積大者勝出
在一個項目中不可能同時存在兩個強符號。如果在一個多文件的項目中定義兩個同名的函數后者全局變量,那么連接器在鏈接時就會報重定義錯誤。
但是在一個工程中允許強符號和弱符號同時存在,比如可以定義一個初始化的全局變量和一個未初始化的全局變量,這種寫法在編譯時是可以編過的。
編譯器對這種同名符號沖突時,在做符號決議時,一般會選擇強符號,丟掉弱符號。
還有一種情況是,在一個工程中,當都是弱符號時,那么編譯器該選擇哪個呢?誰在內存中存儲空間大,就選誰。
變量的弱符號與強符號
?
//?func1.c? int?a?=?1; int?b; void?func(void) { ????printf("func.a?=?%d? ",?a);? ????printf("func.b?=?%d? ",?b);???????????????????????????????????????????? } //?main.c int?a; int?b?=?2; void?func(); int?main() { ????printf("main.a?=?%d ",?a);? ????printf("main.b?=?%d ",?b);? ????func(); ???? ????return?0; }
?
編譯后,程序運行結果如下。可以看出打印的都是強符號的值。
?
main.a?=?1 main.b?=?2 func.a?=?1? func.b?=?2?
?
一般不建議在一個工程中定義多個不同類型的同名弱符號,編譯時可能會出現各種各樣的問題。也不能同時定義兩個同名的強符號,否則會報重定義錯誤。我們可以使用GNU C 的擴展 weak 屬性,將一個強符號轉換為弱符號。
?
int?a?__attribute__((weak))?=?1;
?
函數的強符號與弱符號
鏈接器對于同名的函數沖突,同樣遵循相同的規則。函數名本身是一個強符號,在一個工程中定義兩個同名的函數,編譯器肯定會報重定義錯誤。但是我們可以通過weak 屬性聲明,將其中的一個函數名轉換為弱符號。
?
//func1.c int?a?__attribute__((weak))?=?1; void?func(void) { ????printf("func.a?=?%d ",?a); } //main.c int?a?=?4; void?__attribute__((weak))?func(void) { ????printf("main.a?=?%d ",?a); } int?main(void) { ???func(); ???return?0; }
?
弱符號的用途
在一個源文件中引用一個編號或者函數,當編譯器只看到聲明,而沒看到其定義時,一般編譯時不會報錯。在鏈接階段,鏈接器會到其他文件中找到這些符號的定義,若未找到,則報未定義錯誤。
當函數被聲明一個弱符號時,會有一個奇特地方:當鏈接器找不到這個函數的定義時,也不會報錯。編譯器會將這個函數名,即弱符號,設置為0或者一個特殊值。只有當程序運行時,調用到這個函數,跳轉到零地址或者一個特殊的地址才會報錯誤,產生一個內存錯誤。
如果我們在使用函數前,判斷這個函數地址是否為0,即可避免段錯誤。你會發現,即使函數未定義也可以正常編過。
弱符號的這個特性在庫函數開發設計中應用十分廣泛,如果在開發一個庫時,基礎功能已經實現,有些高級功能還未實現,那么你就可以將這些函數通過weak 屬性聲明轉換為一個弱符號。
7. 屬性聲明:alias
GNU C 擴展了一個 alias 屬性,這個屬性很簡單,主要用來給函數定義一個別名
?
void?__f(void) { ????printf("__f "); } void?f(void)?__attribute__((alias("__f"))); int?main(void) { ????f(); ????return?0; }
?
在Linux 內核中你會發現alias有時候會和weak屬性一起使用。如有些接口隨著內核版本升級,函數接口發生了變化,我們可以通過alias屬性對舊的接口名字進行封裝,重新起一個接口名字。
?
//f.c void?__f(void) { ????printf("__f "); } void?f()?__attribute__((weak,?alias("__f"))); //main.c void??__attribute__((weak))?f(void); void?f(void) { ????printf("f "); } int?main() { ????f(); ????return?0; }
?
如果我們在main.c 中定義了f()函數,那么main 函數調用f()會調用薪定義的函數,否則調用__f()函數
8. 屬性聲明:noinline 和 always_inline
8.1 什么是內聯函數
說起內聯函數,就不得不說起函數調用開銷。一個函數在執行過程中,如果要調用其他函數,則一般會執行以下過程:
保存當前函數現場。
跳到調用函數執行。
恢復當前函數現場。
繼續執行當前函數。
對于一些短小精悍,并且調用頻繁的函數,調用開銷大,這個時候我們可以將函數聲明為內聯函數。編譯器遇到內聯函數會想宏一樣將內聯函數之間在調用處展開,這樣做就減少了函數調用的開銷。
8.2 內聯函數與宏
與宏相比,內聯函數有以下優勢:
參數類型檢查:內聯函數本質上還是一個函數,在編譯過程中編譯器會對齊進行參數檢查,而宏不具備這個特性。
便于調試:函數支持的調試功能有斷點、單步等。
返回值:內聯函數有返回值。這個優勢是相對于ANSI C 說的。因為現在的宏也有返回值和類型了,如使用語句表達式定義的宏
接口封裝:有些內聯函數可以用來封裝一個接口,而宏不具備這個特性。
8.3 編譯器對內聯函數的處理
我們雖然可以通過inline 關鍵字將一個函數聲明為一個內聯函數,但是編譯器不一定會對這個函數內聯展開。編譯器也要根據實際情況進行評估,權衡展開和不展開的利弊,并最終決定要不要展開。
內聯函數并不是完美的,也有一些缺點。內聯函數會增大程序的體積。
一般而言判斷一個內聯函數是否展開,從程序員的角度主要從以下幾點出發:
函數體積小。
函數體內無指針賦值、遞歸、循環語句等。
調用頻繁。
當我們認為一個函數體積小、而且被大量調用,應做內聯展開時,就可以使用static inline 關鍵字修飾它,但是編譯器不一定會內聯展開。如果想明確告訴編譯器一定要展開,或者不展開就可以使用 noinline 和 always_inline 對函數的屬性做一個聲明
8.4 內聯函數為什么定義在頭文件中?
在Linux 內核中,你會看到大量的內聯函數被定義在頭文件中,而且常常使用static關鍵字修飾。
為什么定義在頭文件中呢?因為它是一個內聯函數,可以像宏一樣使用,在任何想使用內聯函數的源文件中,都不必親自在定義一遍,直接包含這個頭文件即可。
為什么還要用static 修飾呢?因為使用inline關鍵字定義的內聯函數,編譯器不一定會內聯展開,那么當一個工程中多個頭文件包含這個內聯函數的定義時,編譯時就可能報重復定義的錯誤。使用satic 關鍵字修飾,則可以限定這個函數的作用域在各自的源文件內,避免重復定義的發生。
9. 總結
本文主要介紹了 GNU C 的擴展語法 __attributr__ 關鍵字,并對其中常用的屬性聲明做了詳細的介紹:
section
packed
aligned
format
alias
weak
noinline
always_inline
評論
查看更多