完全理解連接器:二,符號決議

符號決議

在這個過程中,連接器須要作的工做就是確保全部目標文件中的符號引用都有惟一的定義。要想理解這句話咱們首先來看看一個典型的c文件裏都有些什麼。程序員

c源文件中都有什麼

如圖所示是一個典型的c源文件,該文件中的變量能夠劃分爲兩類:編程

  • 全局變量:好比x_global_uninit,x_global_init,fn_c。只要程序沒有結束運行,全局變量均可以隨時使用。注意,用static修飾的全局變量好比y_global_uninit,其生命週期也等同於程序的運行週期,只是這種全局變量只能在所被定義的文件當中使用,對其它文件不可見。
  • 局部變量:好比y_local_uninit,y_local_init,局部局部變量的生命週期和全局變量不一樣,局部變量變量只能在相應的函數內部使用,當函數調用完成後該函數中的局部變量也就沒法使用了。由於局部變量只存在於函數運行時的棧幀當中,函數調用完成後相應的棧幀被自動回收(若是你還不能理解這句話是什麼意思沒有關係,我會在後面的文章當中詳細講解程序運行時的內存模型)。

clipboard.png

目標文件裏有什麼

編譯器的任務就是把人類能夠理解的代碼轉換成機器能夠執行的機器指令,源文件編譯後造成對應的目標文件,這個咱們在以前的章節中已經屢次提到過了。源文件被編譯後生成的目標文件中本質上只有兩部分:segmentfault

  • 代碼部分:你可能會想,一個源文件中不都是代碼嗎,這裏的代碼指的是計算機能夠執行的機器指令,也就是源文件中定義的全部函數。好比上圖中定義的函數fn_b以及fn_c。
  • 數據部分:源文件中定義的全局變量。若是是已經初始化後的全局變量,該全局變量的值也存在於數據部分。

到目前爲止,你能夠把一個目標文件簡單的理解爲由兩部分組成,代碼部分中保存的是CPU能夠執行的機器指令,這些機器指令來自程序員所定義的函數,編譯器將這些定義的函數翻譯成機器指令並存放在目標文件的代碼部分。數據部分存放的是機器指令所操做的數據。所以目前,你能夠簡單的將目標文件理解爲一個只有兩部分的文件,如圖所示:微信

clipboard.png

你可能會好奇函數中定義的局部變量爲何沒有放到目標文件的數據段當中,這是由於局部變量是函數私有的,局部變量只能在該函數內部使用而全局變量時沒有這個限制的,因此函數私有的局部變量被放在了代碼段中,做爲機器指令的操做數。函數

編譯器在編譯過程當中遇到外部定義的全局變量或函數時,只要編譯器能找到相應的變量聲明就會在內心默唸「all is well, all is well(一切順利)「,從這裏能夠看出編譯器的要求仍是很低的,至於所使用變量的定義編譯器是不會費力去四處搜索,而是愉快的繼續接下來的編譯。注意,這裏再次強調一下,編譯器在遇到外部定義的全局變量或者函數時只要能在當前文件找到其聲明,編譯器就認爲編譯正確。而尋找使用變量定義的這項任務就被留給了連接器。連接器的其中一項任務就是要肯定所使用的變量要有其惟一的定義。雖然編譯器給連接器留了一項任務,但爲了讓連接器工做的輕鬆一點編譯器仍是多作了一點工做的,這部分工做就是符號表(Symbol table)。spa

符號表(Symbol table)

咱們在上一節中提到,雖然編譯器很不厚道的給連接器留了一項任務,可是編譯器爲了連接器工做的輕鬆一點仍是作了一點事情,這就是符號表。那符號表中保存的是什麼呢,符號表中保存的信息有兩部分:翻譯

  • 該目標文件中引用的全局變量以及函數
  • 該目標文件中定義的全局變量以及函數

以上圖中的代碼爲例,編譯器在編譯過程當中每次遇到一個全局變量或者函數名都會在符號表中添加一項,最終編譯器會統計出以下所示的一張符號表:code

clipboard.png

z_global以及fn_a是未定義的,由於在當前文件中,這兩個變量僅僅是聲明,編譯器並無找到其定義。剩餘的變量編譯器均可以在當前文件中找到其定義。生命週期

fn_b以及fn_c爲當前文件定義的函數,由於在代碼段。遊戲

剩餘的符號都是全局變量,所以放在了數據段。

有同窗可能會問,爲何全局變量y_global_uninit ,y_global_init以及函數fn_b不可被其它目標文件引用,這是由於這些變量用static修飾過了,在C語言中經static修飾過的函數的函數以及變量都是當前文件私有的,對外部不可見,這裏必定要注意。因此static這個關鍵字的用法就是,若是你認爲一個變量只應該被當前文件使用而不暴露給外部,那麼你就可使用static關鍵字修飾一下。

