1. 程式人生 > >文件的鏈接過程

文件的鏈接過程

set 定位 數據 reserve 創建 不同的 程序代碼 隨機 技術分享

在構建大型程序的時候,為了方便代碼管理,會根據不同的功能把代碼分為多個片段(或模塊)並存儲在不同的文件中,在代碼執行時需要把這些代碼模塊合並成一個單一的可執行文件,這個合並過程稱之為鏈接。本文詳細描述了鏈接的整個過程。

一、從源代碼到可執行目標文件

GCC編譯C源碼有四個步驟:

預處理——> 編譯——> 匯編 ——> 鏈接

1. 預處理階段,預處理器將C源代碼包含的頭文件編譯進來,形成預處理文件。

2. 編譯階段,在這個階段編譯器會檢查代碼的規範性,是否有語法錯誤等,在確定無誤後,編譯器把代碼翻譯成匯編語言。

3. 匯編階段,匯編器把在編譯階段生成的匯編語言轉成二級制目標代碼,分為可重定位目標文件,可執行目標代碼和可共享目標代碼。

4. 鏈接階段,鏈接器把多個可重定位目標文件鏈接成最終可執行的目標文件。

接下來主要分析了鏈接階段;

二、可重定位目標文件

代碼模塊在編譯之後會形成可重定位目標文件,文件中存儲的代碼為二進制格式,但是這些代碼是不能載入內存中運行的,原因有二:

a. 可能確少入口函數,在C語言中,入口函數為main;

b. 一些符號引用(全局變量或函數)缺乏定義,這些符號的定義存在其他模塊文件中;

可重定位目標文件中除了基本的程序指令和程序數據之外,還提供了其他的數據結構(比如符號表)來提供鏈接時需要的信息。可重定位目標文件分為多個節,主要的節段如下;

1. 代碼和數據

.text節:存儲所有指令和常量,編譯器對指令中的未知符號,會生成對應的重定位條目;

.data節: 存儲所有已被初始化的全局變量、靜態變量;

.bss節:存儲所有未被初始化或初始化為0的全局變量和靜態變量;

這三個節是目標文件的主體,負責符號定義和符號引用,程序運行時這些節的內容會加載入內存中執行,其他節是為了幫助鏈接器和加載器完成代碼的鏈接和加載。另外存儲數據的還有3個偽節:

.UNDEF:未定義符號,表明被這個目標文件引用,但是在其他地方定義

.COMMON:表示還未分配位置的未初始化的數據目標

.ABS:不該被重定位的符號

2. 符號表

符號表用來描述程序代碼和程序數據中存儲的指令和數據,其中的符號包含如下幾類:

1. 在本模塊定義,但是可被其他模塊引用的符號,包括函數,全局變量;
2. 在本模塊引用,但是在其他模塊中定義的符號,包括函數,全局變量;
3. 只被本模塊定義和引用的本地符號。帶static的函數和帶static的全局變量和本地變量;

每一條符號描述的數據結構如下:

1 typedef struct{
2   int name;
3   char type:4,
4       binding:4;
5   char    reserved;
6   short    section;
7   long    value;
8   long    size;
9 }ELF64_Symbol;

符號表中的條目主要描述了符號定義的如下特征:符號類型,即函數還是變量(type),作用域,即全局還是局部(binding),符號定義所在的節(section),符號定義在節中的偏移量(value),符號定義所占空間的大小,連接器正是通過這些信息進行符號解析並對指令和數據進行重定位。

3. rel.text和rel.data

.rel.text: 一個.text節中位置的列表。當鏈接器將此文件與其他目標文件鏈接時需要修改這些位置,一般任何調用外部函數或引用全局變量的指令都要修改

.rel.data: 引用或定義的任何全局變量的重定位信息,任何已初始化的全局變量,如果它的初值是一個全局變量地址或外部函數地址,就需要修改

重定位時會修改這兩個節段的值,然後鏈接器會通過這兩個字段的條目計算符號引用指向的地址。條目的數據結構如下:

1 typedef struct{
2   long   offset;
3   long   type:32,
4        symbol:32;
5   long   addend;
6 }ELF64_Rela;

三、可執行目標文件

可執行目標文件可加載到內存中運行,其程序代碼和數據中引用的符號都已經定位到對應的虛擬內存空間。由於沒有未定義的符號和未初始化的數據目標,所以UNDEF、COMMON和ABS節段是不存在的。可執行目標文件結構如下:

技術分享圖片

四、鏈接過程

為了形成可執行的文件,多個可重定位目標文件需要合並在一起,為了正常合並,需要做到以下三點:

1. 解決符號定義沖突,多個符號可能在不同的文件中有多個定義,鏈接器需要按一定的規則選擇其中的一個定義或拋出錯誤;

2. 重定位,由於多個文件合並成了一個文件,代碼和數據在原來文件中的相對地址在新文件中將會發生改變,因此他們的位置需要重新定位,並且需要修改符號表和rel.data即rel.text表;

3. 計算符號引用地址,文件合並之後每個符號引用都有了唯一的定義,因此需要計算符號引用指向的地址,並將符號引用替換為對應的地址;

1. 符號解析——解決符號定義沖突

由於多個模塊中可能存在對同一符號的重復定義,通過符號解析過程,可以確保每一個符號有且只有一個定義,並且符號表中對每個定義只存在唯一的符號解析。我們把已經初始化或初始化為0的符號稱之為強符號,未初始化的符號稱之為弱符號,符號解析規則如下:
規則一: 同一個符號不能存在兩個及兩個以上的強類型,否則拋出錯誤;
規則二: 同一個符號如果存在1個強類型和多個弱類型,那麽選擇強類型;
規則三: 同一個符號如果存在多個弱類型,則隨機選擇一個;

備註:多重定義全局變量會造成一些意想不到的錯誤,而且是默默發生的,編譯系統不會警告,並會在程序執行很久後才能表現出來,且遠離錯誤處。特別是在模塊很多的大型軟件中,這類錯誤很難修正,因此定義全局變量時要習慣賦初始值。

符號解析時,鏈接器會創建3個空的列表,分別存儲未定義的符號(假設為A),已定義的符號(假設為B),以及目標文件列表(假設為C)。初始狀態三個列表都為空,接下來鏈接器會依次選擇目標文件,並將其加入列表C,然後根據目標文件的定義和解析規則更新A和B,當所有目標文件都遍歷完成後,如果A不為空,說明有的符號引用未定義,會拋出錯誤。否則表明所有的引用都有唯一的定義,然後就能進行重定位了。

2. 重定位

符號解析完成後,由於每一個符號引用都有對應的唯一的定義,因此可以獲得其地址和所占空間大小,根據這些信息可以依次把代碼和數據按不同的字段聚合在一起,並重新定位聚合後符號的內存位置,然後修改符號表及rel.text和rel.data表中的符號描述。下面是一個簡單的演示示例,兩個模塊分別定義了兩個變量x,y:

技術分享圖片

3. 計算符號引用地址

重定位完成後通過rel.text和rel.data中的值可以計算出引用符號的地址,從而把代碼和數據中的符號引用替換為相應的內存地址;

文件的鏈接過程