在完成空間與位址的分配步驟之後,鏈結器就進入了符號解析與重定位的步驟,這也就是靜態鏈結的核心作用;
在分析符號解析和重定位之前,首先讓我們來看看「a.o」裡面是怎麼使用這兩個外部符號,也就是說我們在「a.c」源程式裡面使用了「shared」變數和「swap」函式,那麼編譯器在將「a.c」編譯成指令時,它如何訪問「shared」變數?如何呼叫「swap」函式?
使用objdump的-d引數可以看到「a.o」的**反彙編結果:
我們知道在程式的**裡面使用的都是虛擬位址,在這裡也可以看到「main」的起始位址以0x00000000開始,等到空間分配完成之後,各個函式才回確定自己在虛擬位址空間中的位置;
我們可以很清楚地看見「a.o」的反彙編結果中,「a.o」共定義了函式main,這個函式占用了0x33個位元組,共17條指令;最左邊的那列是每條指令的偏移量,每一行代表一條指令(有些指令的長度很長,如偏移0x18的mov指令,它的二進位制顯示佔據了兩行)。我們已經用粗體標出了兩個引用「shared」和「swap」的位置,對於「shared」的引用是一條「mov」指令,這條指令總共8個位元組,它的作用是將「shared」的位址賦值給esp暫存器+4的偏移位址中去,前面4個位元組是指令碼,,後面4個位元組是「shared」的位址,我們只關心後面的4個位元組部分,如圖4-4:
當源**「a.c」在被編譯成目標檔案時,編譯器並不知道「shared」和「swap」的位址,因為它們定義在其他目標檔案中,所以編譯器就暫時把位址0看成「shared」的位址,我們可以看到這條「mov」指令中,關於「shared」的位址部分為「0x00000000」。
另乙個偏移是0x26的指令的一條呼叫,它其實就是表示對swap函式的呼叫,如4-5所示:
這條指令共5個位元組,前面的0xe8是操作碼(intel從ia-32手冊可以查閱到),這條指令是一條近址相對位移呼叫指令(call near),後面的4個位元組就是被呼叫函式的相對於呼叫指令的下一條指令的偏移量。在沒有重定位之前,相對偏移被置為0xfffffffc(小端),它是常量「-4」的補碼形式。
讓我們來仔細看看這條指令的含義。緊跟在這條call指令後面的那條指令為add指令,add指令的實際呼叫位址為0x27。我們可以看到0x27存放著並不是swap函式的位址,跟前面的「shared」 一樣,「0xfffffffc」只是乙個臨時的假位址,因為在編譯的時候,編譯器並不知道「swap」的真正位址。
編譯器把這兩條指令的位址部分暫時用位址「0x00000000」和「0xfffffffc」代替著,把真正的位址計算工作留給了鏈結器。我們通過前面的空間和 位址分配可以得知,鏈結器在完成位址和空間分配之後就已經確定了所有符號的虛擬位址了,那麼鏈結器就可以根據符號的位址對每個須要重定位的指令進行地位修正。我們用objdump來反彙編輸出程式「ab」的**段,可以看到main函式的兩個重定位入口都已經被修正到正確的位置:
經過修正之後,「shared」和「swap」的位址分別是0x08049108和0x00000009。關於「shared」很好理解,因為「shared」的變數的位址的卻是0x08049108。對於「swap」來說稍顯晦澀。我們前面介紹過,這個「call」指令的下一條指令是一條近址相對位移呼叫指令,他後面跟的是呼叫指令的下一條指令的偏移量。
那麼鏈結器是怎麼知道哪些指令是要被調整的呢?這些指令的哪些部分要被調整?怎麼調整?比如上面例子中「mov」指令和「ca」指令的調整方式就有所不同。事實上在elf檔案中,有乙個叫重定位表( relocation table)的結構專門用來儲存這些與重定位相關的資訊,我們在前面介紹elf檔案結構時已經提到過了重定位表,它在elf檔案中往往是個或多個段。
對於可重定位的elf檔案來說,它必須包含有重定位表,用來描述如何修改相應的段裡的內容。對於每個要被重定位的elf段都有乙個對應的重定位表,而乙個重定位表往往就是elf檔案中的乙個段,所以其實重定位表也可以叫重定位段,我們在這裡統一稱作重定位表。比如**段「text」如有要被重定位的地方,那麼會有乙個相對應叫「, rel text」的段儲存了**段的重定位表;如果**段「data」有要被重定位的地方,就會有乙個相對應叫「 rel. data"」的段儲存了資料段的重定位表。我們可以使用 objdump來檢視目標檔案的重定位表。
這個命令可以用來檢視「ao」裡面要重定位的地方,即「a.o」所有引用到外部符號的位址。每個要被重定位的地方叫乙個重定入口( relocation entry,我們可以看到「a.o"裡面有兩個重定位入口。重定位入口的偏移(oset)表示該入口在要被重定位的段中的位置,「 relocation records for txt」表示這個重定位表是**段的重定位表,所以偏移表示**段中須要被調整的位置。對照前面的反彙編結果可以知道,這裡的0xlc和0x27分別就是**段中「mov」指令和「call」指令的位址部分.
對於32位的 intel x86系列處理器來說,重定位表的結構也很簡單,它是乙個el32 rel 結構的陣列,每個陣列元素對應乙個重定位入口。ef32rel的定義如下:
在我們通常的觀念裡,之所以要鏈結是因為我們目標檔案中用到的符號被定義在其他目標檔案,所以要將它們鏈結起來。比如我們直接使用ld來鏈結「a.o」,而不將「b.o」作為輸入。鏈結器就會發現 shared和swap兩個符號沒有被定義,沒有辦法完成鏈結工作:
這也是我們平時在編寫程式的時候最常碰到的問題之一,就是鏈結時符號未定義。導致這個問題的原因很多,最常見的一般都是鏈結時缺少了某個庫,或者輸入目標檔案路徑不正確或符號的宣告與定義不一樣。所以從普通程式設計師的角度看,符號的解析佔據了鏈結過程的主要內容.。
通過前面指令重定位的介紹,我們可以更加深層次地理解為什麼缺少符號的定義會導致鏈結錯誤。其實重定位過程也伴隨著符號的解析過程,每個目標檔案都可能定義一些符號也可能引用到定義在其他目標檔案的符號。重定位的過程中,每個重定位的入口都是對乙個符號的引用,那麼當鏈結器須要對某個符號的引用進行重定位時,它就要確定這個符號的目標位址。這時候鏈結器就會去查詢由所有輸入目標檔案的符號表組成的全域性符號表,找到相應的符號後進行重定位。
比如我們檢視「a.o」的符號表:
global」型別的符號,除了「main」函式是定義在**段之外,其他兩個「 shared和「swap」都是「und」,即「 undefined」未定義型別,這種未定義的符號都是因為該目標檔案中有關於它們的重定位項。所以在鏈結器掃瞄完所有的輸入目標檔案之後,所有這些未定義的符號都應該能夠在全域性符號表中找到,否則鏈結器就報符號未定義錯誤。
鏈結器,符號解析與重定位 概念
符號解析。將每個符號引用剛好和乙個符號定義聯絡起來。符號分為四類 匯出符號 export,本地符號 匯入符號 import,外部符號 靜態符號 本地符號 區域性符號 本地符號,不出現在符號表中 匯出符號,在本模組定義,能夠被其他模組引用的符號。非static全域性函式,非static全域性變數。匯入...
鏈結器,符號解析與重定位 概念
符號分為四類 匯出符號 export,本地符號 匯入符號 import,外部符號 靜態符號 本地符號 區域性符號 本地符號,不出現在符號表中 匯出符號,在本模組定義,能夠被其他模組引用的符號。非static全域性函式,非static全域性變數。匯入符號,在其他模組定義,被本模組引用的符號。exter...
PE重定位表解析
在模組被載入到記憶體中,如果該模組沒有裝載到期待的位置,裡面以固定形式而不是以偏移形式硬編碼的位址就需要修正,這樣程式才能被正確載入。例如 call 401203。這個在編譯器編譯的時候將call後面的函式位址以硬編碼的形式固定住,那麼一旦模組不是被載入到40000的基址,而是被載入到100000,...