靜態鏈接

《程序員的自我修養——鏈接、裝載與庫》讀書筆記

        靜態鏈接要解決的問題是將幾個目標文件鏈接起來成爲形成一個可執行文件。現在的鏈接器一般都採用一種叫做兩步鏈接(Two-pass Linking)的方法。

  • 第一步,地址與空間分配,掃描所有的輸入目標文件,合併它們各個節,更新節表和全局符號表。
  • 第二步,符號解析與重定位,利用上一步蒐集到的信息,讀取文件中節的數據、重定位信息,進行符號解析與重定位,調整代碼中的地址等。

1. 空間和地址的分配

        鏈接器會爲目標文件分配地址和空間,這裏不僅是指在輸出的可執行文件中的空間,也是在裝載後的虛擬地址中的虛擬地址空間。但像.bss這樣的節來說,分配空間的意義只限於虛擬地址空間。用objdump -h可以看到鏈接前後的虛擬地址的分配情況,其中VMA表示虛擬地址(Virtual Memory Address),LMA表示加載地址(Load Memory Address),正常情況下這兩個值是一樣的。


靜態鏈接前
圖1 靜態鏈接前


靜態鏈接後
圖2 靜態鏈接後

        鏈接前,目標文件中的所有節的VMA都是0,此時虛擬空間還沒有被分配。等到鏈接後,可執行文件中的各個節都被分配到了相應的虛擬地址。

        輸入文件中的各個節的虛擬地址確定以後,鏈接器開始計算各個符號的虛擬地址。因爲各個符號在節內的相對位置是固定的,它們的地址也已經是確定的了,也就是節的虛擬首地址加上該符號在節內的偏移量。這樣鏈接器就可以更新全局符號表了。

2. 符號解析與重定位

        在ELF文件中有一個叫重定位表(Relocation Table)的結構,專門用來保存這些與重定位相關的信息,它在ELF文件中往往是一個或多個節。對於每個要重定位的ELF節都有一個對應的重定位表,佔據一個節,所以也可以叫重定位節。比如,.rel.text節保存了代碼節.text的重定位表,.rel.data節保存了.data的重定位表。objdump -r指令可以用來查看目標文件的重定位表。每個要被重定位的地方叫做一個重定位入口(Relocation Entry),重定位入口的偏移(Offset)表示該入口在要被重定位的節中的位置。

        不同機器指令的尋址方式可能不同,這樣導致重定位時的修正方式也是不同的。舉個例子,採用絕對尋址的指令,重定位的修正方式爲保存在被修改位置的值 + 符號的實際地址,採用相對尋址的指令的修正方式爲保存在被修改位置的值 + 符號的實際地址 - 被修正的位置的地址

3. 強、弱符號和強、弱引用

        當多個目標文件中含有相同名字的全局符號的定義時,這些目標文件在鏈接時將會出現符號重定義的錯誤。這種符號可以被稱爲強符號(Strong Symbol)。當然還有一些符號的定義可以被稱爲弱符號(Weak Symbol)。對於C/C++來說,編譯器默認函數和初始化了的全局變量爲強符號,未初始化的全局變量爲弱符號。GCC的__attribute__((weak))可以用來定義任何一個強符號爲弱符號。強符號和弱符號都是針對符號的定義來說的,而不是符號的引用。針對強弱符號的概念,鏈接器會按照如下規則處理:

  1. 不允許強符號被多次定義,否則會報重定義錯誤
  2. 如果一個符號在某個目標文件中爲強符號,在其它文件中都爲弱符號,那麼選擇強符號
  3. 如果一個符號在所有的目標文件中都是弱符號,那麼選擇其中佔用空間最大的一個

        在目標文件中對外部目標文件的符號引用在鏈接過程中也要被正確決議,如果沒有找到該符號的定義,鏈接器就會報符號未定義錯誤,這種引用被稱爲強引用(Strong Reference),與之相應的還有一種弱引用(Week Reference)。在處理弱引用時,如果該符號未被定義,則鏈接器對該引用不報錯,默認其爲0或者某個特殊的值,以便於程序代碼能夠識別。在GCC中可以使用__attribute((weakref))__這個擴展關鍵字來聲明一個引用爲弱引用。

        這種弱符號和弱引用對於庫來說十分有用,比如庫中定義的弱符號可以被用戶定義的強符號所覆蓋,從而使程序可以使用自定義版本的庫函數;或者程序可以對某些擴展模功能模塊的引用定義爲弱引用,當我們的擴展模塊與程序鏈接在一起時,功能模塊就可以正常使用;如果我們去掉了某些功能模塊,那麼程序也可以正常鏈接,只是缺少了相應的功能,這使得程序更加容易剪裁和組合。

4. COMMON塊

        在前面ELF文件的基本結構中,我們說未初始化的全局變量和局部靜態變量存儲在.bss節中,其實現在的編譯器生成的目標文件中,未初始化的全局變量往往並沒有被放在.bss節中。在我們瞭解了強弱符號和強弱符號的相關知識後,可以來探討這個問題了。

        現在的編譯器和鏈接器都支持一種叫做COMMON塊(Common Block)的機制,這種機制最早來源於Fortran,早期的Fortran沒有動態分配空間的機制,程序員必須事先聲明他所需要的臨時使用空間的大小。Fortran把這種空間叫做COMMON塊,當不同的目標文件需要的COMMON塊空間大小不一致時,以最大的那一塊爲準。

        現代的鏈接機制在處理弱符號的時候,採用的就是和COMMON塊一樣的機制。當有一強符號時,最終輸出結果中的符號所佔空間與強符號的相同,但若有弱符號的大小大於強符號,鏈接器會輸出警告。導致需要COMMON塊這種機制的根本原因是鏈接器不支持符號類型,也就無法判斷各符號的類型是否一致。

        當編譯器將一個編譯單元編譯成目標文件時,如果該編譯單元中包含了弱符號,那麼該弱符號最終所佔的空間大小此時是未知的,也就不能爲它在.bss節分配空間,只有鏈接器在鏈接的時候才能確定該弱符號的大小,它可以在最終輸出文件的.bss節爲其分配空間。所以總體看來,未初始化的全局變量最終還是被放在.bss節中的。GCC的編譯選項-fno-common和擴展關鍵字__attribute__((nocommon))允許我們將未初始化的全局變量不以COMMON塊的形式處理,那麼它就相當於一個強符號了。

6. 靜態鏈接庫

        靜態鏈接庫實際上是一組目標文件的集合,即很多目標文件壓縮打包後形成的一個文件。gcc在執行靜態鏈接時,會自動找到我們的目標文件所引用的目標文件所在的和所依賴的靜態鏈接庫,並把它們鏈接進來。爲了減小空間的浪費,靜態鏈接庫中的每個目標文件往往只包含一個函數。

7. 總結

        這裏描述的就是鏈接的基本過程,但是靜態鏈接存在着很多弊端,比如可執行文件變得很大,庫文件更新不方便等等,現在的程序大多都使用的動態鏈接的方式,後面繼續展開。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章