静态链接中的那点事儿(1)
作為一個程序員或者說C++程序開發人員,想必對ELF目標文件從整體輪廓到某些局部的細節都非常熟知。該系列帖子主要為了解決一個疑惑:當我們有多個目標文件時,如何將它們連接起來形成一個可執行文件?這個過程發生了什么?其實,讀到這里,可能就了解到,這其實就是鏈接的核心內容:靜態鏈接。
1.應用到的兩個源代碼文件
<span style="font-size:18px;">/* a.c */ extern int shared;int main() {int a = 100;swap(&a, &shared);cout << a;cout << endl;cout << shared; }</span> <span style="font-size:18px;">/* b.c */ int shared = 1;void swap(int* a, int* b) {*a ^= *b ^= *a ^= *b; }</span>首先使用GCC將“a.c”、“b.c”分別編譯稱目標文件“a.o”、“b.o”。從代碼中,我們可以看到,“b.c”一共定義了兩個全局符號:一個是變量“shared”,另一個是函數“swap”;“a.c”中定義了一個全局符號就是“main”。模塊“a.c”中引用到了“b.c”中的swap和shared。我們要研究的工作就是“a.o”文件與“b.o”文件是怎么樣鏈接成一個可執行文件 “ab”的?2.空間與地址分配
對于連接器而言,整個鏈接過程中,他就是將幾個輸出目標文件加工后合并成一個輸出文件。根據我們已有的ELF文件格式知識,我們知道可執行文件中的代碼段和數據段都是由輸入的目標文件合并而來的。這里我們首先來探索第一個問題:對于多個輸入目標文件,鏈接器如何將它們的各個段合并到輸出文件?或者說,輸出文件中的空間如何分配給輸入文件?
按序疊加:
按序疊加的思想非常簡單粗暴,就是直接將各個目標文件一次合并,該思路可以用下面圖示說明:
該種方法的確很簡單,但是帶來一個很直接的問題:在很多輸入文件愛你的情況下,輸出文件會有很多零散的段。比如說,一個規模稍大的應用程序可能會有數百個目標文件,如果每個目標文件都分別有.text段、.data段、.bss段,那最后的輸出文件將會有成百上千個零散的段。這種做法非常消耗空間,造成內存空間中大量的內部碎片,并不是一個很好的方案。
相似段合并:
一個更實際的方法就是將相同性質的段合并到一起,其設計思想如下圖所示:
正如我們了解到的,“.bss”段在目標文件和可執行文件中并不占用文件空間,但是在裝載時需要占用地址空間。所以鏈接器在合并各個段的同時,也會將“.bss”段合并,并且分配虛擬空間。這里有一個問題,先前一直很迷惑,這里可以做一個小小的理解。所謂的“空間分配”到底是什么空間?
其實“連接器為目標文件分配地址和空間”這句話中的“地址和空間”有兩個含義:第一個是指在輸出的可執行文件中的空間;第二個是在裝載后的虛擬地址中的虛擬地址空間。對于有實際數據的段,比如".text"和“.data”來說,他們在文件中和虛擬地址中都要分配空間,因為他們在這兩個里面都存在!然而,對于“.bss”這樣的段來說,分配空間的意義只限于虛擬地址空間,因為他在文件中并沒有內容。事實上,我們在這里談到的空間分配只關注與虛擬地址空間的分配。
當代操作器多采用后一種空間分配策略。整個鏈接過程可以分為兩步:
1.空間與地址分配。掃描所有輸入的目標文件,并且獲得他們的各個段的長度、屬性和位置,并且將輸入目標文件中的符號表綜所有的符號定義和符號引用收集起來,統一放到一個全局符號表中。這一步,鏈接器將能夠獲得所有目標文件的段長度,并且將它們合并。而且通過計算出輸出文件中各個段合并后的長度和位置,建立映射關系。2.符號解析與重定位。在上面收集到信息的基礎上,讀取輸入文件中段的數據、重定位信息,并且進行符號解析和重定位、調整代碼中的地址。這一步才是鏈接過程的核心,特別是重定位過程。
連接前后各個段的屬性分析:
注:VMV(Virtual Memory Address,虛擬地址);LMA(Load Memory Address,加載地址)。正常情況下兩者一樣。
這個連接前后,目標文件各段的分配、程序虛擬地址等可以用下圖表示:
符號地址的確定:
在第一步完成之后,連接器開始計算各個符號的虛擬地址。因為各個符號在段內的相對位置是固定的,所以這個時候“main”、"shared"、"swap"等地址也就確定了,只不過鏈接器需要給每個符號加上一個偏移量,使他們能夠調整到正確的虛擬地址。
3.符號解析與重定位
重定位:
在完成空間和地址的分配步驟以后,鏈接器就進入符號解析與重定位的步驟,這也是靜態鏈接的核心內容。在分析符號解析與重定位之前,首先看看“a.o”里面是怎么樣使用那兩個東東的("shared"、"swap")【也就是說,我們在a.c源文件中使用了“shared”變量和“swap”函數,那么編譯器再將“a.c”編譯成指令時,是如何訪問該變量?以及如何調用該函數的呢?】
對a.o文件進行反匯編得到下面代碼清單:
注:在程序的代碼里面使用的都是虛擬地址,從上圖中我們可以看到“main”函數的起始地址是0x0000000,這是因為在未進行空間分配之前,目標文件代碼段中的起始地址都應該以0x0000000開始,等待空間分配完成以后,各個函數才會確定自己在虛擬地址空間中的位置。
通過反匯編結果,我們能夠清楚的看到,”a.o“共定義了一個main函數,共占用0x33個字節,共17條指令;對于變量”shared“的引用是一條”move“指令,它的作用是將”shared“的地址賦值到ESP寄存器+4的偏移地址中去。對于函數”swap“的引用是一條”call“指令。
重定位表:
那么鏈接器是怎樣知道哪些指令是要被調整的呢?這些指令有哪些部分需要部分調整呢?又怎么進行調整?這些工作其實就是依賴重定位表完成的!該結構專門用來保存這些與重定位有關的信息。
符號解析:
其實在我們普通的觀念中,之所以要進行鏈接是因為我們的目標文件中用到的符號被定義在其他的目標文件中了,所以我們要把他們整合起來。比如,如果我們直接用ld命令來鏈接”a.o“文件,而不輸入”b.o“文件,那么鏈接器就會報錯,提示我們沒有發現shared和swap兩個符號的定義,鏈接就會失敗,實驗效果如下圖所示:
其實,這也是我們平時在編寫程序的時候最常見到的問題:鏈接是符號沒有定義!導致這個問題的原因有很多,最常見的一般都是鏈接時缺少了某個庫,或者輸入目標文件路徑不正確或符號的生命與定義不一樣!!!
4.小結
通過前面的介紹,我們可以更加深層次地理解為什么缺少符號的定義會導致連接錯誤!其實重定位過程也伴隨符號的解析過程,每個目標文件都可能定義一些符號,也可能引用到定義在其他目標文件中的符號。重定位的過程中,每個重定位的入口都是對一個符號的引用,那么當鏈接器需要對某個符號的引用進行重定位時,就必須明白該富豪的目標地址。這時候,鏈接器就會去查找由所有目標文件的符號所組成的全局符號表,找到相應的符號后就可以開展重定位工作。
總結
以上是生活随笔為你收集整理的静态链接中的那点事儿(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 超可爱桌面电子宠物下载
- 下一篇: SEOer必须注意的10种错误SEO做法