符號解析與重定位


1.重定位

在完成空間與地址的分配步驟之后,鏈接器就進入了符號解析與重定位的步驟,這也就是靜態鏈接的核心作用;
在分析符號解析和重定位之前,首先讓我們來看看“a.o”里面是怎么使用這兩個外部符號,也就是說我們在“a.c”源程序里面使用了“shared”變量和“swap”函數,那么編譯器在將“a.c”編譯成指令時,它如何訪問“shared”變量?如何調用“swap”函數?
使用objdump的-d參數可以看到“a.o”的代碼反匯編結果:
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”指令的下一條指令是一條近址相對位移調用指令,他后面跟的是調用指令的下一條指令的偏移量。

2. 重定位表

那么鏈接器是怎么知道哪些指令是要被調整的呢?這些指令的哪些部分要被調整?怎么調整?比如上面例子中“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的定義如下:

typedef struct {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
}

3.符號的解析

在我們通常的觀念里,之所以要鏈接是因為我們目標文件中用到的符號被定義在其他目標文件,所以要將它們鏈接起來。比如我們直接使用ld來鏈接“a.o”,而不將“b.o”作為輸入。鏈接器就會發現 shared和swap兩個符號沒有被定義,沒有辦法完成鏈接工作:

這也是我們平時在編寫程序的時候最常碰到的問題之一,就是鏈接時符號未定義。導致這個問題的原因很多,最常見的一般都是鏈接時缺少了某個庫,或者輸入目標文件路徑不正確或符號的聲明與定義不一樣。所以從普通程序員的角度看,符號的解析占據了鏈接過程的主要內容.。

通過前面指令重定位的介紹,我們可以更加深層次地理解為什么缺少符號的定義會導致鏈接錯誤。其實重定位過程也伴隨着符號的解析過程,每個目標文件都可能定義一些符號也可能引用到定義在其他目標文件的符號。重定位的過程中,每個重定位的入口都是對一個符號的引用,那么當鏈接器須要對某個符號的引用進行重定位時,它就要確定這個符號的目標地址。這時候鏈接器就會去查找由所有輸入目標文件的符號表組成的全局符號表,找到相應的符號后進行重定位。

比如我們查看“a.o”的符號表:

GLOBAL”類型的符號,除了“main”函數是定義在代碼段之外,其他兩個“ shared和“swap”都是“UND”,即“ undefined”未定義類型,這種未定義的符號都是因為該目標文件中有關於它們的重定位項。所以在鏈接器掃描完所有的輸入目標文件之后,所有這些未定義的符號都應該能夠在全局符號表中找到,否則鏈接器就報符號未定義錯誤。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM