上一篇介紹了目標(biāo)文件的格式,有了對(duì)結(jié)構(gòu)的認(rèn)識(shí),這篇講靜態(tài)鏈接,主要是關(guān)于目標(biāo)文件如何鏈接起來組成可執(zhí)行文件。筆記后面把ld鏈接腳本語法省略,暫時(shí)用不到這么牛逼的武器。
本文導(dǎo)圖

實(shí)驗(yàn)代碼
實(shí)驗(yàn)代碼
- 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;
}
對(duì)上面內(nèi)容的解釋:
- b中定義兩個(gè)全局符號(hào),變量shared和函數(shù)swap
- a中定義了一個(gè)全局符號(hào)main
- a中引用到了b中的swap和shared
空間地址分配
鏈接額過程就是將輸入的目標(biāo)文件合并為一個(gè)輸出的可執(zhí)行文件。如何將目標(biāo)文件的各個(gè)段合并到可執(zhí)行文件中,也就是空間如何分配,總體有如下兩種方式,按序疊加與相似段合并。——合并規(guī)則
按序疊加很簡單,就是按照目標(biāo)文件的順序疊加起來,可以用下圖說明:

- 如果可執(zhí)行文件成千上百個(gè)目標(biāo)文件組成,就會(huì)出現(xiàn)很多零散的段,每個(gè)目標(biāo)文件都有三個(gè)最為核心的段,這樣就會(huì)非常浪費(fèi)空間,因?yàn)槊總€(gè)段都需要有一定的地址和空間對(duì)其要求。比如x86來說,段的裝載地址和空間對(duì)其單位是頁,也就是4096字節(jié)(一個(gè)頁大小是4096字節(jié)),會(huì)造成極大的內(nèi)存空間碎片。
- 關(guān)于內(nèi)存地址對(duì)其可以看看內(nèi)存對(duì)齊規(guī)則之我見,簡單來講就是因?yàn)?strong>CPU是按字讀取內(nèi)存。所以內(nèi)存對(duì)齊的話,不會(huì)出現(xiàn)某個(gè)類型的數(shù)據(jù)讀一半的情況,需要再二次讀取內(nèi)存??梢蕴嵘L問效率。編譯器想通過空間換時(shí)間,通過適當(dāng)增加padding,使每個(gè)成員的訪問都在一個(gè)指令里完成,而不需要兩次訪問再拼接。
相似段合并這種方式更加實(shí)際,比如講.text段合并到可執(zhí)行文件的.text段,各個(gè)段一次合并。如下圖所示:

鏈接器為目標(biāo)文件分配地址空間有兩層含義:
- 一個(gè)是在輸出可執(zhí)行文件中的空間
- 另一個(gè)是裝載后的虛擬地址中的虛擬地址空間
- 之前提到過.bbs段,在可執(zhí)行文件中并不占用文件空間,但是在占用虛擬地址空間,因?yàn)?bbs段在在文件中并沒有內(nèi)容。
目前只討論關(guān)于虛擬地址空間的分配
具體來講鏈接器空間分配策略都是用第二中,并且采用兩步鏈接。
- 第一步:空間與地址分配——掃描所有目標(biāo)文件,得到各個(gè)段的長度,將所有目標(biāo)文件的符號(hào)表中的符號(hào)定義及引用信息統(tǒng)一放到一個(gè)全局符號(hào)表??梢愿鶕?jù)目標(biāo)文件的段長度,將他們合并,建立映射關(guān)系。
- 第二步:符號(hào)解析與重定位——根據(jù)上面的信息,讀取文件中的段數(shù)據(jù),重定位信息,進(jìn)行符號(hào)解析與重定位,調(diào)整代碼地址。這一步才是狠心,尤其是重定位。
用ld將a.o、b.o連接起來
Linux
ld a.o b.o -e main -o ab
Mac
ld a.o b.o -e _main -o ab
ld命令的兩個(gè)參數(shù)含義是:
-o:指定輸出文件名;
-e:指定程序的入口符號(hào)。
鏈接之后各個(gè)段的屬性
Linux

