參考文獻:
《程序員的自我修養---鏈接、裝載與庫》第4章 靜態鏈接
開發平台:
[thm@tanghuimin static_link]$ uname -a Linux tanghuimin 2.6.32-358.el6.x86_64 #1 SMP Fri Feb 22 00:31:26 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
1.ELF文件格式概貌
readelf -h 查看elf文件頭部信息可以看到Type值有三種:REL,EXEC,DYN。
REL文件是只被編譯沒有被鏈接過的文件,其格式屬於左邊一種,elf header+section1,2,3...+section header table,每個section對應一個section header table entry,section header table為各個section提供索引。沒有被鏈接過的文件沒有program header,不能被加載到內存中運行,readelf -l會提示”There are no program headers in this file”。
EXEC和DYN文件屬於被鏈接過的文件,其格式屬於右邊一種,elf header+program header table+segment1,2,3...+section header table。每個segment對應一個program header table entry,program header table為各個segment提供索引。EXEC和DYN文件有program headers,可以被加載到內存中運行,readelf -l可以看到一個segment是由一個或多個section構成,Type為LOAD的segment可以被加載到內存中運行,其他類型的segment提供輔助信息。
2.實例分析
(1)創建文件
創建文件common.c
int val = 1; int func(void) { return (val+10); }
創建文件test.c
extern int val; extern int func(void); int main() { val = 10; func(); return 0; }
(2)編譯
編譯兩個.c文件
gcc -c test.c
gcc -c common.c
生成的test.o和common.o屬於REL類型
來分析一下編譯后生成的REL文件
(2.1)先看看test.o:
readelf -s test.o查看test.o的符號表
[thm@tanghuimin static_link]$ readelf -s test.o Symbol table '.symtab' contains 11 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 26 FUNC GLOBAL DEFAULT 1 main 9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND val 10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND func
因為val和func不是在test.c中定義的,所以這兩個符號的Ndx(符號所在section的index)為UND。為了能讓程序順利執行,我們希望在未來鏈接的過程中可以從其他文件中找到val和func這兩個符號,並確定這兩個符號的地址,確定未定義符號的地址的過程即是“重定位”(relocation)。
readelf -S test.o可以看到test.o的section header table
[thm@tanghuimin static_link]$ readelf test.o -S There are 12 section headers, starting at offset 0x128: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 000000000000001a 0000000000000000 AX 0 0 4 [ 2] .rela.text RELA 0000000000000000 00000548
0000000000000030 0000000000000018 10 1 8 [ 3] .data PROGBITS 0000000000000000 0000005c 0000000000000000 0000000000000000 WA 0 0 4 [ 4] .bss NOBITS 0000000000000000 0000005c 0000000000000000 0000000000000000 WA 0 0 4 [ 5] .comment PROGBITS 0000000000000000 0000005c 000000000000002d 0000000000000001 MS 0 0 1 [ 6] .note.GNU-stack PROGBITS 0000000000000000 00000089
0000000000000000 0000000000000000 0 0 1 [ 7] .eh_frame PROGBITS 0000000000000000 00000090
0000000000000038 0000000000000000 A 0 0 8 [ 8] .rela.eh_frame RELA 0000000000000000 00000578
0000000000000018 0000000000000018 10 7 8 [ 9] .shstrtab STRTAB 0000000000000000 000000c8 0000000000000059 0000000000000000 0 0 1 [10] .symtab SYMTAB 0000000000000000 00000428
0000000000000108 0000000000000018 11 8 8 [11] .strtab STRTAB 0000000000000000 00000530
0000000000000016 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
我們重點關注rela section。
可以看到rela.text entry的描述中,link=10,info=1,link表示被重定位的符號所在的符號表的section index,info表示需要被重定位的section的index,通俗點講就是,將來有朝一日我知道了該符號的地址,我該把這個地址寫到哪個section里面去,這里是.text。
readelf -r test.o可以看到rel section里的詳細信息。
[thm@tanghuimin static_link]$ readelf test.o -r Relocation section '.rela.text' at offset 0x548 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000006 000900000002 R_X86_64_PC32 0000000000000000 val - 8 00000000000f 000a00000002 R_X86_64_PC32 0000000000000000 func - 4 Relocation section '.rela.eh_frame' at offset 0x578 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
offset表示該符號在被重定位的section中的偏移,info的高4個字節表示該符號在.symtab中的index,低4字節表示重定位的類型,不同的類型計算目標地址的方法不一樣。
綜上所述,我們可以得出符號val和func的種種信息:
val的重定位地址是在.text的偏移為6處,將來的鏈接過程中,連接器要將val的地址寫到這個位置上來,val在.symtab中的index為9。
func的重定位地址是在.text的偏移為f處,將來的鏈接過程中,連接器要將func的地址寫到這個位置上來,func在.symtab中的為a。
關於重定位的類型,《ELF V1.2》的第57和93頁有詳細說明。
這里兩個符號的類型 R_X86_64_PC32,重定位地址的計算方法為S+A-P,即符號地址和下條指令間的偏移量。
objdump -S test.o查看匯編文件
[thm@tanghuimin static_link]$ objdump -S test.o test.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: c7 05 00 00 00 00 0a movl $0xa,0x0(%rip) # e <main+0xe> b: 00 00 00 e: e8 00 00 00 00 callq 13 <main+0x13>
13: b8 00 00 00 00 mov $0x0,%eax 18: c9 leaveq 19: c3 retq
可以看到.text中偏移6處四個字節(val的地址)為全0,偏移f處四個字節(func的地址)為全0。
(2.2)再來看看common.o:
readelf -s查看common.o的符號表
...... 8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 val 9: 0000000000000000 15 FUNC GLOBAL DEFAULT 1 func
可以看到val定義在index為3的.data里,func定義在index為1的.text里,這兩個符號都是在common.c文件內部定義的。
readelf -S查看common.o的section header table
...... [ 2] .rela.text RELA 0000000000000000 00000528
0000000000000018 0000000000000018 10 1 8 ......
readelf -r查看common.o的重定位詳細信息
Relocation section '.rela.text' at offset 0x528 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000006 000800000002 R_X86_64_PC32 0000000000000000 val – 4 ......
以上信息可以得出,需要被重定位的符號是val,它在.symtab中的index為8,需要被重定位的地址是在.text中偏移為6處,重定位類型為 R_X86_64_PC32,即.text偏移為6處的地址是val地址和下一條指令間的偏移。
Objdump -S查看common.o的匯編文件:
...... 0000000000000000 <func>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # a <func+0xa> a: 83 c0 0a add $0xa,%eax d: c9 leaveq e: c3 retq
可以看到偏移為6處的四個字節(val的地址)全為0,需要在鏈接的時候寫入val的地址。
(3)鏈接
將兩個.o文件鏈接,
gcc -o test test.o common.o
生成的test為EXEC類型
靜態鏈接的過程引用《程序員的自我修養》第101頁的概述:
第一步:空間與地址分配
掃描所有的輸入目標文件,獲得它們的各個段的長度、屬性和位置,並且將輸入目標文件中的符號表中所有的符號定義和符號引用收集起來,統一放到一個全局符號表。這一步中,連接器將能獲得所有輸入目標文件的段長度,並且將它們合並,計算出輸出文件中各個段合並后的長度與位置,並建立映射關系。
第二步:符號解析與重定位
使用上面第一步中收集到的所有信息,讀取輸入文件中段的數據、重定位信息,並且進行符號解析與重定位、調整代碼中的地址等。事實上第二步是鏈接過程的核心,特別是重定位的過程。
提取關鍵字可以是:合並段,全局符號表,重定位
來看看重定位之后的test文件
readelf -l查看test進程在內存中的映像分布:
...... LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000664 0x0000000000000664 R E 200000 LOAD 0x0000000000000668 0x0000000000600668 0x0000000000600668
0x00000000000001e8 0x00000000000001f8 RW 200000 ......
可以看到text segment被映射到虛擬地址0x400000處,data segment被映射到虛擬地址0x600668處。
readelf test -s查看test的符號表
...... 54: 000000000060084c 4 OBJECT GLOBAL DEFAULT 24 val ...... 57: 0000000000400490 15 FUNC GLOBAL DEFAULT 13 func ...... 64: 0000000000400474 26 FUNC GLOBAL DEFAULT 13 main ......
反匯編
objdump -S test > test.S
...... 0000000000400474 <main>: 114 400474: 55 push %rbp 115 400475: 48 89 e5 mov %rsp,%rbp 116 400478: c7 05 ca 03 20 00 0a movl $0xa,0x2003ca(%rip) # 60084c <val>
117 40047f: 00 00 00
118 400482: e8 09 00 00 00 callq 400490 <func>
119 : b8 00 00 00 00 mov $0x0,%eax 120 40048c: c9 leaveq 121 40048d: c3 retq 122 40048e: 90 nop 123 40048f: 90 nop 124
125 0000000000400490 <func>: 126 400490: 55 push %rbp 127 400491: 48 89 e5 mov %rsp,%rbp 128 400494: 8b 05 b2 03 20 00 mov 0x2003b2(%rip),%eax # 60084c <val>
129 40049a: 83 c0 0a add $0xa,%eax 130 40049d: c9 leaveq 131 40049e: c3 retq 132 40049f: 90 nop ......
main函數中
地址0x400478處:
400478: c7 05 ca 03 20 00 0a movl $0xa,0x2003ca(%rip) # 60084c <val>
%rip+0x2003ca=0x400482+0x2003ca=0x60084c=val的地址
地址0x400482處:
118 400482: e8 09 00 00 00 callq 400490 <func>
該條指令的下一條指令地址為0x400487,0x400487+0x09=0x400490=func的地址
func中
地址0x 400494處:
128 400494: 8b 05 b2 03 20 00 mov 0x2003b2(%rip),%eax # 60084c <val>
%rip+ 0x2003b2= 0x40049a+0x2003b2=0x60084c=val的地址
由此可見這三處重定位的地址都為符號地址與下條指令間的偏移,符合上面分析的重定位類型。