程序链接之符号解析和重定位
一、空間與地址分配
鏈接器在連接過程中的工作就是把多個輸入的目標文件加工合并成一個輸出文件。有幾種不同的方案:
按序疊加
按序疊加可以說是最簡單的一個方案,就是將輸入的目標文件按照次序疊加起來。
?從圖中可以看到,在有很多輸入文件的情況下,輸出文件將有很多零散的段。因為每個段都要遵循空間對齊,所以這樣會占用大量空間。比如,一個段長度只有1字節,但是按照空間對齊,其在內存也要占用4096個字節。
相似段合并
為了解決按需疊加所帶來的問題,引入了相似段合并這個概念,就是把相同性質的段合并到一起。.bss段其實在目標文件和可執行文件中并不占用文件的空間(不占用磁盤空間),但是在裝載時是要占用地址空間的。
很多書中提到的“鏈接器為目標文件分配地址和空間”,這一句話其實是有兩個含義的。對于.text和.data這樣有實際數據的段,鏈接器在文件中和虛擬地址中都要分配空間。但是對于.bss這樣的段,在文件中就不必分配空間,目的是節約磁盤空間。
目前的鏈接器都不采用按序疊加的方法,而是采用相似段合并的方法。
鏈接器采用的是相似段合并的方法,使用這種方法的鏈接器一般都采用兩步鏈接的方法。
第一步 空間與地址分配:鏈接器獲取所有目標文件的段長度,將他們合并,計算出輸出文件中各個段合并后的長度與位置,建立映射關系。
第二步 符號解析與重定位:使用上一步所收集到的信息,讀取輸入文件中段的數據、重定位信息、進行符號解析和重定位、調解代碼中的地址。
鏈接器前后的程序所用的地址其實已經是程序應該在進程中使用的虛擬地址。Linux下一般從0x08048000開始分配。地址確定很簡單,基址地址(0x08048000)+偏移地址(符號在進程中的地址)。
二、符號
鏈接的本質就是把多個不同的目標文件之間相互“粘”到一起,就像是拼圖一樣,把每一塊“目標文件”拼接成一個完整的“程序”。
在鏈接中,目標文件之間相互拼合實際上就是目標文件之間地址的引用,即對函數和變量的地址的引用。將函數和變量統稱為符號,函數名或變量名就是符號名。
符號的類型
定義在本目標文件的全局符號:可被其他目標文件引用
外部符號:在本目標文件中引用的全局符號
段名:由編譯器產生,值為該段的起始地址。
局部符號:只在編譯單元內部可見,其他目標文件不可見
特殊符號
在linux下使用ld作為鏈接器來鏈接可執行文件時,定義很多符號可以引用。這些符號稱為特殊符號。以下是幾個很具有代表性的特殊符號:
__executable_strat:代碼段的起始地址
__etext或_etext或etext:代碼段的結束地址
_edata或edata:數據段的結束地址
_end或end:程序的結束
ELF符號表結構
typedef struct
{
?? ?Elf32_Word st_name;
?? ?Elf32_Addr st_value;
?? ?Elf32_Word st_size;
?? ?unsigned char st_info;
?? ?unsigned char st_other;
?? ?Elf32_Half st_shndx;
}Elf32_Sym
符號修飾與函數簽名
為了避免和庫文件中的符號發生符號沖突,就出現了符號修飾機制。
UNIX下的C語言規定,C語言中的符號經過編譯后需要在在符號名前加上下劃線“_“。
int func()
?? ?{
?? ??? ?...
?? ?}
以上函數名被修飾成 “_func”。
在C++中則增加了名稱空間 ?namespace
C++中的符號修飾
因為C++支持重載的特征,所以函數符號修飾相對復雜,引入術語函數簽名來表示C++中函數的符號修飾。函數簽名包含了一個函數的信息,包括函數名、參數類型、所在類、以及名稱空間。
linux下函數簽名的規則:所有符號都以_Z開頭,在名稱空間或類中后面緊跟N,再以E結尾。比如一個名稱空間foo中的全局變量bar就會被修飾為_ZN3foo3barE。
C++解決與C的兼容問題——extern
C和C++中的符號修飾是不同的,所以就存在不兼容問題,為了解決這個兼容問題,C++中有個用來聲明或定義一個C符號的extern “C” 關鍵字用法。以下是一個示例:
extern “C”只能定義在全局范圍,不能定義在函數內
extern "C"
{
?? ?int func(int);
?? ?int var;
}
同時C++編譯器會在編譯C++文件時默認定義一個宏**“__cplusplus”**,來使得能夠兼容C語言的頭文件,這也是能在C++中使用#include<stdio.h>的原因。
強符號和弱符號
在編程中會出現多個目標文件中含有相同名字全局符號的定義,這種情況就叫符號重復定義。為了解決這個問題,引入了強符號和弱符號規則。在C/C++語言中,編譯器默認函數和初始化了的全局變量為強符號,未初始化的全局變量為弱符號。以下是發生符號重復定義的處理規則:
強符號與強符號之間:編譯器報錯
強符號與弱符號之間:編譯器選擇強符號
弱符號與弱符號之間:編譯器選擇其中占用空間最大的一個
強引用和弱引用
在編譯器對引用的外部符號進行決議時,如果沒有找到該符號定義,編譯器就會報符號未定義錯誤的稱之為強引用,如果沒有找到該符號定義,編譯器就默認其為0的稱之為弱引用。
庫中定義的弱符號可以被用戶定義的強符號所覆蓋,使得用戶可以讓程序使用自定義版本的庫函數。
三、符號解析與重定位
重定位
#include "func.c" int main() {func(); }在這段代碼中的函數func()定義在其他文件中,所以編譯器就暫時把地址0看做是“func()”的地址。等到鏈接器在完成地址和空間分配之后就可以確定所有符號的虛擬地址(包括func),那么鏈接器就可以根據重定位表對每個需要重定位的符號進行地址修正。
符號解析
鏈接是因為在目標文件中用到的符號被定義在其他目標文件。這也是編譯過程中出現“undefined symbol”這類編譯錯誤的原因。
重定位的過程往往也伴隨著符號的解析過程,每個重定位的入口都是對一個符號的引用,那么當鏈接器需要對某個符號的引用進行重定位時,它就要確定這個符號的目標地址,這時候鏈接器就會去查找由所輸入目標文件的符號表組成的全局符號表,找到相應的符號進行重定位。
總結
以上是生活随笔為你收集整理的程序链接之符号解析和重定位的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 时序分析基本概念(一)——建立时间
- 下一篇: 一份工作,坚持多久跳槽最合适?