Mac
$ objdump -h a.o
a.o: file format Mach-O 64-bit x86-64
Sections:
Idx Name Size Address Type
0 __text 0000002e 0000000000000000 TEXT
1 __compact_unwind 00000020 0000000000000030 DATA
2 __eh_frame 00000040 0000000000000050 DATA
$ objdump -h b.o
b.o: file format Mach-O 64-bit x86-64
Sections:
Idx Name Size Address Type
0 __text 0000002c 0000000000000000 TEXT
1 __data 00000004 000000000000002c DATA
2 __compact_unwind 00000020 0000000000000030 DATA
3 __eh_frame 00000040 0000000000000050 DATA
$ objdump -h ab
ab: file format Mach-O 64-bit x86-64
Sections:
Idx Name Size Address Type
0 __text 0000005c 0000000000001f20 TEXT
1 __eh_frame 00000080 0000000000001f80 DATA
2 __data 00000004 0000000000002000 DATA
在Linux中VMA表示的是虛擬地址,LMA表示的是加載地址,一般這兩個(gè)值一樣,但是有些嵌入式系統(tǒng)中會(huì)不一樣。Mac中只有一個(gè)地址也就是虛擬地址。
現(xiàn)在直接看VMA和SIZE,暫時(shí)忽略文件偏移。在鏈接之前虛擬地址都是零(MAC上起始的.text段為0),因?yàn)樘摂M地址空間還沒有分配,所以默認(rèn)都是0,但是鏈接之后,可執(zhí)行文件ab各個(gè)段都分配了相應(yīng)的虛擬地址,所以可以看到text已經(jīng)分配到地址。
對(duì)應(yīng)到Linux中ELF文件,.text段分配到了0x08048094,大小是0x72字節(jié),.data段從地址0x08049108開始,大小為四字節(jié)。總體來說如下圖:

為什么不從虛擬地址的0地址開始分配呢。涉及到操作系統(tǒng)進(jìn)程虛擬地址的分配規(guī)則。Linux下ELF文件默認(rèn)從0x08048000開始分配的。
符號(hào)地址的確定
第一步過程中確定了在可執(zhí)行文件中的空間分布。比如.text其實(shí)段0x08040894,.data段其實(shí)地址0x08049108.
第一步完成之后,鏈接器就開始計(jì)算各個(gè)符號(hào)的虛擬地址。符號(hào)在段內(nèi)的位置是固定的,比如main、shared、wap地址已經(jīng)是確定的了,只不過需要鏈接器給每個(gè)符號(hào)添加一個(gè)偏移量。
比如a.o中的main函數(shù)相對(duì)于a.o的text偏移量是X,經(jīng)過鏈接之后a.o的text段位于虛擬地址0x08048094,那么main的地址就是0x08048094+X。從前面的objdump可以看到main位于a.o的text段偏移是0。所以main這個(gè)符號(hào)最終在可執(zhí)行文件中的地址是0x08048094+0。

符號(hào)解析、重定位
完成了空間和地址分配,鏈接器開始進(jìn)行符號(hào)解析及重定位。
使用objdump的參數(shù)d查看反匯編結(jié)果
未鏈接a.o反匯編結(jié)果
Mac 下
$ objdump -d a.o
a.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
_main:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 48 83 ec 10 subq $16, %rsp
8: 48 8d 7d fc leaq -4(%rbp), %rdi
c: 48 8b 35 00 00 00 00 movq (%rip), %rsi
13: c7 45 fc 64 00 00 00 movl $100, -4(%rbp)
1a: b0 00 movb $0, %al
1c: e8 00 00 00 00 callq 0 <_main+0x21>
21: 31 c9 xorl %ecx, %ecx
23: 89 45 f8 movl %eax, -8(%rbp)
26: 89 c8 movl %ecx, %eax
28: 48 83 c4 10 addq $16, %rsp
2c: 5d popq %rbp
2d: c3 retq
Linux下:

