檔案的鏈結過程

2022-07-10 02:48:11 字數 2905 閱讀 3950

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

gcc編譯c原始碼有四個步驟:

預處理——> 編譯——> 彙編 ——> 鏈結

1. 預處理階段,預處理器將c源**包含的標頭檔案編譯進來,形成預處理檔案。

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

3. 彙編階段,彙編器把在編譯階段生成的組合語言轉成二級制目標**,分為可重定位目標檔案,可執行目標**和可共享目標**。

4. 鏈結階段,鏈結器把多個可重定位目標檔案鏈結成最終可執行的目標檔案。

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

**模組在編譯之後會形成可重定位目標檔案,檔案中儲存的**為二進位制格式,但是這些**是不能載入記憶體中執行的,原因有二:

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

b. 一些符號引用(全域性變數或函式)缺乏定義,這些符號的定義存在其他模組檔案中;

可重定位目標檔案中除了基本的程式指令和程式資料之外,還提供了其他的資料結構(比如符號表)來提供鏈結時需要的資訊。可重定位目標檔案分為多個節,主要的節段如下;

1. **和資料

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

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

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

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

.common:表示還未分配位置的未初始化的資料目標

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

2. 符號表

符號表用來描述程式**和程式資料中儲存的指令和資料,其中的符號包含如下幾類:

1. 在本模組定義,但是可被其他模組引用的符號,包括函式,全域性變數;

2. 在本模組引用,但是在其他模組中定義的符號,包括函式,全域性變數;

3. 只被本模組定義和引用的本地符號。帶static的函式和帶static的全域性變數和本地變數;

每一條符號描述的資料結構如下:

1 typedef struct

elf64_symbol;

符號表中的條目主要描述了符號定義的如下特徵:符號型別,即函式還是變數(type),作用域,即全域性還是區域性(binding),符號定義所在的節(section),符號定義在節中的偏移量(value),符號定義所佔空間的大小,聯結器正是通過這些資訊進行符號解析並對指令和資料進行重定位。

3. rel.text和rel.data

.rel.text: 乙個.text節中位置的列表。當鏈結器將此檔案與其他目標檔案鏈結時需要修改這些位置,一般任何呼叫外部函式或引用全域性變數的指令都要修改

.rel.data: 引用或定義的任何全域性變數的重定位資訊,任何已初始化的全域性變數,如果它的初值是乙個全域性變數位址或外部函式位址,就需要修改

1 typedef struct

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中的值可以計算出引用符號的位址,從而把**和資料中的符號引用替換為相應的記憶體位址;

編譯鏈結過程

在談編譯鏈結過程之前我們需要了解一下虛擬位址空間以及程式在編譯鏈結過程時經過了什麼步驟。虛擬位址空間之前在程序空間的部落格中詳細介紹過了,詳見 上圖就是32位系統中4g虛擬位址空間的分布情況 text 段 指令段,存放的是指令 在程式中,我們把區域性變數定義 區域性變數的 定義是指令而不是資料 還有...

動態鏈結過程

最近學習了elf檔案的格式,重點關注了動態鏈結過程中的使用到的section 第一步程式在載入時,會把直譯器程式加入到.interp段。可以解決動態庫和可執行檔案的載入。一般來講程式的載入方式是懶啟動,lazy 除非指定了ld bind now環境變數非0,那麼在程式啟動時就會把外部符號位址全部載入...

編譯鏈結過程(一)

什麼是編譯?什麼是鏈結?為什麼需要編譯和鏈結?在很久以前,計算機發展的初期,還在用機器語言編寫程式,量比較少時是不需要編譯和鏈結的。因為當時的程式設計師直接編寫機器碼讓計算機執行。每種cpu的指令是不相同的,所以每乙個程式要換一台不同cpu的機器上執行時,需要重新寫程式,而且機器語言 涉及很多計算機...