【嵌入式】C语言高级编程-强符号和弱符号(09)
00. 目錄
文章目錄
- 00. 目錄
- 01. weak屬性
- 02. 變量強符號和弱符號
- 03. 函數(shù)強符號和弱符號
- 04. 弱符號的作用
- 05. alias屬性
- 06. 附錄
01. weak屬性
GNU C 通過 attribute 聲明weak屬性,可以將一個強符號轉(zhuǎn)換為弱符號。
用法:
void __attribute__((weak)) func(void); int num __attribte__((weak);編譯器在編譯源程序時,無論你是變量名、函數(shù)名,在它眼里,都是一個符號而已,用來表征一個地址。編譯器會將這些符號集中,存放到一個叫符號表的 section 中。
在一個軟件工程項目中,可能有多個源文件,由不同工程師開發(fā)。有時候可能會遇到這種情況:A 工程師在他負(fù)責(zé)的 A.c 源文件中定義了一個全局變量 num,而 B 工程師也在他負(fù)責(zé)的 B.c 源文件中定義了一個同名全局變量 num。那么當(dāng)我們在程序中打印變量 num 的值時,是該打印哪個值呢?
是時候表演真正的技術(shù)了。這時候,就需要用編譯鏈接的原理知識來分析這個問題了。編譯鏈接的基本過程其實很簡單,主要分為三個階段。
編譯階段:編譯器以源文件為單位,將每一個源文件編譯為一個 .o 后綴的目標(biāo)文件。每一個目標(biāo)文件由代碼段、數(shù)據(jù)段、符號表等組成。
鏈接階段:鏈接器將各個目標(biāo)文件組裝成一個大目標(biāo)文件。鏈接器將各個目標(biāo)文件中的代碼段組裝在一起,組成一個大的代碼段;各個數(shù)據(jù)段組裝在一起,組成一個大的數(shù)據(jù)段;各個符號表也會集中在一起,組成一個大的符號表。最后再將合并后的代碼段、數(shù)據(jù)段、符號表等組合成一個大的目標(biāo)文件。
重定位:因為各個目標(biāo)文件重新組裝,各個目標(biāo)文件中的變量、函數(shù)的地址都發(fā)生了變化,所以要重新修正這些函數(shù)、變量的地址,這個過程稱為重定位。重定位結(jié)束后,就生成了可以在機器上運行的可執(zhí)行程序。
上面舉例的工程項目,在編譯過程中的鏈接階段,可能就會出現(xiàn)問題:A.c 和 B.c 文件中都定義了一個同名變量 num,那鏈接器到底該用哪一個呢?解決該問題就涉及強符號和弱符號問題。
02. 變量強符號和弱符號
在一個程序中,無論是變量名,還是函數(shù)名,在編譯器的眼里,就是一個符號而已。符號可以分為強符號和弱符號。
- 強符號:函數(shù)名、初始化的全局變量名;
- 弱符號:未初始化的全局變量名。
在一個工程項目中,對于相同的全局變量名、函數(shù)名,我們一般可以歸結(jié)為下面三種場景。
- 強符號+強符號
- 強符號+弱符號
- 弱符號+弱符號
強符號和弱符號在解決程序編譯鏈接過程中,出現(xiàn)的多個同名變量、函數(shù)的沖突問題非常有用。一般我們遵循下面三個規(guī)則。
- 一山不容二虎
- 強弱可以共處
- 體積大者勝出
為了方便,這是我編的順口溜。主要意思就是:在一個項目中,不能同時存在兩個強符號,比如你在一個多文件的工程中定義兩個同名的函數(shù),或初始化的全局變量,那么鏈接器在鏈接時就會報重定義的錯誤。但一個工程中允許強符號和弱符號同時存在。比如你可以同時定義一個初始化的全局變量和一個未初始化的全局變量,這種寫法在編譯時是可以編譯通過的。編譯器對于這種同名符號沖突,在作符號決議時,一般會選用強符號,丟掉弱符號。還有一種情況就是,一個工程中,同名的符號都是弱符號,那編譯器該選擇哪個呢?誰的體積大,即誰在內(nèi)存中存儲空間大,就選誰。
程序示例
#include <stdio.h>int a = 1; int b;void fun(void) {printf("fun a = %d\n", a);printf("fun b = %d\n", b); }int a; int b = 2;int main(void) {printf("main a = %d\n", a);printf("main b = %d\n", b);fun();return 0; }執(zhí)行結(jié)果
deng@itcast:~/tmp$ gcc test.c deng@itcast:~/tmp$ ./a.out main a = 1 main b = 2 fun a = 1 fun b = 2我們在 main.c 和 func.c 中分別定義了兩個同名全局變量 a 和 b,但是一個是強符號,一個是弱符號。鏈接器在鏈接過程中,看到?jīng)_突的同名符號,會選擇強符號,所以你會看到,無論是 main 函數(shù),還是 func 函數(shù),打印的都是強符號的值。
一般來講,不建議在一個工程中定義多個不同類型的弱符號,編譯的時候可能會出現(xiàn)各種各樣的問題,這里就不舉例了。在一個工程中,也不能同時定義兩個同名的強符號,即初始化的全局變量或函數(shù),否則就會報重定義錯誤。但是我們可以使用 GNU C 擴展的 weak 屬性,將一個強符號轉(zhuǎn)換為弱符號。
程序示例
# test.c #include <stdio.h>int a = 4;void fun(void);int main(void) {printf("main a = %d\n", a);fun();return 0; }# fun.c #include <stdio.h>int a __attribute__((weak)) = 1;void fun(void) {printf("fun a = %d\n", a); }執(zhí)行結(jié)果
deng@itcast:~/tmp$ gcc test.c fun.c deng@itcast:~/tmp$ ./a.out main a = 4 fun a = 4 deng@itcast:~/tmp$通過 weak 屬性聲明,將 func.c 中的全局變量 a,轉(zhuǎn)換為一個弱符號,然后在 main.c 里同樣定義一個全局變量 a,并初始化 a 為4。鏈接器在鏈接時會選擇 main.c 中的這個強符號,所以在兩個文件中,打印變量 a 的值都是4。
03. 函數(shù)強符號和弱符號
鏈接器對于同名變量沖突的處理遵循上面的強弱規(guī)則,對于函數(shù)同名沖突,同樣也遵循相同的規(guī)則。函數(shù)名本身就是一個強符號,在一個工程中定義兩個同名的函數(shù),編譯時肯定會報重定義錯誤。但我們可以通過 weak 屬性聲明,將其中一個函數(shù)轉(zhuǎn)換為弱符號。
程序示例
//fun.c#include <stdio.h>int a __attribute__((weak)) = 1;void __attribute__((weak)) fun(void) {printf("fun a = %d\n", a); }// test.c #include <stdio.h>int a = 4;void fun(void) {printf("test.c fun....\n"); }int main(void) {printf("main a = %d\n", a);fun();return 0; }執(zhí)行結(jié)果
deng@itcast:~/tmp$ gcc fun.c test.c deng@itcast:~/tmp$ ./a.out main a = 4 test.c fun....在這個程序示例中,我們在 main.c 中重新定義了一個同名的 func 函數(shù),然后將 func.c 文件中的 func() 函數(shù),通過 weak 屬性聲明轉(zhuǎn)換為一個弱符號。鏈接器在鏈接時會選擇 main.c 中的強符號,所以我們在 main 函數(shù)中調(diào)用 func() 時,實際上調(diào)用的是 main.c 文件里的 func() 函數(shù)。
04. 弱符號的作用
在一個源文件中引用一個變量或函數(shù),當(dāng)我們只聲明,而沒有定義時,一般編譯是可以通過的。這是因為編譯是以文件為單位的,編譯器會將一個個源文件首先編譯為 .o 目標(biāo)文件。編譯器只要能看到函數(shù)或變量的聲明,會認(rèn)為這個變量或函數(shù)的定義可能會在其它的文件中,所以不會報錯。甚至如果你沒有包含頭文件,連個聲明也沒有,編譯器也不會報錯,頂多就是給你一個警告信息。但鏈接階段是要報錯的,鏈接器在各個目標(biāo)文件、庫中都找不到這個變量或函數(shù)的定義,一般就會報未定義錯誤。
當(dāng)函數(shù)被聲明為一個弱符號時,會有一個奇特的地方:當(dāng)鏈接器找不到這個函數(shù)的定義時,也不會報錯。編譯器會將這個函數(shù)名,即弱符號,設(shè)置為0或一個特殊的值。只有當(dāng)程序運行時,調(diào)用到這個函數(shù),跳轉(zhuǎn)到0地址或一個特殊的地址才會報錯。
程序示例
#include <stdio.h>int a = 4;void __attribute__((weak)) fun(void);int main(void) {printf("main a = %d\n", a);fun();return 0; }執(zhí)行結(jié)果
deng@itcast:~/tmp$ gcc test.c deng@itcast:~/tmp$ ./a.out main a = 4 段錯誤 (核心已轉(zhuǎn)儲)在這個示例程序中,我們沒有定義 func() 函數(shù),僅僅是在 main.c 里作了一個聲明,并將其聲明為一個弱符號。編譯這個工程,你會發(fā)現(xiàn)是可以編譯通過的,只是到了程序運行時才會出錯。
為了防止函數(shù)運行出錯,我們可以在運行這個函數(shù)之前,先做一個判斷,即看這個函數(shù)名的地址是不是0,然后再決定是否調(diào)用、運行。這樣就可以避免段錯誤了,示例代碼如下。
程序示例
#include <stdio.h>int a = 4;void __attribute__((weak)) fun(void);int main(void) {printf("main a = %d\n", a);if (NULL != fun){fun();}return 0; }執(zhí)行結(jié)果
deng@itcast:~/tmp$ gcc test.c deng@itcast:~/tmp$ ./a.out main a = 4函數(shù)名的本質(zhì)就是一個地址,在調(diào)用 func 之前,我們先判斷其是否為0,為0的話就不調(diào)用了,直接跳過。你會發(fā)現(xiàn),通過這樣的設(shè)計,即使這個 func() 函數(shù)沒有定義,我們整個工程也能正常的編譯、鏈接和運行!
弱符號的這個特性,在庫函數(shù)中應(yīng)用很廣泛。比如你在開發(fā)一個庫,基礎(chǔ)的功能已經(jīng)實現(xiàn),有些高級的功能還沒實現(xiàn),那你可以將這些函數(shù)通過 weak 屬性聲明,轉(zhuǎn)換為一個弱符號。通過這樣設(shè)置,即使函數(shù)還沒有定義,我們在應(yīng)用程序中只要做一個非0的判斷就可以了,并不影響我們程序的運行。等以后你發(fā)布新的庫版本,實現(xiàn)了這些高級功能,應(yīng)用程序也不需要任何修改,直接運行就可以調(diào)用這些高級功能。
弱符號還有一個好處,如果我們對庫函數(shù)的實現(xiàn)不滿意,我們可以自定義與庫函數(shù)同名的函數(shù),實現(xiàn)更好的功能。比如我們 C 標(biāo)準(zhǔn)庫中定義的 gets() 函數(shù),就存在漏洞,常常成為黑客堆棧溢出攻擊的靶子。
#include <stdio.h>int main(void) {char a[10];gets(a);puts(a);return 0; }C 標(biāo)準(zhǔn)定義的庫函數(shù) gets() 主要用于輸入字符串,它的一個 Bug 就是使用回車符來判斷用戶輸入結(jié)束標(biāo)志。這樣的設(shè)計很容易造成堆棧溢出。比如上面的程序,我們定義一個長度為10的字符數(shù)組用來存儲用戶輸入的字符串,當(dāng)我們輸入一個長度大于10的字符串時,就會發(fā)生內(nèi)存錯誤。
接著我們定義一個跟 gets() 相同類型的同名函數(shù),并在 main 函數(shù)中直接調(diào)用,代碼如下。
#include <stdio.h>char *gets(char *str) {printf("hello 123456789\n");return NULL; }int main(void) {char a[10];gets(a);return 0; }執(zhí)行結(jié)果
deng@itcast:~/tmp$ gcc test.c deng@itcast:~/tmp$ ./a.out hello 123456789 deng@itcast:~/tmp$通過運行結(jié)果,我們可以看到,雖然我們定義了跟 C 標(biāo)準(zhǔn)庫函數(shù)同名的 gets() 函數(shù),但編譯是可以通過的。程序運行時調(diào)用 gets() 函數(shù)時,就會跳轉(zhuǎn)到我們自定義的 gets() 函數(shù)中運行。
05. alias屬性
GNU C 擴展了一個 alias 屬性,這個屬性很簡單,主要用來給函數(shù)定義一個別名。
程序示例
#include <stdio.h>void _fun(void) {printf("_fun\n"); }void fun() __attribute__((alias("_fun")));int main(void) {char a[10];fun();return 0; }執(zhí)行結(jié)果
deng@itcast:~/tmp$ gcc test.c deng@itcast:~/tmp$ ./a.out _fun通過 alias 屬性聲明,我們就可以給 _fun() 函數(shù)定義一個別名 fun(),以后我們想調(diào)用 _fun() 函數(shù),可以直接通過 fun() 調(diào)用即可。
在 Linux 內(nèi)核中,你會發(fā)現(xiàn) alias 有時會和 weak 屬性一起使用。比如有些函數(shù)隨著內(nèi)核版本升級,函數(shù)接口發(fā)生了變化,我們可以通過 alias 屬性給這個舊接口名字做下封裝,起一個新接口的名字。
//f.c void __f(void) {printf("__f()\n"); } void f() __attribute__((weak,alias("__f"))); ? //main.c void __attribute__((weak)) f(void); void f(void) {printf("f()\n"); } ? int main(void) {f();return 0; }當(dāng)我們在 main.c 中新定義了 f() 函數(shù)時,在 main 函數(shù)中調(diào)用 f() 函數(shù),會直接調(diào)用 main.c 中新定義的函數(shù);當(dāng) f() 函數(shù)沒有新定義時,就會調(diào)用 __f() 函數(shù)。
06. 附錄
參考:C語言嵌入式Linux高級編程
總結(jié)
以上是生活随笔為你收集整理的【嵌入式】C语言高级编程-强符号和弱符号(09)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【嵌入式】C语言高级编程-变参函数(08
- 下一篇: 【嵌入式】C语言高级编程-内联函数(10