链接器相关的一些基本问题
鏈接器相關(guān)的一些基本問(wèn)題
學(xué)習(xí)或者了解鏈接器,有一些基本的問(wèn)題需要關(guān)心:鏈接器做些什么;鏈接器和體系結(jié)構(gòu);程序是怎樣生成的。下面做簡(jiǎn)要介紹。
鏈接器做些什么
鏈接器之所以存在或者產(chǎn)生,基本上是由于程序開(kāi)發(fā)的模塊化。這里講的模塊,主要是編譯概念上的模塊,通常他們按照功能劃分,比如一個(gè).c或者.cpp文件就是一個(gè)編譯單元,就是一個(gè)模塊,編譯后就產(chǎn)生一個(gè).o目標(biāo)文件。為了最終生成一個(gè)可執(zhí)行文件、靜態(tài)庫(kù)或者動(dòng)態(tài)庫(kù),就需要把各個(gè)編譯單元按照特定的約定組合到一起。這里特定的約定指的就是“目標(biāo)文件格式”,它定義了目標(biāo)文件、庫(kù)文件和可執(zhí)行文件的格式,這里組合這一過(guò)程就叫做鏈接。
一個(gè)編譯模塊中,通常是函數(shù)的定義和全局?jǐn)?shù)據(jù)的定義,數(shù)據(jù)類型的定義通常在頭文件中,編譯時(shí)會(huì)被包含在編譯模塊中。函數(shù)和數(shù)據(jù)由符號(hào)來(lái)標(biāo)識(shí),一般符號(hào)有全局和靜態(tài)之分,全局符號(hào)可以被其他模塊引用,而靜態(tài)符號(hào)只能在本模塊中引用。編譯各個(gè)模塊時(shí),編譯器會(huì)解析該模塊。重要的一項(xiàng)工作就是建立符號(hào)表,符號(hào)表中包含了本模塊有哪些符號(hào)可以被其他模塊引用(導(dǎo)出符號(hào)),還包括本模塊引用(導(dǎo)入符號(hào),即未定義符號(hào))、但在其他模塊中定義的符號(hào)。每一個(gè)符號(hào)都關(guān)聯(lián)一個(gè)地址,這個(gè)地址指明了該符號(hào)在本模塊中的偏移地址(通常是一個(gè)從0開(kāi)始的地址)。
鏈接器在鏈接過(guò)程中,會(huì)掃描各個(gè)模塊的符號(hào)表,得到一個(gè)“全局符號(hào)表”,鏈接器由此決定一個(gè)符號(hào)在哪里被定義,在哪里被引用。并且,將符號(hào)引用處替換為定義處的地址,這一過(guò)程就叫做符號(hào)解析。
鏈接器的一項(xiàng)終極目標(biāo)就是生成可執(zhí)行文件。通常,可執(zhí)行文件和普通目標(biāo)文件的重要區(qū)別就是地址空間的使用。主流操作系統(tǒng)中,可執(zhí)行文件都是基于虛擬地址空間的,即每個(gè)可執(zhí)行文件都有相同且獨(dú)立的地址空間,并且文件中各個(gè)段(代碼段,數(shù)據(jù)段,以及進(jìn)程空間中的堆棧段)都有相似的布局。而普通目標(biāo)文件卻使用從零開(kāi)始的地址空間,這樣一來(lái),模塊M中的符號(hào)m就可能和模塊N中的符號(hào)n擁有“相同”的地址。在鏈接器鏈接各個(gè)模塊時(shí),會(huì)從各個(gè)模塊中“提取”類型相同的段進(jìn)行合并,并將合并后的段寫入可執(zhí)行文件中。這一過(guò)程被稱為存儲(chǔ)空間的分配。值得一提的是,棧、堆以及未初始化的數(shù)據(jù)這些“運(yùn)行時(shí)”需要的空間不會(huì)在可執(zhí)行文件中占據(jù)磁盤空間,但它們占用相應(yīng)的地址空間。
由于存在上述“合并”過(guò)程,前面提到的符號(hào)解析就涉及到另外一個(gè)過(guò)程:重定位。由于各個(gè)模塊中的函數(shù)/數(shù)據(jù)地址會(huì)被重新排放,那么對(duì)這些符號(hào)的引用也必須被相應(yīng)地調(diào)整。這一調(diào)整過(guò)程被稱作重定位。
符號(hào)解析,存儲(chǔ)空間分配,還有重定位,這三個(gè)過(guò)程是一個(gè)有機(jī)的整體,是“同時(shí)”進(jìn)行的,且這三個(gè)過(guò)程也是模塊化所帶來(lái)的必須要解決的問(wèn)題。
鏈接器和體系結(jié)構(gòu)
在我們編寫普通應(yīng)用程序時(shí),不需要過(guò)多的關(guān)系體系結(jié)構(gòu)的問(wèn)題,但對(duì)于鏈接器學(xué)習(xí)者/編寫者來(lái)說(shuō),相當(dāng)大的工作都必須圍繞體系結(jié)構(gòu)展開(kāi),比如目標(biāo)平臺(tái)的ABI,內(nèi)存地址,指令格式,尋址方式等等,下面做大致介紹。
一個(gè)體系結(jié)構(gòu)的ABI,即二進(jìn)制程序接口,主要包括硬件和操作系統(tǒng)兩個(gè)層面。內(nèi)存字長(zhǎng),對(duì)齊方式,子程序調(diào)用時(shí)的約定(參數(shù)傳遞方式,返回值傳遞方式),系統(tǒng)調(diào)用如何進(jìn)行,目標(biāo)文件格式等等,都屬于ABI的范疇,都是鏈接器工作時(shí)要密切關(guān)注的問(wèn)題。
另外,多字節(jié)數(shù)據(jù)的字節(jié)序也是鏈接器需要考慮的。字節(jié)序是關(guān)于如何使用線性的連續(xù)字節(jié)來(lái)表示多字節(jié)數(shù)據(jù)的問(wèn)題,有Little-endian和Big-endian兩種字節(jié)序。小端序是指將多字節(jié)數(shù)據(jù)的低權(quán)重字節(jié)放在內(nèi)存的低地址處,大端序則正好相反。直觀講,從內(nèi)存的低地址向高地址方向看,先看到多字節(jié)數(shù)據(jù)的低權(quán)重字節(jié)的就是小端序,否則就是大端序。至于為什么會(huì)存在兩種字節(jié)序,兩者有何優(yōu)劣,我覺(jué)得這只是個(gè)“個(gè)人喜好”問(wèn)題,就好象剝雞蛋先磕破哪一頭,蹲廁所時(shí)臉里還是臉朝外一樣,事實(shí)上,直到前些天,我才親眼看到,真的是有“茅房拉屎臉朝內(nèi)”的人的。
指令格式和尋址方式也會(huì)影響鏈接器的工作,因?yàn)樵诜?hào)重定位的時(shí)候,鏈接器需要修改指令中操作數(shù)部分,所以需要知道每種指令的指令格式及尋址方式,以便對(duì)指令做出適當(dāng)?shù)恼{(diào)整。
程序是怎樣生成的
事情漸漸明了了,編譯器前端對(duì)語(yǔ)言進(jìn)行詞法、語(yǔ)法分析,建立語(yǔ)法樹,編譯器后端在語(yǔ)法樹的基礎(chǔ)上針對(duì)特定的平臺(tái)生成指令,并按照特定的格式輸出到磁盤中的目標(biāo)文件。鏈接器按照前面所說(shuō)的過(guò)程生成最終的可執(zhí)行文件或程序庫(kù)。(這里的程序庫(kù)特指動(dòng)態(tài)庫(kù),因?yàn)殪o態(tài)庫(kù)只是目標(biāo)文件的簡(jiǎn)單集合,理論上不需要鏈接器的參與)本文只針對(duì)鏈接器進(jìn)行簡(jiǎn)單的討論,關(guān)于編譯器的功能和相關(guān)原理,有大量的資料可以參考。
目標(biāo)文件格式
目標(biāo)文件格式是指令、數(shù)據(jù)在磁盤中存儲(chǔ)形式的一種約定。它描述了指令、數(shù)據(jù)的存儲(chǔ)格式和布局,并且針對(duì)不同類型的文件(普通目標(biāo)文件,可執(zhí)行文件,動(dòng)態(tài)庫(kù))有不同描述側(cè)重點(diǎn)。另外它還描述了一些供外部程序使用的元信息,例如普通目標(biāo)文件中的符號(hào)表的內(nèi)容和組織形式,待重定位符號(hào)的信息;可執(zhí)行文件中各個(gè)段的信息,程序的入口點(diǎn)(程序從何處開(kāi)始執(zhí)行),哪些符號(hào)需要在運(yùn)行時(shí)解析,這些符號(hào)包含在哪些動(dòng)態(tài)庫(kù)中,以及解析這些庫(kù)需要哪種動(dòng)態(tài)鏈接器等等;動(dòng)態(tài)庫(kù)為了實(shí)現(xiàn)同時(shí)被多個(gè)進(jìn)程鏈接需要什么樣的組織形式,本身又引用了其他動(dòng)態(tài)庫(kù)的哪些符號(hào)等等。通常,不同的平臺(tái)都會(huì)有自己的目標(biāo)文件的格式標(biāo)準(zhǔn):
- COM:DOS最初采用的一種非常簡(jiǎn)單的格式,除了指令、數(shù)據(jù)之外,基本不包含其他信息。
- PE:Windows當(dāng)前采用的目標(biāo)格式,繼承自COFF,是一種主流的現(xiàn)代目標(biāo)格式,相比COM有更強(qiáng)大的功能支持。
- a.out:最初UNIX平臺(tái)采用的目標(biāo)格式,簡(jiǎn)單且功能強(qiáng)大,但對(duì)于C++這樣的高級(jí)語(yǔ)言支持不足。
- ELF:當(dāng)前Linux/Unix平臺(tái)采用的主流格式,繼承自a.out,且對(duì)高級(jí)語(yǔ)言支持很友好。
除了ABI,目標(biāo)格式的不同,也是在一種操作系統(tǒng)下編譯的程序無(wú)法在另一種操作系統(tǒng)中執(zhí)行的原因。
程序庫(kù)
終于到庫(kù)了,研究庫(kù)很有趣,也相當(dāng)實(shí)用。概念上,庫(kù)可以分為靜態(tài)庫(kù),動(dòng)態(tài)庫(kù),且這兩種庫(kù)都可以實(shí)現(xiàn)為“共享庫(kù)”,但在實(shí)現(xiàn)上,靜態(tài)共享庫(kù)由于需要考慮態(tài)度的問(wèn)題、實(shí)現(xiàn)太過(guò)復(fù)雜且得不償失,現(xiàn)實(shí)中很少有這種類型的庫(kù)。所以,應(yīng)用中只存在兩種庫(kù):靜態(tài)庫(kù)和動(dòng)態(tài)共享庫(kù),下面分別做簡(jiǎn)要介紹,關(guān)于在Linux中如何創(chuàng)建這兩種庫(kù),可以參考我之前寫的一篇博客,或者其他更詳盡更優(yōu)秀的資料。
靜態(tài)庫(kù)
在功能特性上,靜態(tài)庫(kù)是指這樣一種庫(kù),在鏈接時(shí),其中被引用的代碼、數(shù)據(jù)被“復(fù)制”到引用該庫(kù)的程序中。在格式上,靜態(tài)庫(kù)十分簡(jiǎn)單,他是普通目標(biāo)文件的集合,是一種簡(jiǎn)單的拼接。事實(shí)上,Linux平臺(tái)下靜態(tài)庫(kù).a文件使用獨(dú)立的歸檔工具ar建立,為了使鏈接器能夠有效地查找?guī)熘邪母鱾€(gè)目標(biāo)文件以及符號(hào),經(jīng)常還需要一個(gè)叫做ranlib的工具在.a文件中建立索引。
在鏈接時(shí),鏈接器想普通目標(biāo)文件一樣使用靜態(tài)庫(kù),僅僅多了在庫(kù)中查找符號(hào)及對(duì)應(yīng)目標(biāo)文件的過(guò)程。
動(dòng)態(tài)鏈接庫(kù)
動(dòng)態(tài)鏈接庫(kù)和靜態(tài)庫(kù)差異較大,Linux平臺(tái),它由ELF格式直接支持。但由于共享庫(kù)的特殊性,它需要一些特殊特性的支持:
PIC(Position Independent Code),位置無(wú)關(guān)代碼。動(dòng)態(tài)共享庫(kù)需要在運(yùn)行時(shí)動(dòng)態(tài)地加載到內(nèi)存,為了在各個(gè)進(jìn)程中調(diào)用共享庫(kù)中的代碼和數(shù)據(jù),就需要將該庫(kù)映射到不同進(jìn)程的進(jìn)程空間。由于各個(gè)進(jìn)程的地址空間使用情況不盡相同,很難將共享庫(kù)映射到各進(jìn)程相同的位置。這樣一來(lái),就對(duì)共享庫(kù)的代碼提出了挑戰(zhàn),它需要能夠在不同的地址區(qū)間上都正確的執(zhí)行。位置無(wú)關(guān)代碼就是因此而提出的。
位置無(wú)關(guān)代碼的基本思想是這樣的:將共享中對(duì)絕對(duì)地址的引用轉(zhuǎn)化為相對(duì)地址的形式。對(duì)于函數(shù)調(diào)用,實(shí)現(xiàn)起來(lái)很簡(jiǎn)單,因?yàn)榇a是只讀的,指令間的相對(duì)地址也是固定的,只需要將函數(shù)調(diào)用轉(zhuǎn)化會(huì)相對(duì)地址即可。但對(duì)于數(shù)據(jù)的引用就復(fù)雜很多了,由于各個(gè)進(jìn)程都需要訪問(wèn)共享庫(kù)中的數(shù)據(jù),而這些進(jìn)程通常是毫無(wú)關(guān)聯(lián)的,一個(gè)進(jìn)程對(duì)共享庫(kù)數(shù)據(jù)的修改不應(yīng)該被其他進(jìn)程看到。一種好的方法就是讓各個(gè)進(jìn)程都擁有自己的一份數(shù)據(jù)拷貝。但這又引出一個(gè)問(wèn)題,共享庫(kù)是被動(dòng)態(tài)映射的,數(shù)據(jù)空間也只能在映射時(shí)才需要分配,那么在共享庫(kù)代碼中如何引用這些數(shù)據(jù),以達(dá)到不同進(jìn)程使用相同的代碼訪問(wèn)不同的數(shù)據(jù)呢?
于是另外一種結(jié)構(gòu)被引入了,即GOT(Global Offset Table),全局偏移量表。它的基本思想也是相對(duì)地址引用。在共享庫(kù)的數(shù)據(jù)段加入GOT,GOT的表項(xiàng)保存各個(gè)數(shù)據(jù)的地址。由于共享庫(kù)中指令和數(shù)據(jù)段的相對(duì)地址在鏈接后是固定的,這樣在指令中就可以使用相對(duì)地址來(lái)找到GOT的起始地址,然后根據(jù)各個(gè)數(shù)據(jù)在GOT的偏移量來(lái)找到其對(duì)應(yīng)的地址。而該地址是在共享庫(kù)被映射到進(jìn)程空間時(shí),由動(dòng)態(tài)鏈接器在相應(yīng)的進(jìn)程空間中分配并設(shè)置的。
接下來(lái)的問(wèn)題就是,進(jìn)程中如何調(diào)用共享庫(kù)中的代碼呢?ELF使用一種延遲加載的方法,即當(dāng)進(jìn)程調(diào)用共享庫(kù)中一個(gè)函數(shù)時(shí),才解析該函數(shù)的地址,且只有第一次才解析,第一次解析之后的調(diào)用就不會(huì)被再次解析,而是將之前解析到的地址保存下來(lái)。這里又引入一種機(jī)制,叫做PLT(Procedure Linkage Table),它和GOT一起(使用共享庫(kù)的進(jìn)程也有一個(gè)GOT)引入了一個(gè)函數(shù)調(diào)用的間接層。
類似共享庫(kù)的GOT,進(jìn)程的GOT表項(xiàng)保存了本進(jìn)程引用的共享庫(kù)中的函數(shù)地址,但在第一次對(duì)該函數(shù)的調(diào)用之前,該表項(xiàng)保存的并不是函數(shù)的地址,而是指向PLT中一個(gè)指令的地址。為了方便說(shuō)明問(wèn)題,假設(shè)進(jìn)程中main函數(shù)引用了libmath.so中的函數(shù)add,那么PLT大致是這個(gè)樣子的,
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | .plt0:0x080483d0: pushl 0x8049ff80x080483d6: jmp *0x8049ffc ... add@plt:0x080483e0 <+0>: jmp *0x804a0000x080483e6 <+6>: push $0x00x080483eb <+11>: jmp 0x80483d0 ... main: ...0x080484ec <+24>: call 0x80483e0 <add@plt> # 對(duì)add函數(shù)的調(diào)用 ... |
第十二行對(duì)add函數(shù)的調(diào)用跳轉(zhuǎn)到第五行,這是一條jmp指令,0x804a000是它的地址操作數(shù),該地址即是add在GOT中的地址項(xiàng),最初該地址處保存的地址是0x080483e6,即jmp指令的下一條push指令。于是最初jmp指令執(zhí)行后沒(méi)有任何效果,直接執(zhí)行下一條指令。push指令將add在重定位項(xiàng)的索引入棧,通過(guò)該重定位項(xiàng)可以得到add符號(hào)本身(即字符串a(chǎn)dd)。然后又是一條jmp指令它跳轉(zhuǎn)到第二行,又是一個(gè)push指令,接下來(lái)又是jmp指令,這個(gè)jmp指令也使用了GOT中的一個(gè)表項(xiàng),該表項(xiàng)存儲(chǔ)的是動(dòng)態(tài)鏈接器(ld.so)的加載/解析函數(shù)。在解析函數(shù)中,查找add符號(hào)在共享庫(kù)中的地址,將該地址填入add對(duì)應(yīng)的GOT表項(xiàng),然后跳轉(zhuǎn)至add函數(shù)開(kāi)始執(zhí)行。到下一次調(diào)用add函數(shù),第五行的jmp指令就直接跳轉(zhuǎn)到add函數(shù)了。
動(dòng)態(tài)鏈接基本就是這個(gè)過(guò)程了。在這個(gè)過(guò)程中有許多有意思的指令,將棧玩弄于鼓掌,像變魔術(shù)一樣,有興趣的話可以使用gdb等相關(guān)工具調(diào)試一下。
一些工具
玩弄二進(jìn)制,很多實(shí)用工具是離不了的,最重要的就是GNU Binutils二進(jìn)制工具鏈了。包括查看ELF文件信息的readelf,對(duì)目標(biāo)文件、可執(zhí)行文件、共享庫(kù)、core內(nèi)存轉(zhuǎn)儲(chǔ)文件等反匯編的objdump,重量級(jí)的調(diào)試器gdb,查看共享庫(kù)使用情況的ldd等等。
一些參考資料
了解鏈接器工作原理,現(xiàn)成的資料并不多,《Linkers and Loaders》算是經(jīng)典了,中文版也可以買得到,翻譯得還算中規(guī)中矩。另外,《程序員的自我修養(yǎng):鏈接、裝載與庫(kù)》寫的很淺顯,不錯(cuò)的一本書。
為了更好的理解鏈接器,必須對(duì)ELF的細(xì)節(jié)有所了解,Executable and Linkable Format這份文檔可以參考。
研究二進(jìn)制,匯編知識(shí)是必須的,如果是Linux平臺(tái),了解些GNU 匯編(AT&T匯編)是再好不過(guò)了,不過(guò)講解GNU匯編的資料更是少上加少,布魯姆的《匯編語(yǔ)言程序設(shè)計(jì)》雖然內(nèi)容不多,但對(duì)非專業(yè)匯編程序員也足夠用了,這本書是有英文電子版的。
有了相關(guān)工具和入門資料,剩下的就是折騰了。
最后,附上之前做的幻燈片,就像這篇文章一樣,臭長(zhǎng),枯燥
總結(jié)
以上是生活随笔為你收集整理的链接器相关的一些基本问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 奇迹暖暖梦幻搭配2选1女王大人攻略
- 下一篇: 关于Xcode上的Other linke