《程序員的自我修養》學習筆記(四)————靜態鏈接

        人們把每個源代碼模塊獨立地編譯,然後按照需要將它們“組裝”起來,這個組裝模塊的過程就是鏈接。鏈接的主要內容就是把各個模塊之間相互引用的部分處理好,使得各個模塊之間能夠正確地鏈接。鏈接的過程主要包括地址與空間分配(Address and Storage Allocation)、符號決議(Symbol Resolution)和重定位(Relocation)等步驟。符號決議有時也叫做符號綁定、名稱綁定、名稱決議、地址綁定等,“決議”更傾向於靜態鏈接,而“綁定”則傾向於動態鏈接。
        當我們有兩個目標文件時,如何將它們鏈接起來形成一個目標文件呢?這個過程中發生了什麼?這個就是鏈接的核心內容:靜態鏈接。

圖1

我們以a.c和b.c爲例分析該過程。

1. 空間與地址分配

        鏈接器就是把幾個目標文件加工後合成一個輸出文件。在這裏,我們就需要把目標文件“a.o”和“b.o”,輸出成可執行文件“ab”。從ELF文件格式中可以知道,可執行文件中的代碼段和數據段都是由輸入的目標文件中合併來的。那麼多個輸入目標文件,鏈接器如何把他們各自的段合併到輸出文件,也就是輸出文件中的空間如何分配給輸入文件。

1.1 按序疊加       

        按序疊加,就是直接將各個目標文件依次合併,但是這樣會造成在有多個文件的輸入下,輸出文件將有很多零散的段。而且一般的硬件都有字節對齊,這樣很容易造成內存碎片,浪費。

1.2 相似段合併

       相似段合併,就是將相同性質的段合併到一起,比如所有的輸入文件的“.text”合併到輸出文件的“.text”段等。
“鏈接器爲目標文件分配地址和空間”這裏的“地址和空間”有兩個含義:(1)可執行文件中的空間;(2)在裝載後的虛擬地址中的虛擬地址空間。
        現在的鏈接器空間分配主要是第二種,使用這種鏈接器一般採用兩步鏈接:
第一步:空間與地址分配
        掃描所有的輸入目標文件,並且獲得它們的各個段的長度、屬性和位置,並且將輸入目標文件中的符號表中所有的符號定義和符號引用收集起來,統一放到一個全局符號表。這一步,鏈接器將能夠獲得所有輸入目標文件的段長度,並且將它們合併,計算出輸出文件中各個段合併後的長度與位置,並建立映射關係。
第二步:符號解析與重定位
        使用上一步收集到的所有信息,讀取輸入文件中段的數據、重定位信息,並且進行符號解析與重定位、調整代碼中的地址等。第二步是鏈接過程的核心,特別是重定位過程。

圖2

        其中VMA表示Virtual Memory Address,即虛擬地址,LMA表示Load Memory Address,即加載地址。
        鏈接前後的程序中所使用的地址已經是程序在進程中的虛擬地址,即我們關心上面各個段中的VMA和Size,忽略File off。可以看到,目標文件中的所有段的VMA都是0,因爲虛擬空間還沒有被分配,所以它們默認爲0。等鏈接以後,可執行文件“ab”中的各個段都被分配到了相應的虛擬地址。

1.3 符號的確定

圖3

         從表中的Value(地址)可以看出main函數和swap函數位於.text段中,shared位於.data段中。所以,“a.o”、“b.o”的各個段已經合併在“ab”的相應的段中了。

2. 符號解析與重定位

2.1 重定位

          在完成空間和地址的分配步驟以後,鏈接器就進入了符號解析與重定位的步驟,這也是靜態鏈接的核心內容。在分析符號解析和重定位之前,首先看看“a.o”裏面是怎樣使用這兩個外部符號的,也就是說我們在“a.c”的源代碼裏面使用了“shared”變量和“swap”函數,那麼編譯器在將“a.c”編譯成指令時,它如何訪問“shared”變量?如何調用“swap”函數?

圖4

        我已經用紅框標出了兩個引用“shared”和“swap”的位置。由於編譯器不知道他們的地址,所以使用“0x00000000”代替着,把真正的地址計算工作留給鏈接器。查看“ab”的反彙編如下圖所示;

圖5

         經過修正以後,“shared”和“swap”的地址分別爲0x006001b8和0x00000003。關於“shared”變量的地址,我們可以從圖3符號表中可以看到,確實是0x006001b8。“callq”指令後面跟着的是指令的下一條指令的偏移量,即0x40010d+0x00000003=0x400110。

2.2 重定位表

       鏈接器是怎麼知道哪些指令是要被調整的呢?在ELF文件中,有一個重定位表(Relocution Table)的結構專門來保存這些與重定位相關的信息。對於可重定位的ELF文件來說,它必須包含有重定位表,用來描述如何修改相應的段裏的內容。對於每個要重定位的ELF段都有一個相應的重定位表,而一個重定位表往往就是ELF文件中的一個段,所以其實重定位表也叫重定位段,這裏我們統一稱爲重定位表。代碼段“.text”如有要重定位的地方,那麼就會有一個相對應叫“.rel.text”的段保存了代碼段的重定位表。

圖6

        查看“a.o”裏面要重定位的地方,即“a.o”所有引用到外部符號的地址。每個要被重定位的地方叫一個重定位入口,“a.o”有兩個重定位入口。從圖4中可以看出這兩個偏移就是代碼段中“mov”指令和“callq”指令的地址位置。
        重定位表的結構也很簡單,它是一個結構體的數組,每個數組元素對應一個重定位入口。

圖7

2.3 符號解析

圖8

        我們查看“a.o”的符號表,如圖8所示。“Global”類型的符號,除了“main”函數是定義在代碼段中,其他連個個 “shared” 和“swap”都是“UND”,即“undefined”未定義類型,這種未定義的符號就是因爲該目標文件中有關於它們的重定位項。所以鏈接器掃描完所有的輸入目標文件後,所有這些未定義的符號都應該能夠在全局符號表中找到,否則鏈接器就報符號未定義錯誤,如圖9所示。

圖9

 

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