詳解靜態連結的過程
本文是“編譯那點事”系列的第二篇,主要詳細介紹靜態連結的過程,方便程式入門者進行檢視。
- 原始碼
本文示例程式碼如下,分為a.c 和b.c檔案
a.c檔案如下
/* a.c */
extern int shared;
int main(){
int a = 100;
swap(&a, &shared);
}
b.c檔案如下
/* b.c */
int shared = 1;
void swap(int *a, int *b){
*a ^= *b ^= *a ^= *b;
}
分別使用gcc -c a.c -o a.o 和 gcc -c b.c -o b.o 生成.o檔案,之後將兩個.o檔案進行連結,連結命令如下:
ld a.o b.o -e main -o ab
使用objdump -h 分別檢視a.o, b.o 和ab可執行檔案,發現以下兩點:
- a.o 和 b.o檔案中的相同段都會合並,比如.text, .data, 其按照的是邏輯是相似度合併,而不是按照順序進行合併,這樣做能夠節約很大一部分記憶體空間。
- VMA即虛擬空間地址都是0X00000000,這是因為還沒有進行連結,連結通常是分為兩步:一是空間和地址的分配;二是符號解析與重定位
檢視ab可執行檔案得到結果如下:
可以看到所有段的VMA不再是0x0000000,而是有了具體的值。這一步的本質是:連結器按照相似段分配空間的辦法,確定各個.o檔案中的各個段在連結後的虛擬地址,由於我這裡使用的64位機器
之後連結器開始計算各個符號的虛擬地址。我們在連結時,使用了-e main, 其表示將main函式作為程式入口。假設a.o檔案中的main函式相對於a.o的”.text”偏移量是X,那麼在連結以後,當a.o檔案的.text位於虛擬地址的0x00000000004000e8時,那麼main的虛擬地址則為0x00000000004000e8 + X(注意進位制)。
符號解析和重定位
在分析符號解析和重定位之前,我們看下a.o檔案是如何呼叫兩個外部變數(shared, swap)的,對a.o檔案進行反彙編,得到如下結果:
Disassembly of section .text:
0000000000000000 <main>:
0 : 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: be 00 00 00 00 mov $0x0,%esi
18: 48 89 c7 mov %rax,%rdi
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 00 00 00 00 callq 25 <main+0x25>
25: c9 leaveq
26: c3 retq
其中在偏移為0x13處的彙編為be 00 00 00 00, shared的地址暫時就為00000000; 在偏移為0x20處的彙編為e8 00 00 00 00, swap的地址也暫時為00000000。
可以看到,編譯器對於這兩個外部變數的地址,暫時都用”0x00000000”來代替,把真正計算地址的工作交給了連結器。我們同樣反彙編ab檔案得到如下結果:
ab: file format elf64-x86-64
Disassembly of section .text:
00000000004000e8 <main>:
4000e8: 55 push %rbp
4000e9: 48 89 e5 mov %rsp,%rbp
4000ec: 48 83 ec 10 sub $0x10,%rsp
4000f0: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
4000f7: 48 8d 45 fc lea -0x4(%rbp),%rax
4000fb: be b8 01 60 00 mov $0x6001b8,%esi
400100: 48 89 c7 mov %rax,%rdi
400103: b8 00 00 00 00 mov $0x0,%eax
400108: e8 03 00 00 00 callq 400110 <swap>
40010d: c9 leaveq
40010e: c3 retq
40010f: 90 nop
可以看到,經過連結器的修正,按照小端位元組序,”shared”的地址為0x6001b8,其為絕對地址,非常好理解。 “swap”的地址為0x00000003,注意到callq指令為一條近址相對位移呼叫指令,callq的下一條指令地址為0x40010d, 其加上0x000003,剛好即為0x400110,可以看到swap的絕對地址剛好就是它!
重定位表
那麼我們會好奇,連結器怎麼知道.text中有兩個符號需要重定位呢?答案就隱藏在重定位表中。對於每個可重定位的ELF檔案來說,它必須包含有重定位表,用來描述如何修改相應的段中的內容。程式碼段和資料段分別都會有其對應的可重定位表,分別叫做.rela.text或者.rela.data。我們可以使用命令objdump -r a.o看看a.o檔案中的重定位表如下:
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000014 R_X86_64_32 shared
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
從上面的offset欄位可以看到,在.text中的shared符號和swap符號需要被重定位,offset表示它們在.text的偏移量是多少。跟上面a.o反彙編結果進行對比,發現確實是在偏移量為0x14和0x21處!
符號解析
在程式設計過程中,我們經常會遇到”undefined to reference…”此類bug,這個表示連結時符號未定義,一般錯誤原因在於缺少某個庫或者輸入目標檔案路徑不正確或者符號宣告與定義不一致。可重定位表裡麵包含了需要重定位的符號以及在什麼地方。除此之外,我們通過符號表.symtab,也可以檢視到有哪些符號需要被重定位。例如,我們看a.o的符號表: readelf -s a.o:
在其中,我們可以看到shared和swap都是屬於undefined,所以需要被重定位。
總結
在靜態連結過程中,連結器首先會進行地址和空間的重定位,然後會通過符號表,可重定位表解析符號,找到需要被重定位的符號,然後進行符號的重定位。更通俗一點講,如同拼圖遊戲一樣,先完成基本輪廓的拼圖,中間偶爾會空幾個難解的圖塊,我們先暫時不管(類似於編譯器直接假設地址為0),可以在一張表中記錄其缺失位置,然後會剩下幾個難解的圖塊,你可以給它們也編一個號,和上表進行關聯,最後把對照它們的位置資訊,把圖塊補上,即對應於符號的重定位和解析了。