靜態(tài)鏈接
當(dāng)有兩個(gè)目標(biāo)文件時(shí),如何將它們連接起來形成一個(gè)可執(zhí)行文件敢会?其中發(fā)生了什么曾沈?
使用兩個(gè)源代碼文件作為研究例子:
a.c
extern int shared;
int main()
{
int a = 100;
swap(&a,&shared);
}
b.c
int shared = 1;
void swap(int *a,int *b)
{
*a^=*b^=*a^=*b;
}
使用gcc將兩個(gè)文件編譯成目標(biāo)文件a.o和b.o
gcc -c a.c b.c
圖示:
b.c里面定義了兩個(gè)全局符號,
- shared
- 函數(shù)swap
a.c里面定義了一個(gè)全局符號
- main
a.c引用到了b.c里面的swao與shared鸥昏,接下來就是把這兩個(gè)目標(biāo)文件鏈接在一起最終形成一個(gè)可執(zhí)行文件
空間與地址分配
可執(zhí)行文件中代碼段和數(shù)據(jù)段都是輸入的目標(biāo)文件合并而來的晦譬,那么鏈接過程產(chǎn)生的第一個(gè)問題:對于多個(gè)輸入目標(biāo)文件,鏈接器如何將它們的各個(gè)段合并到輸出文件互广?
按序疊加
一個(gè)方案就是按照輸入次序疊加起來:
這種做法很簡單敛腌,但是有一個(gè)問題:
在很多輸入文件的時(shí)候,輸出文件將會(huì)有很多零散的段惫皱,這種做法很浪費(fèi)空間像樊,因?yàn)槊總€(gè)段都需要有一定的地址和空間對齊要求,而且還會(huì)造成碎片問題旅敷,所以這不是一個(gè)好的方案
相似段合并
一個(gè)更實(shí)際的方法是將相同性質(zhì)的段合并到一起生棍,比如將所有輸入文件的.text合并到輸出文件的.text,接著是.data,.bss
圖示:
.bss段在目標(biāo)文件和可執(zhí)行文件并不占用文件空間媳谁,但是在裝載時(shí)占用地址空間涂滴,所以鏈接器在合并各個(gè)段的時(shí)候,需要將.bss也合并晴音,并且分配虛擬空間
鏈接器為目標(biāo)文件分配地址和空間中柔纵,地址和空間有兩個(gè)含義:
一個(gè)是在輸出的可執(zhí)行文件中的空間
一個(gè)是在裝載后的虛擬地址中的虛擬地址空間
對于有實(shí)際數(shù)據(jù)的段,如.text和.data,它們在文件中和虛擬地址中都有分配空間锤躁,因?yàn)樗鼈冊谶@兩者中都存在搁料,而對于.bss這樣的段來說,分配空間的意義只局限于虛擬地址空間,因?yàn)樗谖募胁]有內(nèi)容
現(xiàn)在鏈接器空間分配策略采用第二種方法郭计,使用這種方法的鏈接器一般采用兩步鏈接的方法:
- 空間和地址分配
掃描所有的輸入目標(biāo)文件霸琴,并且獲得它們的各個(gè)段的長度,屬性昭伸,位置梧乘,并且將輸入目標(biāo)文件中的符號表中所有的符號定義和符號引用收集起來,統(tǒng)一放在一個(gè)全局符號表庐杨。這一步宋下,鏈接器能夠獲得所有輸入目標(biāo)文件的段長度,并且將它們合并辑莫,計(jì)算出輸出文件中各個(gè)段合并后的長度和位置学歧,并建立映射關(guān)系。
- 符合解析與重定位
使用上面的第一步中收集到的所有信息各吨,讀取輸入文件中段的數(shù)據(jù)枝笨,重定位信息,并且進(jìn)行符合解析與重定位揭蜒,調(diào)整代碼中的地址等横浑。
用鏈接器將a.o和b.o鏈接起來:
ld a.o b.o -e main -o ab
接下來使用objdump來查看鏈接前后地址的分配情況:
objdump -h a.o
輸出:
a.o: 文件格式 elf64-x86-64
節(jié):
Idx Name Size VMA LMA File off Algn
0 .text 00000051 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000091 2**0
ALLOC
3 .comment 0000002a 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000bb 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
objdump -h b.o
輸出
b.o: 文件格式 elf64-x86-64
節(jié):
Idx Name Size VMA LMA File off Algn
0 .text 0000004b 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 0000008c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000090 2**0
ALLOC
3 .comment 0000002a 0000000000000000 0000000000000000 00000090 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000ba 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
objdump -h ab
輸出:
ab: 文件格式 elf64-x86-64
節(jié):
Idx Name Size VMA LMA File off Algn
13 .text 00000222 0000000000000560 0000000000000560 00000560 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000009 0000000000000784 0000000000000784 00000784 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 00000004 0000000000000790 0000000000000790 00000790 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame_hdr 00000044 0000000000000794 0000000000000794 00000794 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .eh_frame 00000128 00000000000007d8 00000000000007d8 000007d8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .init_array 00000008 0000000000200db8 0000000000200db8 00000db8 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000008 0000000000200dc0 0000000000200dc0 00000dc0 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .dynamic 000001f0 0000000000200dc8 0000000000200dc8 00000dc8 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000048 0000000000200fb8 0000000000200fb8 00000fb8 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .data 00000014 0000000000201000 0000000000201000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .bss 00000004 0000000000201014 0000000000201014 00001014 2**0
ALLOC
24 .comment 00000029 0000000000000000 0000000000000000 00001014 2**0
CONTENTS, READONLY
//省略了剩余內(nèi)容
上述輸出中:VMA:虛擬地址,LMA:加載地址
鏈接前后的程序中所使用的的地址已經(jīng)是程序在進(jìn)程中的虛擬地址屉更,即各個(gè)段里面的VMA和Size徙融,可以看到,在鏈接之前瑰谜,目標(biāo)文件中所有段的VMA都是0欺冀,因?yàn)樘摂M地址空間還沒分配,所以默認(rèn)為0萨脑,鏈接之后隐轩,可執(zhí)行文件ab中各個(gè)段都被分配到了相應(yīng)的虛擬地址,
符號地址的確定
在第一步的掃描和空間分配階段渤早,鏈接器按照上面的空間分配方法進(jìn)行分配职车,這時(shí)候輸入文件中各個(gè)段在鏈接后的虛擬地址就已經(jīng)確定了。
當(dāng)前面這步完成后鹊杖,鏈接器開始計(jì)算各個(gè)符號的虛擬地址悴灵,因?yàn)楦鱾€(gè)符號在段內(nèi)的相對位置是固定的,只不過鏈接器要給每個(gè)符號加上一個(gè)偏移量骂蓖,使得它們能夠調(diào)整到正確的虛擬地址积瞒。
符號解析與重定位
重定位
完成空間與地址分配后,鏈接器就進(jìn)入了符號解析與重定位的步驟
在分析符號解析與重定位之前涯竟,先看看a.o里面是怎樣使用這兩個(gè)外部符號的赡鲜,也就是源代碼里面的shared變量以及swap函數(shù)
編譯器將a.c編譯成指令后空厌,如何訪問shared和調(diào)用swap庐船?
使用objdump的-d參數(shù)可以查看反匯編結(jié)果
objdump -d a.o
a.o: 文件格式 elf64-x86-64
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: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
f: 00 00
11: 48 89 45 f8 mov %rax,-0x8(%rbp)
15: 31 c0 xor %eax,%eax
17: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%rbp)
1e: 48 8d 45 f4 lea -0xc(%rbp),%rax
22: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 29 <main+0x29>
29: 48 89 c7 mov %rax,%rdi
2c: b8 00 00 00 00 mov $0x0,%eax
31: e8 00 00 00 00 callq 36 <main+0x36>
36: b8 00 00 00 00 mov $0x0,%eax
3b: 48 8b 55 f8 mov -0x8(%rbp),%rdx
3f: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
46: 00 00
48: 74 05 je 4f <main+0x4f>
4a: e8 00 00 00 00 callq 4f <main+0x4f>
4f: c9 leaveq
50: c3 retq
程序代碼里面使用的都是虛擬地址银酬,可以發(fā)現(xiàn)main的起始地址為 0x00000000,這是因?yàn)樵谖催M(jìn)行空間分配之前筐钟,目標(biāo)文件代碼段中的起始地址以 0x00000000開始揩瞪,等到空間分配完成以后,各個(gè)函數(shù)才會(huì)確定自己在虛擬地址中的位置
上邊反匯編輸出中:
- 最左邊那列是每條指令的偏移量篓冲。一行代表一條指令
接著反匯編輸出ab的代碼段李破,可以發(fā)現(xiàn)main函數(shù)的兩個(gè)重定位入口被修正到了正確的位置:
objdump -d ab
000000000000066a <main>:
66a: 55 push %rbp
66b: 48 89 e5 mov %rsp,%rbp
66e: 48 83 ec 10 sub $0x10,%rsp
672: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
679: 00 00
67b: 48 89 45 f8 mov %rax,-0x8(%rbp)
67f: 31 c0 xor %eax,%eax
681: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%rbp)
688: 48 8d 45 f4 lea -0xc(%rbp),%rax
68c: 48 8d 35 7d 09 20 00 lea 0x20097d(%rip),%rsi # 201010 <shared>
693: 48 89 c7 mov %rax,%rdi
696: b8 00 00 00 00 mov $0x0,%eax
69b: e8 1b 00 00 00 callq 6bb <swap>
6a0: b8 00 00 00 00 mov $0x0,%eax
6a5: 48 8b 55 f8 mov -0x8(%rbp),%rdx
6a9: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
6b0: 00 00
6b2: 74 05 je 6b9 <main+0x4f>
6b4: e8 87 fe ff ff callq 540 <__stack_chk_fail@plt>
6b9: c9 leaveq
6ba: c3 retq
00000000000006bb <swap>:
重定位表
鏈接器如何知道哪些指令需要被調(diào)整?這些指令的哪些部分要被調(diào)整壹将?怎么調(diào)整嗤攻?
在ELF文件中,有一個(gè)叫做重定位表的結(jié)構(gòu)專門用來保存這些與重定位相關(guān)的信息
對于可重定位的ELF文件來說诽俯,它必須具有重定位表妇菱,用來描述如何修改相應(yīng)的段里的內(nèi)容,而對于每個(gè)要被重定位的ELF段都有一個(gè)對應(yīng)的重定位表暴区,這一個(gè)重定位表往往就是ELF文件中的一個(gè)段闯团,比如代碼段.text如有要被重定位地方,那么會(huì)有一個(gè)相對于叫做.rel.text的段保存了代碼段的重定位表
使用objdump查看:
objdump -r a.o
a.o: 文件格式 elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000025 R_X86_64_PC32 shared-0x0000000000000004
0000000000000032 R_X86_64_PLT32 swap-0x0000000000000004
000000000000004b R_X86_64_PLT32 __stack_chk_fail-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
每個(gè)要被重定位的地方叫一個(gè)重定位入口仙粱,重定位入口的偏移OFFSET 表示該入口在要被重定位的段中的位置房交,“RELOCATION RECORDS FOR [.text]:”表示這個(gè)重定位表是代碼段的重定位表
符號解析
之所以鏈接時(shí)因?yàn)槟繕?biāo)文件中用到的符號被定義在其他目標(biāo)文件,所以要將它們鏈接起來伐割,比如直接用ld來鏈接a.o候味,而不將b.o作為輸入,就會(huì)報(bào)錯(cuò)
ld a.o
ld: 警告: 無法找到項(xiàng)目符號 _start; 缺省為 00000000004000b0
a.o:在函數(shù)‘main’中:
a.c:(.text+0x25):對‘shared’未定義的引用
a.c:(.text+0x32):對‘swap’未定義的引用
a.c:(.text+0x4b):對‘__stack_chk_fail’未定義的引用
重定位的過程也伴隨著符號的解析過程隔心,每個(gè)目標(biāo)文件中都可能定義一些符號负溪,也可能引用到定義在其它目標(biāo)文件的符號,重定位的過程济炎,每個(gè)重定位的入口都是對一個(gè)符號的引用川抡,那么當(dāng)鏈接器要對某個(gè)符號的引用進(jìn)行重定位時(shí),它就要確定這個(gè)符號的目標(biāo)地址须尚,這是鏈接器就去查找所有輸入目標(biāo)文件的符號表組成的全局符號,找到后進(jìn)行重定位
比如查看a.o的符號表:
readelf -s a.o
Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.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 81 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail
可以發(fā)現(xiàn)"shared"和"swap"都是UND耐床,即“undefined”未定義類型
COMMON塊
弱符號機(jī)制允許同一個(gè)符號的定義存在于多個(gè)文件中。目前的鏈接器本身并不支持符號的類型撩轰,即變量類型對于鏈接器來說是透明的昧廷,它只知道一個(gè)符號的名字偎箫,并不知道類型是否一致。
多個(gè)符號(同名)定義類型不一致的幾種情況
- 兩個(gè)或兩個(gè)以上強(qiáng)符號類型不一致
有一個(gè)強(qiáng)符號淹办,其他都是弱符號眉枕,出現(xiàn)類型不一致
兩個(gè)或兩個(gè)以上弱符號類型不一致
針對第一種,無需額外處理怜森,因?yàn)槎鄠€(gè)強(qiáng)符號定義本身就是非法的
現(xiàn)在的編譯器和鏈接器都支持一種叫COMMON塊的機(jī)制速挑,來源于Fortran,早起Fortran沒有動(dòng)態(tài)分配空間的機(jī)制副硅,程序員必須事先聲明它所需的臨時(shí)使用空間的大小
COMMON類型的鏈接規(guī)則:原則上講最終鏈接后的輸出文件中姥宝,弱符號的大小以輸入文件中最大(所占空間)的那個(gè)為準(zhǔn)
如果有 一個(gè)符號為強(qiáng)符號,那么最終輸出結(jié)果所占空間大小與強(qiáng)符號相同恐疲±奥【如果鏈接過程中,有弱符號大于強(qiáng)符號流纹,編譯器會(huì)報(bào)警告】
未初始化的全局變量最終還是被放在BSS段的
一旦有一個(gè)未初始化的全局變量不是以COMMON塊的形式存在糜烹,那么它就相當(dāng)于一個(gè)強(qiáng)符號。
靜態(tài)庫鏈接
程序之所以有用漱凝,因?yàn)樗鼤?huì)有輸入輸出疮蹦,這些輸入輸出對象可以是數(shù)據(jù),也可以是人茸炒,也可以是另外一個(gè)程序愕乎,還可以是另外一臺(tái)計(jì)算機(jī)。但是一個(gè)程序如何做到輸入輸出呢壁公?最簡單的辦法是使用操作系統(tǒng)提供的應(yīng)用程序編程接口(API)感论。當(dāng)然,操作系統(tǒng)也是一個(gè)程序紊册。
程序如何使用操作系統(tǒng)提供的 API ? 一般情況下比肄,一種語言的開發(fā)環(huán)境往往會(huì)附帶語言庫,這些庫就是對操作系統(tǒng) API 的包裝囊陡。庫里面還有一些很常用的函數(shù)芳绩,這部分函數(shù)不調(diào)用操作系統(tǒng) API。
靜態(tài)庫可以簡單的看成一組目標(biāo)文件的集合撞反,即很多目標(biāo)文件打包后形成的一個(gè)文件
Linux中最常用的C語言靜態(tài)庫libc位于/usr/lib/libc.a妥色,屬于glibc項(xiàng)目的一部分
Windows上,最常用的C語言庫是IDE附帶的運(yùn)行庫
鏈接過程控制
絕大部分情況下遏片,我們使用鏈接器提供的默認(rèn)規(guī)則對目標(biāo)文件進(jìn)行鏈接撮竿。一般情況下沒有問題幢踏,但對于一些特殊程序惑折,比如操作系統(tǒng)內(nèi)核,BIOS或在一些沒有操作系統(tǒng)的情況下運(yùn)行的程序(如引導(dǎo)程序 Boot Loader或者嵌入式系統(tǒng)的程序)敛助,以及另外的一些需要特殊的鏈接過程的程序纳击,如一些內(nèi)核驅(qū)動(dòng)程序焕数,它們往往受限于一些特殊的條件堡赔,如需要指定輸出文件的各個(gè)段虛擬地址善已,段的名稱离例,段的存放順序等宫蛆,因?yàn)檫@些特殊的環(huán)境耀盗,特別是某些硬件的限制,往往對程序的各個(gè)段的地址有特殊的要求磷醋。
由于整個(gè)鏈接過程有很多內(nèi)容需要確定:
使用哪些目標(biāo)文件
使用哪些庫文件
是否在最終可執(zhí)行文件中保存調(diào)試信息
輸出文件格式(可執(zhí)行文件還是動(dòng)態(tài)鏈接庫)
還要考慮是否要導(dǎo)出某些符號以供調(diào)試器或程序本身或其他程序使用等
鏈接控制腳本
鏈接器一般都提供多種控制整個(gè)鏈接過程的方法邓线,以用來產(chǎn)生用戶所需要的文件。一般鏈接器有如下3種方法:
- 1.使用命令行來給鏈接器指定參數(shù)震庭,如 ld 使用的 -o, -e
- 2.將鏈接器指令存放在目標(biāo)文件里面器联,編譯器進(jìn)程會(huì)通過這種方法向鏈接器傳遞指令拨拓。
- 3.使用鏈接控制腳本渣磷,也是最為靈活醋界,最為強(qiáng)大的鏈接控制方法
最小的程序
這個(gè)最小的程序形纺,功能就是在終端上輸出:"Hello world!",但這個(gè)經(jīng)典的hello wrold程序有點(diǎn)不同:
之前的程序使用了printf函數(shù)逐样,該函數(shù)是C語言庫的一部分官研,這次希望這個(gè)小程序能夠擺脫C語言運(yùn)行庫闯睹,使得它成為一個(gè)獨(dú)立于任何庫的純正的程序
經(jīng)典的版本程序使用了庫始花,所以必須由main函數(shù)酷宵,所以此次不適用main這個(gè)函數(shù)名了用nomain作為整個(gè)程序的入口
- 經(jīng)典的程序會(huì)產(chǎn)生許多段躬窜,這次將所有段都合并到一個(gè)叫做tinytext的段荣挨,這個(gè)段的名字是任意起的
TinyHelloWorld.c
char* str = "Hello World!\n";
void print(){
asm( "movl $14, %%edx \n\t"
"movl %0, %%ecx \n\t"
"movl $0, %%ebx \n\t"
"movl $4, %%eax \n\t"
"int $0x80 \n\t"
::"r"(str):"edx","ecx","ebx");
}
void exit() {
asm( "movl $42,%ebx \n\t"
"movl $1,%eax \n\t"
"int $0x80 \n\t");
}
void nomain() {
print();
exit();
}
程序入口函數(shù)為nomain函數(shù),調(diào)用了print函數(shù)甚纲,打印“hello world”介杆,接著調(diào)用exit函數(shù)春哨,結(jié)束進(jìn)程悲靴,上面使用了GCC內(nèi)嵌匯編莫其,
現(xiàn)在先使用普通命令行的方式來編譯和鏈接TinyHelloWorld.c:
(1)gcc -c -fno-builtin -m32 TinyHelloWorld.c
(2)ld -static -m elf_i386 -e nomain -o TinyHelloWorld TinyHelloWorld.o
第一步用GCC將TinyHelloWorld.c編譯成TinyHelloWorld.o,接著用ld將TinyHelloWorld.o鏈接成可執(zhí)行文件TinyHelloWorld
其中一些參數(shù)的含義:
- -fno-builtin,GCC提供了很多內(nèi)置函數(shù)(Built-in Function),它會(huì)把一些常用的C庫函數(shù)替換成編譯器的內(nèi)置函數(shù)乱陡,以達(dá)到優(yōu)化的功能
-static憨颠,表示ld將使用靜態(tài)鏈接的方式來鏈接程序爽彤,而不是使用默認(rèn)的動(dòng)態(tài)鏈接
-e nomain适篙,表示該程序的入口函數(shù)為nomain
-o TinyHelloWorld嚷节,指定輸出可執(zhí)行文件名為TinyHelloWorld
ld鏈接腳本
如果把整個(gè)鏈接過程比作一臺(tái)計(jì)算機(jī)硫痰,那么ld鏈接器就是計(jì)算機(jī)的CPU效斑,所有的目標(biāo)文件缓屠,庫文件就是輸入,鏈接結(jié)果輸出的可執(zhí)行文件就是輸出敬矩,而鏈接控制腳本正是這臺(tái)計(jì)算機(jī)的“程序”,
不論是輸出文件還是輸入文件业踏,它們的主要的數(shù)據(jù)就是文件中的各個(gè)段勤家,我們把輸入文件中的段热幔,叫輸入段绎巨,輸出文件中的段稱為輸出段场勤。簡單的說歼跟,控制鏈接過程無非就是控制輸入段如何變成輸出段留瞳,比如哪些輸入段要合并成一個(gè)輸出段叹卷,哪些輸入段要丟棄帝牡;指定輸出段的名字蒙揣,裝載地址罩息,屬性等葱色。
TinyHelloWorld的鏈接腳本TinyHelloWorld.lds:
ENTRY(nomain)
SECTIONS
{
. = 0x0804800 + SIZEOF_HEADERS;
tinytext : { *(.text) *(.data) *(.rodata) }
/DISCARD/ : { *(.comment) }
}
ENTRY :指定了程序的入口為 nomain() 函數(shù)
SECTIONS : 鏈接腳本的主體苍狰,這個(gè)命令制定了各個(gè)輸入端到輸出端的變換淋昭,SECTIONS 后面緊跟著一對大括號里面包含了 SECTIONS 變換規(guī)則翔忽,其中有3條語句,每條語句一行贬丛。第一行是賦值語句,后面2條是段轉(zhuǎn)換規(guī)則够庙,它們的含義如下:
- . = 0x0804800 + SIZEOF_HEADERS; 第一條賦值語句的意思是將當(dāng)期虛擬地址設(shè)置為 0x0804800 + SIZEOF_HEADERS, SIZEOF_HEADERS 為輸出文件的文件大小耘眨。
"." 表示當(dāng)前的虛擬地址,因?yàn)檫@條語句后面緊跟著輸出端 "tinytext", 所以 "tinytext" 段的起始虛擬地址為 0x0804800 + SIZEOF_HEADERS偶宫。它將當(dāng)期虛擬地址
設(shè)置成一個(gè)比較巧妙的值,以便于裝載時(shí)頁面映射更為方便。
- . = 0x0804800 + SIZEOF_HEADERS; 第一條賦值語句的意思是將當(dāng)期虛擬地址設(shè)置為 0x0804800 + SIZEOF_HEADERS, SIZEOF_HEADERS 為輸出文件的文件大小耘眨。
- tinytext : { *(.text) *(.data) *(.rodata) } 第二條是段轉(zhuǎn)換規(guī)則吵冒,它的意思即所有的輸入文件中的名字為 ".text", ".data" 或者 ".rodata" 的段依次合并
到輸出文件的 "tinytext"
- tinytext : { *(.text) *(.data) *(.rodata) } 第二條是段轉(zhuǎn)換規(guī)則吵冒,它的意思即所有的輸入文件中的名字為 ".text", ".data" 或者 ".rodata" 的段依次合并
- /DISCARD/ : { *(.comment) } 第三天規(guī)則為:將所有輸入文件中的名字為 ".comment" 的段丟棄纯命,不保存到輸出文件中。
通過命令行來編譯TinyHelloWorld痹栖,并啟用該鏈接控制腳本:
ld -static -m elf_i386 -T TinyHelloWorld.lds -e nomain -o TinyHelloWorld TinyHelloWorld.o
使用objdum查看TinyHelloWorld的段亿汞,可以發(fā)現(xiàn)整個(gè)程序只有一個(gè)段“tinytext”
參考資料
<<程序員的自我修養(yǎng)—鏈接、裝載與庫>>