嵌入式開發(fā),離不開 C 語言,C語言中有很多語法會直接或間接影響你代碼的質(zhì)量,下面就來講講__attribute__ 關(guān)鍵字的用法。
1. 什么是 __attribute__
GNU C 編譯器增加了一個 __attribute__ 關(guān)鍵字用來聲明一個函數(shù)、變量或類型的特殊屬性。申明這些屬性主要用途就是指導(dǎo)編譯程序進(jìn)行特定方面的優(yōu)化或代碼檢查。
__attrabute__ 的用法非常簡單,當(dāng)我們定義一個一個函數(shù)、變量或者類型時,直接在他名字旁邊添加如下屬性即可:
__attribute__((ATTRIBUTE))
需要注意的是,__attribute__ 后面是兩對小括號,不能圖方便只寫一對,否則會編譯報錯。括號里的 ATTRIUBTE 表示要聲明的屬性,目前支持十幾種屬性聲明:
section:自定義段
aligned:對齊
packed:對齊
format:檢查函數(shù)變參格式
weak:弱聲明
alias:函數(shù)起別名
noinline:無內(nèi)聯(lián)
always_inline:內(nèi)聯(lián)函數(shù)總是展開
......
比如:
charc __attribute__((algined(8)))=4; intglobal_val __attribute__((section(".data")));
當(dāng)然,我們對一個變量也可以同時添加多個屬性。在定義變量前,各個屬性之間用逗號隔開。以下三種聲明方式是沒有問題的。
charc__attribute__((packed,algined(4))); charc__attribute__((packed,algined(4)))=4; __attribute__((packed,algined(4)))charc=4;
2. 屬性聲明:section
section 屬性的主要作用是:在程序編譯時,將一個函數(shù)或者變量放到指定的段,即指定的section 中。
一個可執(zhí)行文件注意由代碼段,數(shù)據(jù)段、BSS 段構(gòu)成。代碼段主要用來存放編譯生成的可執(zhí)行指令代碼、數(shù)據(jù)段和BSS段用來存放全局變量和未初始化的全局變量。
除了這三個段,可執(zhí)行文件還包含一些其他的段。我們可以用 readelf 去查看一個可執(zhí)行文件各個section信息。
下表是不同的 section 及說明:
section | 組成 |
---|---|
代碼段(.text) | 函數(shù)定義、程序語句 |
數(shù)據(jù)段 (.data) | 初始化的全局變量、初始化的靜態(tài)局部變量 |
BSS 段(.bss) | 未初始化的全局變量,未初始化的靜態(tài)局部變量 |
intglobal_val=8; intunint_val; intmain(void) { return0; }
我們使用gcc 編譯這個程序:
gcc-m32-o a.out gnu.c
查看符表號信息:
#readelf-s a.out Num:ValueSize TypeBindVisNdx Name 44:0804c0204OBJECTGLOBAL DEFAULT24unint_val 45:080490904FUNCGLOBAL HIDDEN13__x86.get_pc_thunk.bx 46:0804c0100NOTYPEWEAKDEFAULT23data_start 47:0804c01c0NOTYPEGLOBAL DEFAULT23_edata 48:080491c40FUNCGLOBAL HIDDEN14_fini 49:0804c0184OBJECTGLOBAL DEFAULT23global_val 50:0804c0100NOTYPEGLOBAL DEFAULT23__data_start 51:000000000NOTYPEWEAKDEFAULTUND __gmon_start__ 52:0804c0140OBJECTGLOBAL HIDDEN23__dso_handle 53:0804a0044OBJECTGLOBAL DEFAULT15_IO_stdin_used 54:000000000FUNCGLOBAL DEFAULTUND __libc_start_main@@GLIBC_ 55:0804916085FUNCGLOBAL DEFAULT13__libc_csu_init 56:0804c0240NOTYPEGLOBAL DEFAULT24_end 57:080490801FUNCGLOBAL HIDDEN13_dl_relocate_static_pie 58:0804904055FUNCGLOBAL DEFAULT13_start 59:0804a0004OBJECTGLOBAL DEFAULT15_fp_hw 60:0804c01c0NOTYPEGLOBAL DEFAULT24__bss_start 61:0804915210FUNCGLOBAL DEFAULT13main
查看 section 信息:
#readelf-S a.out
使用 __attribute__ ((section("xxx"))),修改段的屬性。
intglobal_val=0; intunint_val __attribute__((section(".data"))); intmain() { return0; }
可以看到 unint_val 這個變量,已經(jīng)被編譯器放在數(shù)據(jù)段中。當(dāng)然也可以自定義段的名稱。
3. 屬性聲明:aligned
GNU C 通過 __attribute__ 來聲明 aligned 和 packed 屬性,指定一個變量或類型的對齊方式。
通過 aligned 屬性,我們可以顯示地指定變量 a 在內(nèi)存中的地址對齊方式。aligned 有一個參數(shù),表示要按幾個字節(jié)對齊,使用時要注意,地址對齊的字節(jié)數(shù)必須是 2 的冪次方,否則編譯就會報錯。
3.1 地址對齊
#includeinta=1; intb=2; charc1=3; charc2=4; intmain() { printf("a=%p ",&a); printf("b=%p ",&b); printf("c1=%p ",&c1); printf("c2=%p ",&c2); return0; }
可以看到,char 占一個字節(jié),c2的地址緊挨著 c1
a=0x404030 b=0x404034 c1=0x404038 c2=0x404039
使用 aligned 地址對齊
#includeinta=1; intb=2; charc1=3; charc2\__attribute__((aligned(4)))=4; intmain() { printf("a=%p ",&a); printf("b=%p ",&b); printf("c1=%p ",&c1); printf("c2=%p ",&c2); return0; }
可以看到,c2 的地址是按照4字節(jié)對齊
a=0x404030 b=0x404034 c1=0x404038 c2=0x40403c
通過 aligned 屬性聲明,雖然可以顯示的指定變量地址的對齊方式,但是也會因為邊界對齊造成一定的內(nèi)存空間浪費(fèi)。
地址對齊的好處是,為了配合計算機(jī)硬件設(shè)計,可以簡化CPU和內(nèi)存RAM之間的接口和硬件設(shè)計。
例如,一個32位的計算機(jī)操作系統(tǒng),在CPU讀取內(nèi)存時,硬件設(shè)計上可能只支持4字節(jié)或者4字節(jié)倍數(shù)對齊地址訪問,CPU 每次向 RAM 讀寫數(shù)據(jù)時,一個周期可以讀寫4字節(jié)。如果我們把一個int型數(shù)據(jù)就放在4字節(jié)對齊的地址上,那么CPU就可以一次性把數(shù)據(jù)讀取完畢,否則可能需要讀取兩次。
3.2 結(jié)構(gòu)體對齊
結(jié)構(gòu)體作為一種復(fù)雜的數(shù)據(jù)類型,編譯器在給一個結(jié)構(gòu)體變量分配存儲空間時,不僅要考慮結(jié)構(gòu)體內(nèi)各個成員的對齊,還要考慮結(jié)構(gòu)體整體的對齊。
為了結(jié)構(gòu)體各成員對齊,編譯器可能會在結(jié)構(gòu)體內(nèi)填充一些字節(jié)。為了結(jié)構(gòu)體的整體對齊,編譯器可能會在結(jié)構(gòu)體的末尾一些空間。
#includestructdata{ chara; intb; short c; }; intmain() { structdatas; printf("size=%d ",sizeof(s)); printf("a=%p ",&s.a); printf("b=%p ",&s.b); printf("c=%p ",&s.c); return0; }
四字節(jié)對齊:占12字節(jié)
size=12 a=0xffb6c374 b=0xffb6c378 c=0xffb6c37c
結(jié)構(gòu)體成員順序不同,所占大小有可能不同:
#includestructdata{ chara; short b; intc; }; intmain() { structdatas; printf("size=%d ",sizeof(s)); printf("a=%p ",&s.a); printf("b=%p ",&s.b); printf("c=%p ",&s.c); return0; }
四字節(jié)對齊:占8字節(jié)
size=8 a=0xffa2d9f8 b=0xffa2d9fa c=0xffa2d9fc
顯示的指定成員的對齊方式:
#includestructdata{ chara; short b __attribute__((aligned(4))); intc; }; intmain() { structdatas; printf("size=%d ",sizeof(s)); printf("a=%p ",&s.a); printf("b=%p ",&s.b); printf("c=%p ",&s.c); return0; }
四字節(jié)對齊:占12字節(jié)
size=12 a=0xffb6c374 b=0xffb6c378 c=0xffb6c37c
顯示指定結(jié)構(gòu)體對齊方式:
#includestructdata{ chara; short b; intc; }__attribute__((aligned(16))); intmain() { structdatas; printf("size=%d ",sizeof(s)); printf("a=%p ",&s.a); printf("b=%p ",&s.b); printf("c=%p ",&s.c); return0; }
16字節(jié)對齊,末尾填充8字節(jié):占16字節(jié)
size=16 a=0xffa2d9f8 b=0xffa2d9fa c=0xffa2d9fc
3.3 編譯器一定會按照 aligend 指定的方式對齊嗎?
我們通過這個屬性聲明,其實只是建議編譯器按照這種大小地址對齊,但不能超過編譯器允許的最大值。一個編譯器,對每個基本的數(shù)據(jù)類型都有默認(rèn)的最大邊界對齊字節(jié)數(shù),如果超過了,則編譯器只能按照它規(guī)定的最大對齊字節(jié)數(shù)來對變量分配地址。
4. 屬性聲明:packed
aligned 屬性一般用來增大變量的地址對齊,元素之間地址對齊會造成一定的內(nèi)存空洞,而packed屬性則正好相反,一般用來減少地址對齊,指定變量或類型使用最可能小的地址對齊方式。
顯示的對結(jié)構(gòu)體成員使用packed
#includestructdata{ chara; short b __attribute__((packed)); intc __attribute__((packed)); }; intmain() { structdatas; printf("size=%d ",sizeof(s)); printf("a=%p ",&s.a); printf("b=%p ",&s.b); printf("c=%p ",&s.c); return0; }
使用最小一字節(jié)對齊:
size=7 a=0xfff38fb9 b=0xfff38fba c=0xfff38fbc
對整個結(jié)構(gòu)體添加packed屬性
structdata{ chara; short b; intc; }__attribute__((packed));
內(nèi)核中的packed、aligned 聲明
在內(nèi)核源碼中,我們經(jīng)??吹絘ligned 和 packed 一起使用,即對一個變量或者類型同時使用packed 和 aligned 屬性聲明。這樣做的好處是即避免了結(jié)構(gòu)體各成員間地址對齊產(chǎn)生的內(nèi)存空洞,又指定了整個結(jié)構(gòu)體的對齊方式。
structdata{ chara; short b; intc; }__attribute__((packed,aligned(8)));
5. 屬性聲明:format
GNU 通過 __attribute__ 擴(kuò)展的 format 屬性,來指定變參函數(shù)的參數(shù)格式檢查。
它的使用方法如下:
__attribute__((format(archetype,string-index,frist-to-check))) voidLOG(constchar*fmt,...)__attribute__((format(printf,1,2)));
屬性format(printf,1,2) 有3各參數(shù),第一個參數(shù)pritnf 是告訴編譯器,按照printf的標(biāo)準(zhǔn)來檢查;第二個參數(shù)表示LOG()函數(shù)所有的參數(shù)列表中格式字符串的位置索引,第三個參數(shù)是告訴編譯器要檢查的參數(shù)的起始位置。
LOG("hello world,i am%d ages ",age);/*前者表示格式字符串,后者表示所有的參數(shù)*/
6. 屬性聲明:weak
GNU C 通過 weak 屬性聲明,可以將一個強(qiáng)符號,轉(zhuǎn)換為弱符號。使用方法如下:
void__attribute__((weak))func(void); intnum __attribute__((weak));
在一個程序中,無論是變量名,還是函數(shù)名,在編譯器眼里,就是一個符號而已,符號可以分為強(qiáng)符號和弱符號。
強(qiáng)符號:函數(shù)名,初始化的全局變量名
弱符號:未初始化的全局變量名。
在一個工程項目中,對于相同的全局變量名、函數(shù)名,我們一般可以歸結(jié)為以下3種場景:
強(qiáng)符號 + 強(qiáng)符號
強(qiáng)符號 + 弱符號
弱符號 + 弱符號
強(qiáng)符號和弱符號主要用來解決在程序鏈接過程中,出現(xiàn)多個同名全局變量、同名函數(shù)的沖突問題,一般我們遵循以下3個原則:
一山不容二虎
強(qiáng)弱可以共處
體積大者勝出
在一個項目中,不可能同時存在兩個強(qiáng)符號。如果在一個多文件的項目中定義兩個同名的函數(shù)后者全局變量,那么連接器在鏈接時就會報重定義錯誤。
但是,在一個工程中允許強(qiáng)符號和弱符號同時存在,比如可以定義一個初始化的全局變量和一個未初始化的全局變量,這種寫法在編譯時是可以編過的。
編譯器對這種同名符號沖突時,在做符號決議時,一般會選擇強(qiáng)符號,丟掉弱符號。
還有一種情況是,在一個工程中,當(dāng)都是弱符號時,那么編譯器該選擇哪個呢?誰在內(nèi)存中存儲空間大,就選誰。
變量的弱符號與強(qiáng)符號
//func1.c inta=1; intb; voidfunc(void) { printf("func.a=%d ",a); printf("func.b=%d ",b); } //main.c inta; intb=2; voidfunc(); intmain() { printf("main.a=%d ",a); printf("main.b=%d ",b); func(); return0; }
編譯后,程序運(yùn)行結(jié)果如下??梢钥闯龃蛴〉亩际菑?qiáng)符號的值。
main.a=1 main.b=2 func.a=1 func.b=2
一般不建議在一個工程中定義多個不同類型的同名弱符號,編譯時可能會出現(xiàn)各種各樣的問題。也不能同時定義兩個同名的強(qiáng)符號,否則會報重定義錯誤。我們可以使用GNU C 的擴(kuò)展 weak 屬性,將一個強(qiáng)符號轉(zhuǎn)換為弱符號。
inta __attribute__((weak))=1;
函數(shù)的強(qiáng)符號與弱符號
鏈接器對于同名的函數(shù)沖突,同樣遵循相同的規(guī)則。函數(shù)名本身是一個強(qiáng)符號,在一個工程中定義兩個同名的函數(shù),編譯器肯定會報重定義錯誤。但是,我們可以通過weak 屬性聲明,將其中的一個函數(shù)名轉(zhuǎn)換為弱符號。
//func1.c inta __attribute__((weak))=1; voidfunc(void) { printf("func.a=%d ",a); } //main.c inta=4; void__attribute__((weak))func(void) { printf("main.a=%d ",a); } intmain(void) { func(); return0; }
弱符號的用途
在一個源文件中引用一個編號或者函數(shù),當(dāng)編譯器只看到聲明,而沒看到其定義時,一般編譯時不會報錯。在鏈接階段,鏈接器會到其他文件中找到這些符號的定義,若未找到,則報未定義錯誤。
當(dāng)函數(shù)被聲明一個弱符號時,會有一個奇特地方:當(dāng)鏈接器找不到這個函數(shù)的定義時,也不會報錯。編譯器會將這個函數(shù)名,即弱符號,設(shè)置為0或者一個特殊值。只有當(dāng)程序運(yùn)行時,調(diào)用到這個函數(shù),跳轉(zhuǎn)到零地址或者一個特殊的地址才會報錯誤,產(chǎn)生一個內(nèi)存錯誤。
如果我們在使用函數(shù)前,判斷這個函數(shù)地址是否為0,即可避免段錯誤。你會發(fā)現(xiàn),即使函數(shù)未定義也可以正常編過。
弱符號的這個特性在庫函數(shù)開發(fā)設(shè)計中應(yīng)用十分廣泛,如果在開發(fā)一個庫時,基礎(chǔ)功能已經(jīng)實現(xiàn),有些高級功能還未實現(xiàn),那么你就可以將這些函數(shù)通過weak 屬性聲明轉(zhuǎn)換為一個弱符號。
7. 屬性聲明:alias
GNU C 擴(kuò)展了一個 alias 屬性,這個屬性很簡單,主要用來給函數(shù)定義一個別名。
void__f(void) { printf("__f "); } voidf(void)__attribute__((alias("__f"))); intmain(void) { f(); return0; }
在Linux 內(nèi)核中你會發(fā)現(xiàn)alias有時候會和weak屬性一起使用。如有些接口隨著內(nèi)核版本升級,函數(shù)接口發(fā)生了變化,我們可以通過alias屬性對舊的接口名字進(jìn)行封裝,重新起一個接口名字。
//f.c void__f(void) { printf("__f "); } voidf()__attribute__((weak,alias("__f"))); //main.c void__attribute__((weak))f(void); voidf(void) { printf("f "); } intmain() { f(); return0; }
如果我們在main.c 中定義了f()函數(shù),那么main 函數(shù)調(diào)用f()會調(diào)用薪定義的函數(shù),否則調(diào)用__f()函數(shù)。
8. 屬性聲明:noinline 和 always_inline
8.1 什么是內(nèi)聯(lián)函數(shù)
說起內(nèi)聯(lián)函數(shù),就不得不說起函數(shù)調(diào)用開銷。一個函數(shù)在執(zhí)行過程中,如果要調(diào)用其他函數(shù),則一般會執(zhí)行以下過程:
保存當(dāng)前函數(shù)現(xiàn)場
跳到調(diào)用函數(shù)執(zhí)行
恢復(fù)當(dāng)前函數(shù)現(xiàn)場
繼續(xù)執(zhí)行當(dāng)前函數(shù)
對于一些短小精悍,并且調(diào)用頻繁的函數(shù),調(diào)用開銷大,這個時候我們可以將函數(shù)聲明為內(nèi)聯(lián)函數(shù)。編譯器遇到內(nèi)聯(lián)函數(shù)會想宏一樣將內(nèi)聯(lián)函數(shù)之間在調(diào)用處展開,這樣做就減少了函數(shù)調(diào)用的開銷。
8.2 內(nèi)聯(lián)函數(shù)與宏
與宏相比,內(nèi)聯(lián)函數(shù)有以下優(yōu)勢:
參數(shù)類型檢查:內(nèi)聯(lián)函數(shù)本質(zhì)上還是一個函數(shù),在編譯過程中編譯器會對齊進(jìn)行參數(shù)檢查,而宏不具備這個特性。
便于調(diào)試:函數(shù)支持的調(diào)試功能有斷點、單步等。
返回值:內(nèi)聯(lián)函數(shù)有返回值。這個優(yōu)勢是相對于ANSI C 說的。因為現(xiàn)在的宏也有返回值和類型了,如使用語句表達(dá)式定義的宏。
接口封裝:有些內(nèi)聯(lián)函數(shù)可以用來封裝一個接口,而宏不具備這個特性。
8.3 編譯器對內(nèi)聯(lián)函數(shù)的處理
我們雖然可以通過inline 關(guān)鍵字將一個函數(shù)聲明為一個內(nèi)聯(lián)函數(shù),但是編譯器不一定會對這個函數(shù)內(nèi)聯(lián)展開。編譯器也要根據(jù)實際情況進(jìn)行評估,權(quán)衡展開和不展開的利弊,并最終決定要不要展開。
內(nèi)聯(lián)函數(shù)并不是完美的,也有一些缺點。內(nèi)聯(lián)函數(shù)會增大程序的體積。
一般而言判斷一個內(nèi)聯(lián)函數(shù)是否展開,從程序員的角度主要從以下幾點出發(fā):
函數(shù)體積小
函數(shù)體內(nèi)無指針賦值、遞歸、循環(huán)語句等
調(diào)用頻繁
當(dāng)我們認(rèn)為一個函數(shù)體積小、而且被大量調(diào)用,應(yīng)做內(nèi)聯(lián)展開時,就可以使用static inline 關(guān)鍵字修飾它,但是編譯器不一定會內(nèi)聯(lián)展開。如果想明確告訴編譯器一定要展開,或者不展開就可以使用 noinline 和 always_inline 對函數(shù)的屬性做一個聲明。
8.4 內(nèi)聯(lián)函數(shù)為什么定義在頭文件中?
在Linux 內(nèi)核中,你會看到大量的內(nèi)聯(lián)函數(shù)被定義在頭文件中,而且常常使用static關(guān)鍵字修飾。
為什么定義在頭文件中呢?因為它是一個內(nèi)聯(lián)函數(shù),可以像宏一樣使用,在任何想使用內(nèi)聯(lián)函數(shù)的源文件中,都不必親自在定義一遍,直接包含這個頭文件即可。
為什么還要用static 修飾呢?因為使用inline關(guān)鍵字定義的內(nèi)聯(lián)函數(shù),編譯器不一定會內(nèi)聯(lián)展開,那么當(dāng)一個工程中多個頭文件包含這個內(nèi)聯(lián)函數(shù)的定義時,編譯時就可能報重復(fù)定義的錯誤。使用satic 關(guān)鍵字修飾,則可以限定這個函數(shù)的作用域在各自的源文件內(nèi),避免重復(fù)定義的發(fā)生。
9. 總結(jié)
本文主要介紹了 GNU C 的擴(kuò)展語法 __attributr__ 關(guān)鍵字,并對其中常用的屬性聲明做了詳細(xì)的介紹:
section
packed
aligned
format
alias
weak
noinline
always_inline
審核編輯:劉清
-
RAM
+關(guān)注
關(guān)注
8文章
1369瀏覽量
114808 -
C語言
+關(guān)注
關(guān)注
180文章
7609瀏覽量
137219 -
嵌入式開發(fā)
+關(guān)注
關(guān)注
18文章
1033瀏覽量
47630 -
BSS
+關(guān)注
關(guān)注
0文章
18瀏覽量
12223 -
GNU
+關(guān)注
關(guān)注
0文章
143瀏覽量
17516
原文標(biāo)題:嵌入式C代碼屬性怎么定義?
文章出處:【微信號:玩點嵌入式,微信公眾號:玩點嵌入式】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論