可執(zhí)行文件ab反匯編結(jié)果:
Mac 下:
objdump -d ab
ab: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
......
_main:
1f20: 55 pushq %rbp
1f21: 48 89 e5 movq %rsp, %rbp
1f24: 48 83 ec 10 subq $16, %rsp
1f28: 48 8d 7d fc leaq -4(%rbp), %rdi
1f2c: 48 8d 35 cd 00 00 00 leaq 205(%rip), %rsi
1f33: c7 45 fc 64 00 00 00 movl $100, -4(%rbp)
1f3a: b0 00 movb $0, %al
1f3c: e8 0f 00 00 00 callq 15 <_swap>
1f41: 31 c9 xorl %ecx, %ecx
1f43: 89 45 f8 movl %eax, -8(%rbp)
1f46: 89 c8 movl %ecx, %eax
1f48: 48 83 c4 10 addq $16, %rsp
1f4c: 5d popq %rbp
1f4d: c3 retq
1f4e: 90 nop
1f4f: 90 nop
......
Linux下

需要懂點(diǎn)匯編才能理解上面的不同。主要是想說明,被引用的函數(shù)或者變量的地址,在鏈接之后被重新定位了。如上面的swap函數(shù)、shared變量。
關(guān)于匯編的學(xué)習(xí)后面會(huì)專門寫一篇!
重定位表
鏈接器通過重定位表才能知道哪些指令需要被調(diào)整,重定位表往往是一個(gè)或多個(gè)段。ELF必須包含重定位表來重新定位符號(hào)。
比如代碼段.text有符號(hào)需要重定位,則就會(huì)有一個(gè).rel.text的段保存了代碼段重定位的信息,如果.data段中有重定位的地方,就會(huì)有一個(gè)對(duì)應(yīng)的.rel.data段保存了數(shù)據(jù)端的重定位表??梢允褂胦bjdump的r參數(shù)查看。
Mac下:
objdump -r a.o
a.o: file format Mach-O 64-bit x86-64
RELOCATION RECORDS FOR [__text]:
000000000000001d X86_64_RELOC_BRANCH _swap
000000000000000f X86_64_RELOC_GOT_LOAD _shared@GOTPCREL
RELOCATION RECORDS FOR [__compact_unwind]:
0000000000000000 X86_64_RELOC_UNSIGNED __text
Linux下:

可以看到a.o有兩個(gè)重定位入口,重定位入口的偏移表示該符號(hào)入口在重定位段中的位置。
對(duì)上面代碼的解析:因?yàn)檫€未進(jìn)行鏈接,首先main函數(shù)其實(shí)地址為0x00000000。這個(gè)main函數(shù)占用了0x33個(gè)字節(jié),17條指令。shared的引用是一條mov指令,一共8個(gè)字節(jié),將shared的地址復(fù)制到ESP寄存器+4的偏移地址中,前四個(gè)是指令碼,后面是shared的地址。暫時(shí)認(rèn)為shared的地址是0x00000000,并且是絕對(duì)地址指令。。
其次對(duì)swap的調(diào)用的指令一個(gè)共5個(gè)字節(jié),0xE8是操作碼,這是個(gè)相對(duì)位移調(diào)用指令,后面四個(gè)字節(jié)就是函數(shù) 相對(duì)于調(diào)用指令的下一條指令的偏移量。沒重定位之前,相對(duì)偏移量為0xFFFFFFFC(小端),常量
-4的補(bǔ)碼形式。
- 第一行表示這個(gè)重定位表是對(duì)代碼段的重定位,所以偏移表示代碼段中需要被調(diào)整的位置。對(duì)照前面的反匯編結(jié)構(gòu)。這里的0x1c和0x27分別對(duì)應(yīng)了代碼段中的mov和call指令部分。
重定位表中保存的是一個(gè)ELF32_Rel(intelx86)結(jié)構(gòu)數(shù)組,表中每一個(gè)元素都對(duì)應(yīng)一個(gè)重定位入口
typedef struct {
ELF32_Addr r_offset;
ELF32_Word r_info;
}
具體含義如下:

重定位表項(xiàng)里面包含了如何定位該符號(hào)的所有信息,偏移位置,類型,符號(hào)等。
符號(hào)解析
符號(hào)未定義錯(cuò)誤在鏈接的時(shí)候經(jīng)常出現(xiàn)。對(duì)應(yīng)上面的例子,
ld a.o
ld: warning: -macosx_version_min not specified, assuming 10.11
ld: warning: object file (a.o) was built for newer OSX version (10.12) than being linked (10.11)
Undefined symbols for architecture x86_64:
"_shared", referenced from:
_main in a.o
"_swap", referenced from:
_main in a.o
"start", referenced from:
implicit entry/start for main executable
ld: symbol(s) not found for inferred architecture x86_64
導(dǎo)致符號(hào)未定義的原因很多(根本原因找不到符號(hào)),比如:
- 鏈接時(shí)少了某個(gè)庫
- 符號(hào)目標(biāo)文本路徑不正確
為什么缺少符號(hào)定義會(huì)導(dǎo)致鏈接錯(cuò)誤?其實(shí)重定位過程伴隨著符號(hào)解析的過程,每個(gè)目標(biāo)文件可能定義一些符號(hào),也可能引用的到其他文件中定義的符號(hào),每個(gè)重定位入口都是對(duì)一個(gè)符號(hào)的引用。當(dāng)鏈接器對(duì)某個(gè)符號(hào)進(jìn)行重定位的時(shí)候,就需要確定這個(gè)符號(hào)的目標(biāo)地址,這個(gè)時(shí)候就會(huì)去查找輸入目標(biāo)文件的符號(hào)表組成的全局符號(hào)表,找到這個(gè)符號(hào)然后重定位。
使用readelf -s查看符號(hào)表。
a.o中的符號(hào)表

注意上面的main、shared、swap都是GLOBAL。main函數(shù)定義在代碼段之外,shared和swap是UND為定義。因?yàn)樗麄兪嵌x哎其他目標(biāo)文件中的。穩(wěn)定一的都是需要在全局符號(hào)表中找到。
指令修正
這部分需要結(jié)合在重定位反匯編用到的偏移量。
不同處理器對(duì)于地址的格式和方式都不一樣。常見的有跳轉(zhuǎn)指令(jump 11種)、子程序調(diào)用指令(call 10種)、數(shù)據(jù)傳送指令(mov 34中)尋址千差萬別。差別如下:
- 近址或遠(yuǎn)址尋址
- 絕對(duì)與相對(duì)尋址
- 尋址長度8、16、32、64位
- 相對(duì)近址32位尋址
- 絕對(duì)近址32位尋址
每個(gè)被修正的長度為32位,4字節(jié),都是近址尋址。區(qū)別就是相對(duì)或者絕對(duì)。之前說過,重定位入口r_info成員低八位表示重定位入口類型

對(duì)應(yīng)到上面的內(nèi)容。swap符號(hào)的引用類型為R_386_PC32,代表是相對(duì)位移調(diào)用指令,而shared是R_386_32類型,他修正的是一條傳輸指令的原,shared的絕對(duì)地址。
絕對(duì)尋址修正和相對(duì)尋址修正的區(qū)別就是前者修正后的地址就是該符號(hào)的實(shí)際地址,而后者修正后的地址為符號(hào)距離被修正位置的地址差。
現(xiàn)在來計(jì)算一下,假設(shè)鏈接之后main函數(shù)的虛擬地址為0x1000,swap函數(shù)的地址為0x2000,shared變量的虛擬地址為0x3000。現(xiàn)在開始修正這兩個(gè)重定位符號(hào)的地址。
shared
根據(jù)上面的分析,首先shared是一個(gè)絕對(duì)尋址修正,結(jié)果應(yīng)該是S(實(shí)際虛擬地址——假設(shè)的)+ A(修正位置的值——從符號(hào)解析中得到的value)
那么修正之后的地址就是:0x3000 + 0x0000000 = 0x3000。那么就應(yīng)該是:

swap
swap需要相對(duì)修正,器修正地址就是S + A - P(被修正的位置,當(dāng)鏈接的時(shí)候這個(gè)值就是被修正位置的虛擬地址 就為0x1000(main函數(shù)地址)+ 0x27)
對(duì)應(yīng)下來:0x2000 + (-4) - (0x1000 + 0x27) = 0xFD5:
那么這條相對(duì)唯一調(diào)用指令地址是改指令下一條指令其實(shí)地址加上偏移量。那就是0x1026 + 0xfd5 = 0x2000。這就是swap函數(shù)的地址。

COMMON塊
編譯器將未初始化的全局變量定義作為弱符號(hào),如上一篇的例子中g(shù)lobal_uninit_var。使用readelf -s查看

類型是一個(gè)SH_COMMON類型。
當(dāng)不同目標(biāo)文件需要的COMMON塊空間大小不一致的時(shí)候,以最大的那塊為準(zhǔn)。
需要使用COMMON機(jī)制的原因是編譯器和鏈接器允許不同類型的弱符號(hào)存在,但是最本質(zhì)的還是鏈接器不支持符號(hào)類型,也就是鏈接器無法判斷各個(gè)符號(hào)類型是否一致。
小結(jié):如果編譯單元包含了弱符號(hào)(比如未初始化的全局變量就是典型的),那么弱符號(hào)最終占有多大空間不知道,所以編譯器無法為改符號(hào)在BSS段分配空間。但是在鏈接過程中,弱符號(hào)大小可以確定了,所以最終在輸出可執(zhí)行文件的BBS段為弱符號(hào)分配空間。最終未初始化的全局變量還是放在BBS段
C++ 相關(guān)問題
C++語言特性太復(fù)雜,必須有編譯器和鏈接器共同支持才能完成工作。關(guān)鍵在于:
- 重復(fù)代碼消除
- 全局構(gòu)造與析構(gòu)
- 特性:虛函數(shù)、函數(shù)重載、繼承、異常等。這些數(shù)據(jù)結(jié)構(gòu)復(fù)雜,往往在不同編譯器和鏈接器之間不能通用,二進(jìn)制兼容性很麻煩。
重復(fù)代碼消除
C++中的模板本質(zhì)來講很像宏,當(dāng)被實(shí)例化的時(shí)候,并不知道自己是否在別的編譯單元被實(shí)例化,所以必定出現(xiàn)重復(fù)代碼。如果不管這些重復(fù)代碼的話,主要會(huì)有如下問題:
- 空間浪費(fèi)
- 地址教易出錯(cuò):可能有兩個(gè)指向同一個(gè)函數(shù)的指針不相等。
- 指令運(yùn)行效率低:因?yàn)楝F(xiàn)代CPU都會(huì)對(duì)指令和數(shù)據(jù)進(jìn)行緩存,同一份指令有多份副本,那么Cache的命中率就會(huì)很低。
比較現(xiàn)實(shí)的做法:每個(gè)目標(biāo)的實(shí)例代碼都單獨(dú)存放到一個(gè)段(段命名如.gnu.linkonce.name,name就是該函數(shù)模板修飾后的名稱)里,每個(gè)段只包含一個(gè)模板實(shí)例。當(dāng)別的編譯單元也有同樣模板實(shí)例的時(shí)候,就會(huì)生成相同的名字,最終鏈接器將他們合并到最后的代碼段。
函數(shù)級(jí)別鏈接
現(xiàn)在的程序和庫都非常龐大,一個(gè)目標(biāo)文件可能包含成千上百個(gè)函數(shù)或者變量,當(dāng)我們需要用到某個(gè)目標(biāo)文件的一個(gè)函數(shù)或變量的時(shí)候,需要把珍格格文件鏈接進(jìn)來,這樣導(dǎo)致輸出的文件也很多。
在C++編譯器中,有個(gè)編譯選項(xiàng)叫做函數(shù)幾倍鏈接,這個(gè)的作用就是讓所有的函數(shù)像前面的模板一樣,單獨(dú)保存到一個(gè)段里面。當(dāng)鏈接器需要用到這個(gè)函數(shù)的時(shí)候,就會(huì)將它合并到輸出文件,沒有用到的函數(shù)就會(huì)被拋棄。這樣的方式同樣有問題,雖然減少了輸出文件的長度,但是會(huì)減慢編譯和鏈接的過程,并且所有函數(shù)保存到獨(dú)立的短中,目標(biāo)函數(shù)的短數(shù)量增加,重定位會(huì)因?yàn)槎痰臄?shù)目增加而變得復(fù)雜,目標(biāo)文件也會(huì)變得相對(duì)較大。
全局構(gòu)造、析構(gòu)
C/C++程序都是從main函數(shù)開始執(zhí)行的,隨著main函數(shù)結(jié)束而結(jié)束。在main函數(shù)之前為了程序能夠順利執(zhí)行。需要初始化進(jìn)程執(zhí)行環(huán)境、如堆分配初始化、線程子系統(tǒng)等。C++的全局對(duì)象構(gòu)造函數(shù)在這一時(shí)期被執(zhí)行
Linux下一班程序的入口是_start,這個(gè)函數(shù)時(shí)Linux系統(tǒng)庫(Glibc)的一部分。當(dāng)程序與Glibc鏈接在一起形成可執(zhí)行文件之后,_start就是程序初始化入口。程序初始化之后,會(huì)調(diào)用main函數(shù)來執(zhí)行程序,main函數(shù)執(zhí)行之后就會(huì)進(jìn)行一些清理工作,然后結(jié)束進(jìn)程。
在ELF定義了兩個(gè)特殊的段,
-
.init段在main函數(shù)之前的可執(zhí)行指令。構(gòu)成進(jìn)程的初始化代碼。所以在main函數(shù)調(diào)用之前,Glibc初始化部分會(huì)執(zhí)行這個(gè)段的代碼。 -
.fini段保存著進(jìn)程的終止代碼指令。所以當(dāng)main函數(shù)正常退出時(shí),Glib會(huì)安排執(zhí)行這個(gè)段中的代碼。
C++、ABI
編譯器有很多種,那么不同編譯器產(chǎn)生的目標(biāo)文件可不可以進(jìn)鏈接呢?
如果兩個(gè)不同的編譯器想要產(chǎn)生的目標(biāo)文件能夠正確的鏈接起來,那么這兩個(gè)目標(biāo)文件必須滿足:
- 采用相同的目標(biāo)文件格式
- 擁有相同的符號(hào)修飾標(biāo)準(zhǔn)
- 變量的內(nèi)存分配方式相同
- 函數(shù)調(diào)用方式相同
把符號(hào)修飾標(biāo)準(zhǔn)、變量內(nèi)存布局、函數(shù)調(diào)用方式等這些跟可執(zhí)行的二進(jìn)制文件兼容性相關(guān)的內(nèi)容統(tǒng)稱為ABI(應(yīng)用程序二進(jìn)制接口)。如果想弄清API和ABI的區(qū)別,可以看最后的擴(kuò)展閱讀內(nèi)容呢。
- 簡單來講就是API往往指源代碼級(jí)別的接口,如POSIX是一個(gè)API標(biāo)準(zhǔn),而ABI是二進(jìn)制層面的級(jí)別。比如C++對(duì)象內(nèi)存布局是C++ABI的一部分。
- 就拿POSIX規(guī)定printf()函數(shù)的原型為例,POSIX保證這個(gè)函數(shù)所有遵循POSIX標(biāo)準(zhǔn)的系統(tǒng)之間都一樣,但是不保證printf在每個(gè)系統(tǒng)執(zhí)行時(shí),是否按照從右到左的參數(shù)壓入堆棧。參數(shù)如何在堆棧中分配等這些實(shí)際運(yùn)行時(shí)的二進(jìn)制級(jí)別問題。
- 由于各大硬件拼圖,編程語言,編譯,鏈接器和操作系統(tǒng)之間的ABI互相不兼容,所以哥哥目標(biāo)文件之間無法互相鏈接。
實(shí)際例子
對(duì)于C語言的目標(biāo)來講,一下幾個(gè)方面會(huì)決定二進(jìn)制是否兼容:

API相同ABI不一定相同。
鏈接過程控制
一般情況下用鏈接器默認(rèn)的鏈接規(guī)則就可以了,但是在一些特殊的情況下就需要自定義一些參數(shù)了,比如引導(dǎo)程序、內(nèi)核驅(qū)動(dòng)程序就需要特殊的鏈接過程。
鏈接過程需要確定的內(nèi)容:
- 使用哪些目標(biāo)文件
- 使用安歇庫文件
- 是否在最終的可執(zhí)行文件保留調(diào)試信息
- 輸出文件格式(可執(zhí)行文件、動(dòng)態(tài)庫、靜態(tài)庫)
- ....
鏈接控制腳本
鏈接控制腳本就是用來控制鏈接行為的
一般鏈接器有如下幾種方式控制鏈接行為:
- 使用命令行:給鏈接器指定參數(shù),比如之前用的ld的-o、-e參數(shù)就屬于這類。
- 鏈接指令存放到目標(biāo)文件里面:編譯器經(jīng)常會(huì)通過這種方式給鏈接器傳遞指令。具體來講比如在PE目標(biāo)文件的.drectve段用來鏈接傳遞參數(shù) 。
- 鏈接控制腳本:最為靈活也是最為強(qiáng)大的控制方式
之前我們?cè)谑褂胠d命令鏈接的時(shí)候,沒有指定鏈接腳本,其實(shí)ld如果沒有指定鏈接腳本,則會(huì)使用默認(rèn)的鏈接腳本。在Linux上使用ld -verbose查看默認(rèn)鏈接腳本。為了更加精確的控制鏈接過程,可以自己寫一個(gè)鏈接腳本,然后指定該腳本控制腳本,比如ld -T link.script
一個(gè)例子
在這里例子中,作者沒有使用main函數(shù)和c中的printf函數(shù)來打印helloword。而是使用了自定義的一套方式,使用了GCC內(nèi)嵌匯編(不是很懂,沒弄過),并且自定義了將所有段合并到一個(gè)叫做tinytext段中。
TinyHelloWord.c源碼

看到后面懵逼了,暫時(shí)停下來去了解下匯編、復(fù)習(xí)下終端等知識(shí)。
- 句柄與普通指針的區(qū)別:指針包含的是引用對(duì)象的內(nèi)存地址,而句柄則是由系統(tǒng)所管理的引用標(biāo)識(shí),該標(biāo)識(shí)可以被系統(tǒng)重新定位到一個(gè)內(nèi)存地址上。這種間接訪問對(duì)象的模式增強(qiáng)了系統(tǒng)對(duì)引用對(duì)象的控制。
使用ld鏈接腳本
無奈是輸出文件還是輸入文件,主要的數(shù)據(jù)就是文件中的各個(gè)段。它們中的段我們稱作輸入段、輸出段??刂奇溄庸褪前芽刂戚斎攵稳绾巫兂奢敵龆?,比如
- 哪些輸入段要合并為一個(gè)輸出段
- 哪些輸入段要丟棄
- 指定輸出段的名字、裝載地址、屬性
比如上面TinyHelloword的鏈接腳本TinyHelloWorld.lds

第一行是ENTRY(nomain)指定了程序入口為nomain()函數(shù)
-
SECTIONS是鏈接腳本的主體,指定了各個(gè)輸入段到輸出段的交換。里面的大括號(hào)包含的是SECTIONS的變化規(guī)則。
- 第一條是賦值語句
. = 0x08048000 + SIZEOF_HEADERS表示把當(dāng)前虛擬地址設(shè)置成為0x08048000 + SIZEOF_HEADERS -
tinytext:{*(.text)*(.data)*(.rodata)}第二條是個(gè)段轉(zhuǎn)換規(guī)則,也就是后面的三個(gè)段合并輸出到文件tinytext中 -
/DISCARD/:{*{comment}}第三個(gè)意思就是丟棄所有輸入文件中的名字為.comment內(nèi)容,不保存到輸出文件中。
默認(rèn)情況下,
.shstrtab、.symtab、.strtab代表段名字符串表,符號(hào)表和字符串表,這三種表,鏈接器在產(chǎn)生可執(zhí)行文件的時(shí)候會(huì)自動(dòng)生成。——可執(zhí)行文件中,符號(hào)表和字符串是可選的,段名字符串表保存段名,必不可少。 - 第一條是賦值語句
ld鏈接腳本語法
這一小節(jié)直接看資料,ld鏈接腳本文件語法解析。平時(shí)很難有機(jī)會(huì)用到這塊知識(shí)。
擴(kuò)展閱讀
ABI-Application binary interface
GCC內(nèi)嵌匯編
Linux 內(nèi)核中斷內(nèi)幕
句柄是什么?
Purpose of memory alignment
內(nèi)存地址對(duì)齊提升程序性能