本質上整個符號表只是想表達兩件事:

  • 我能提供給其它文件使用的符號
  • 我須要其它文件提供給我使用的符號

這裏還有一個問題就是,編譯器將統計的這張符號表放在哪裏了呢?

符號表存放在哪裏

在目標文件裏有什麼這一小節中,咱們將一個目標文件簡單的劃分了兩段,數據段和代碼段,如今咱們要向目標文件中再添加一段,而符號表也被編譯器很貼心的放在目標文件中,所以一個目標文件能夠理解爲如圖所示的三段,而符號表中的內容就是上一節當中編譯器統計的表格。

clipboard.png

有了符號表,連接器就能夠進行符號決議了。

符號決議的過程

在上一節符號表中,咱們知道符號表給連接器提供了兩種信息,一個是當前目標文件能夠提供給其它目標文件使用的符號,另外一個其它目標文件須要提供給當前目標文件使用的符號。有了這些信息連接器就能夠進行符號決議了。如圖所示,假設連接器須要連接三個目標文件:
連接器會依次掃描每個給定的目標文件,同時連接器還維護了兩個集合,一個是已定義符號集合D,另外一個是未定義符合集合U,下面是連接器進行符合決議的過程:
1,對於當前目標文件,查找其符號表,並將已定義的符號並添加到已定義符號集合D中。
2,對於當前目標文件,查找其符號表,將每個當前目標文件引用的符號與已定義符號集合D進行對比,若是該符號不在集合D中則將其添加到未定義符合集合U中。
3,當全部文件都掃描完成後,若是爲定義符號集合U不爲空,則說明當前輸入的目標文件集合中有未定義錯誤,連接器報錯,整個編譯過程終止。

上面的過程看似複雜,其實用一句話歸納就是隻要每一個目標文件所引用變量都能在其它目標文件中找到惟一的定義,整個連接過程就是正確的。

若是你以爲上面的解釋比較晦澀的話,你也能夠將連接符號決議這個過程想象成以下的遊戲:
新學期開學後,幼兒園的小朋友們都帶了禮物要和其它的小朋友們分享,同時每一個小朋友也有本身的心願單,每一個小朋友均可以依照本身的心願單去其它的小朋友那裏拿禮物,整個過程結束後,每一個小朋友都能拿到本身想要的禮物。
在這個遊戲當中,小朋友就比如目標文件,每一個小朋友本身帶的禮物就比如每一個目標文件的已定義符號集合,心願單就比如每一個目標文件中未定義符號的集合。
​​

clipboard.png

實例說明undefined reference

假設咱們寫了一個math.c的數字計算程序,其中定義了一個add函數,該函數在main.c中被引用到,那麼很簡單,咱們只須要在main.c中include寫好的math.h頭文件就可使用add函數了,如圖所示:

clipboard.png

可是因爲粗枝大葉,一不當心把math.c中的add函數給註釋掉了,當你在寫完main.c、打算很瀟灑的編譯一下時,出現了很經典的undefined reference to add(int, int)錯誤,如圖所示:

clipboard.png

這個錯誤實際上是這樣產生的:
1, 連接器發現了你寫的代碼math.o中引用了外部定義的add函數(不要忘了,這是經過檢查目標文件math.o中的符號表獲得的信息),因此連接器開始查找add函數究竟是在哪裏定義的。
2,連接器轉而去目標文件math.o的目標文件符號表中查找,沒有找到add函數的定義。
3,連接器轉而去其它目標文件符號表中查找,一樣沒有找到add函數的定義。
4,連接器在查找了全部目標文件的符號表後都沒有找到add函數,所以連接器中止工做並報出錯誤undefined reference to `add(int, int)',如上圖所示。

所以若是你很清楚連接器符號決議這個過程的話就會進行以下排查:
1:main.c中對add函數的函數名有沒有寫正確。
2:連接命令中有沒有包含math.o,若是沒有添加上該目標文件。
3:若是連接命令沒有問題,查看math.c中定義的add函數定義是否有問題。
4:若是是C和C++混合編程時,確保相應的位置添加了extern "C"。

通常狀況下通過這幾個步驟的排查基本可以解決問題。
因此當你再次看到undefined reference這樣的錯誤的是時候,你就應該能夠很從容的去解決這類問題了。

接下來的內容我會在如下幾篇文章當中一一介紹:
完全理解連接器:三,庫與可執行文件
完全理解連接器:四,重定位

若是你喜歡這一系列的文章,也歡迎關注個人微信公共帳號,碼農的荒島求生,獲取更多內容。

clipboard.png

這個系列完整的文章目錄:

clipboard.png

相關文章
相關標籤/搜索