連接器,是把目標文件連接成可執行文件或動態庫的工具。
它是將高級語言代碼轉化成二進制程序的最后一步。
編譯之后的目標文件里,函數和全局變量的地址并不是真實內存地址,而是一個重定位符號。
連接器的作用,就是把這些重定位符號處理成真實的內存地址。
int printf(const char* fmt, ...);
int main()
printf("hello world");
return 0;
這段代碼在編譯時有2個沒法確定的數據:一是printf()函數的地址,二是字符串常量"hello world"的地址。
printf()函數是個庫函數,它的地址可以在動態庫里,也可以在靜態庫里,還可以在其他.o文件里,編譯器是沒法提前知道的。
字符串常量"hello world"是一個全局常量,它要放在.rodata數據段里。
.rodata數據段的位置編譯器也是沒法確定的,因為最終可能是多個目標文件連接成1個可執行程序,.rodata數據段的具體位置需要連接器來確定。
所以,編譯器就在生成.o文件時就添加1個重定位節、1個符號表,他們包含2個重定位信息:printf()和"hello world"。
然后,由連接器去重寫真實的內存地址。
上面代碼用gcc -c編譯成.o文件之后,用readelf -a查看它的信息,如下圖:
ELF頭
從ELF頭可以看出,編譯后的文件是可重定位文件,運行的系統架構是x86_64。
從它各個節的列表里可以找到.rela.text重定位節和.rodata節,前者存儲重定位信息,后者存儲常量數據。
各個節的列表
重定位節.rela.text的內容有2條:
1,一個指向.rodata節,表示這條重定位的地址在.rodata段里。
2,另一個沒有具體的節,但給了一個函數名puts,表示要找的是這個函數(gcc在編譯時都是把printf轉化成puts函數)。
重定位節和符號表
在上圖的符號表.symtab節里,也可以找到這2條信息:
1,其中的第5條(從0開始)就是"hello world"字符串的信息:它是一個LOCAL的字符串,也就是它的數據在當前文件里的某個節(SECTION),這個節的索引號是5(Ndx列)。
去上面的節列表里查找,可以發現.rodata段確實是第5個節。
2,第11條就是puts()函數的信息,它是GLOBAL的全局函數,不在當前文件的某個節里(Ndx是UND,undefined),需要連接器去其他地方找(庫文件、其他.o文件,etc)。
Ndx這一列表示重定位數據所在的節,當前文件里實現的函數或變量都有節的索引號,但外部全局函數的索引號都是不確定的(UND)。
代碼段,main函數的機器碼
從代碼段.text里的main()函數的機器碼可以看出,裝載"hello world"字符串的指令和調用printf()的指令里的地址都是00 00 00 00。
也就是說,這里需要的真實內存地址是32位的整數,有待連接器進一步填寫。
00 00 00 00也就是高級語言里的NULL,在代碼里都是無效的內存地址,如果不重填的話肯定會發生段錯誤。
lea指令裝載全局變量時使用的內存地址,是變量地址與當前指令地址的偏移量。
rip,指令指針寄存器,它存的是當前指令的地址,x86_64對全局變量的尋址,都是使用的這種方式。
如果是靜態連接,連接器把靜態庫.a和main函數的.o文件合在一起,然后修改這兩個地址就可以了。
如果是動態連接,還需要用到全局偏移量表(GOT,global offset table)和PLT(過程連接表,procedure linkage table)。
動態連接之后的ELF頭
gcc動態連接之后生成的可執行文件。
以前gcc都是生成可執行文件EXEC,現在都是生成動態庫DYN直接運行了(即使main函數所在的文件也這樣)。
上圖ELF頭可以看出類型是DYN,入口地址是0x530。
節的列表
動態鏈接之后文件有特別多的節,其中以.dyn開頭的都是動態庫相關的節。
.plt、.plt.got、.got,這3個就是動態連接所必須的節。
.rela.plt和.rodata依然存在,內容和靜態連接得差不多。
所需的動態庫信息
因為程序運行時要首先加載所需的動態庫,所以必須含有動態庫的信息,如上圖。
這個程序比較簡單,只需要libc.so.6庫。
以下兩圖是重定位節的內容和動態庫支持的庫函數列表,可以看到他們都包含puts()函數,即main()函數所需的printf()。
重定位節
動態庫函數的信息
最后簡單說一下plt和got的內容:
plt分為2個節.plt和.plt.got。
.plt是只讀的可執行代碼,.plt.got是可寫的數據。
操作系統不允許在運行時修改代碼,只允許在運行時修改數據,所以動態連接的程序要想獲得庫函數的地址必須要一個小技巧[呲牙]
加載器必須把庫函數的地址放在一個全局的函數指針變量里,然后讓一段過渡代碼去調用這個函數指針,從而實現動態運行。
這個全局的函數指針就是.plt.got里的一項。
當程序需要多個庫函數時,這些函數指針就形成了一個函數指針數組,這就是.plt.got表。
調用(多個)庫函數的過渡代碼數組就是.plt表:它是有運行權限的,而且是只讀的。
如下圖:
1,最開始的時候,這個函數指針是加載器的加載函數。
2,當第一次調用puts()函數,加載函數會去動態庫里查找它的真實地址,并填寫在這里。
3,之后再調用時,就直接調用puts()函數了。
這是linux系統動態庫函數的需求加載機制。
如果是普通變量,把它的地址放在.got表里就行。
動態庫函數的需求加載