菜鳥在C語言編譯,鏈接時可能遇到的兩個問題


最近在看 CSAPP (Computer Systems A Programmers Perspective 2nd) 的第七章 鏈接。學到了點東西,跟大家分享。下文中的例子都是出自CSAPP第七章。

另外,也可以結合酷殼上的這篇文章和之后的留言來看本文,理解會更加深刻一些。
1.問:如果在不同的C源文件中定義了相同名稱的全局變量會有什么樣的后果呢?

比如下面的這種情況:

有兩個源文件foo3.c和bar3.c:
foo3.c

#include <stdio.h>
void f(void);

int x = 15213;

int main()
{
    f();
    printf("x = %d\n", x);
    return 0;
}

bar3.c

int x;

void f()
{
    x = 15212;
}

要解答這個問題,就得知道鏈接的時候,鏈接器(Linker)是如何解析多處定義的全局符號的:[ CSAPP 7.6.1]

在編譯時,編譯器輸出每個全局符號(global symbols)給匯編器,這些符號要么是強(strong symbol)符號,要么是弱(weak symbol)符號。
函數和已初始化的全局變量是強符號,未初始化的全局變量是弱符號。 根據強弱符號的定義,Unix鏈接器使用下面的規則來處理多處定義的符號: 規則1:不允許有多個強符號【
否則,鏈接的時候會出錯:multiple definition of 'xx'】。 規則2:如果有一個強符號和多個弱符號,那么選擇強符號 規則3:如果有多個弱符號,那么從這些弱符號中任意選一個

所以根據規則2,bar3.c中的x是弱引用,在鏈接時,編譯器會悄悄地會認為bar3.c處引用的是foo3.c中定義的x,所以在main調用f()之后,x = 15212;

小伙伴們如果看明白了,可以想想在IA32/Linux機器上,下面這段代碼的輸出是什么?

foo5.c

#include <stdio.h>
void f(void);

int x = 15213;
int y = 15212;

int main()
{
    f();
    printf("x = 0x%x y = 0x%x \n", x, y);
}

bar5.c

double x;
void f()
{
    x = -0.0
}

 

對於上述C語言中多處定義的全局符號問題,有沒有什么解決方案呢?

個人認為:
        1.盡量減少全局變量的使用(有好多坑),凡是別的文件里用不着的全局變量,都加上static來限定其作用域都在本模塊內,這樣這個變量也就不是全局符號了,是本地符號。
        2.可以在gcc的命令行里加上 -fno-common 參數,當遇到多重定義的全局變量時,會報錯 
   
2.在編譯源代碼的時候,是否會遇到鏈接器死活就是提示錯誤: undefined reference to 'foo'。仔細確認了好幾遍,發現包含了這個函數需要的庫的名稱,也指定了庫的路徑,也通過命令:nm -s libfoo.a 看到libfoo靜態庫里是有foo這個符號的,但鏈接階段就是會報錯。【我當時遇到這個問題時,百思不得其解,快要跳腳罵娘了。后來還是找老鳥同事搞定的。】

上述這種情況,很有可能是命令行里,指定編譯所需的靜態鏈接庫時,鏈接庫的順序有問題。舉個例子:
        foo.c 調用了libx.a和libz.a中的函數,而這兩個庫又需要調用liby.a中的函數,那么在,命令行中,libx.a和libz.a必須在liby.a之前,否則就會出現找不到符號定義的情況。為什么呢?請看解釋[CSAPP 7.6.3]:      

在符號解析的階段,鏈接器從左到右按照文件在編譯器驅動程序命令行上出現的相同順序來掃描可重定位目標文件和存檔文件。(驅動程序自動將命令行中所有的.c文件翻譯為.o文件)在這次掃描中,鏈接器維持一個可重定位目標文件的集合 E,這個集合中的文件會被合並起來形成可執行文件,和一個未解析的符號(也就是,引用了但是尚未定義的符號)集合U,以及一個在前面輸入文件中己定義的符號集合D。初始地,E, U和D 都是空的。 對於命令行上的每個輸入文件f,鏈接器會判斷f是一個目標文件(object file)還是一個存檔文件(archive)。如果f是一個目標文件,那么鏈接器把f添加到E,修改U和D來反映f中的符號定義和引用,並繼續下一個輸入文件。如果f是一個存檔文件,那么鏈接器就嘗試匹配U中未解析的符號和由存檔文件成員定義的符號。如果某個存檔文件成員m,定義了一個符號來解析U中的一個引用,那么就將m加到E中,並且鏈接器修改U和D來反映m中的符號定義和引用。對存檔文件中所有的成員目標文件都反復進行這個過程,直到U和D都不再發生變化。在此時,任何不包含在E 中的成員目標文件都被丟棄,而鏈接器將繼續到下一個輸入文件。 如果當鏈接器完成對命令行上輸入文件的掃描后,U是非空的,那么鏈接器就會輸出一個錯誤並終止。否則,它會合並和重定位E中的目標文件,從而構建輸出的可執行文件。

上面這一大段挺繞的,簡單來說,就一句話:如果在命令行中,定義一個符號的庫出現在引用這個函數/變量的目標文件之前,那么引用就不能被解析,鏈接會失敗。就像上面的例子。

update: 2013/10/20

另外,如果靜態庫之間不是相互獨立的,也有相互引用,那么必須得正確安排好順序。比如,foo.c調用libx.a和libz.a中的函數,而這兩個庫又調用 liby.a中的函數。那么命令行中libx.a和libz.a必須在liby.a之前:

gcc foo.c libx.a libz.a liby.a

如果需要滿足依賴的需求,可以在命令行上重復庫。比如,foo.c 調用 libx.a,該庫又調用 liby.a,而 liby.a 又調用libx.a。那么libx.a 必須在命令行上重復出現:

gcc foo.c libx.a liby.a libx.a     

如果還有不太明白的地方,可以去看看CSAPP的第七章。

強烈推薦大家有時間讀讀CSAPP這本書啊,收獲會很大的。附上 老趙書托(3):深入理解計算機系統


   


如果您看了本篇博客,覺得對您有所收獲,請點擊右下角的“推薦”,讓更多人看到!

資助Jack47寫作,打賞一個雞蛋灌餅錢吧
pay_weixin
微信打賞
pay_alipay
支付寶打賞

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM