本書講述構(gòu)建程序的關(guān)鍵工具鏈接器和加載器,內(nèi)容包括鏈接和加載、體系結(jié)構(gòu)、目標(biāo)文件、存儲分配、符號管理、庫、重定位、加載和覆蓋、共享庫、動態(tài)鏈接和加載、動態(tài)鏈接的共享庫,以及著眼于成熟的現(xiàn)代鏈接器所做的一些變化;并介紹一個持續(xù)的實踐項目,即使用Perl語言開發(fā)一個可用的小鏈接器。本書適合高校計算機(jī)相關(guān)專業(yè)的學(xué)生、實習(xí)程序員、語言設(shè)計者和開發(fā)人員閱讀參考。
自計算機(jī)出現(xiàn)以來,鏈接器和加載器可以說一直是重要的軟件開發(fā)工具之一。鏈接器和加載器使得我們可以按照模塊來開發(fā)程序,而不必開發(fā)一個單獨的大文件。
早在1947年,程序員就開始使用加載器技術(shù)。這是一種很初級的加載器工作方式,如果程序的若干個例程(routine)存儲在多個不同的磁帶上,那么就借助加載器將它們依次加載到內(nèi)存中,并將它們合并、重定位以組合成一個程序。在20世紀(jì)60年代早期,這些加載器就已經(jīng)發(fā)展得相當(dāng)完善了,甚至具備編輯的功能。由于當(dāng)時內(nèi)存很貴且容量有限,計算機(jī)的速度也很慢(以今天的標(biāo)準(zhǔn)),為了充分利用這樣的硬件,這些加載器引入了很多復(fù)雜的特性。例如,使用復(fù)雜的內(nèi)存覆蓋策略解決內(nèi)存不足的問題,將大容量的程序加載到有限的內(nèi)存中;使用鏈接文件重編輯的機(jī)制解決算力不足的問題;使用已鏈接的模塊以節(jié)省重新編譯程序的時間;等等。
20世紀(jì)70到80年代,鏈接技術(shù)幾乎沒有什么進(jìn)展。鏈接器趨向于更加簡單。虛擬內(nèi)存技術(shù)將應(yīng)用程序和覆蓋機(jī)制中的大多數(shù)內(nèi)存管理工作都轉(zhuǎn)移給了操作系統(tǒng),同時,計算機(jī)的處理速度變得越來越快,硬盤容量越來越大,這使得程序員在更新個別模塊時也可以重新鏈接整個程序,而不必僅僅鏈接修改的地方。從20世紀(jì)90年代起,由于增加了諸如動態(tài)鏈接共享庫和C 的諸多現(xiàn)代特性,鏈接器又開始變得復(fù)雜起來。處理器技術(shù)的發(fā)展也促進(jìn)了鏈接器的發(fā)展。例如,具有長指令字和編譯時訪存調(diào)度等特性的先進(jìn)處理器架構(gòu)(在IA64處理器中開始出現(xiàn))需要將一些新的特性加入鏈接器中,以確保在鏈接器中生成的代碼可以滿足處理器的一些復(fù)雜需求,從而充分發(fā)揮硬件的新特性。
讀者對象
本書可供下述幾類讀者閱讀。
學(xué)生:由于鏈接過程看起來似乎非常簡單,操作的過程也很簡捷自然,編譯原理和操作系統(tǒng)課程通常對鏈接和加載的過程缺乏重視。對于使用Fortran、Pascal、C進(jìn)行簡單編程的任務(wù),以及不使用內(nèi)存映射或共享庫的操作系統(tǒng)而言,這么做可能是對的;但是現(xiàn)在情況不一樣了。C 、Java和其他的面向?qū)ο笳Z言需要更加復(fù)雜的鏈接環(huán)境。使用內(nèi)存映射的可執(zhí)行程序、共享庫和動態(tài)鏈接技術(shù)都會影響操作系統(tǒng)的很多部分,操作系統(tǒng)的設(shè)計者如果忽略鏈接問題可能會給系統(tǒng)帶來很大的麻煩。
程序員:程序員也需要知道鏈接器都做了什么,尤其是對現(xiàn)代語言而言。C 語言在鏈接器中引入了很多新的特性,如果不能正確理解這一過程,在鏈接大型的C 程序時就容易產(chǎn)生一些難以診斷的bug。例如,常見的情況是靜態(tài)構(gòu)造函數(shù)沒有按照程序員預(yù)期的順序執(zhí)行。反之,如果能正確合理地使用鏈接器,就能夠發(fā)揮共享庫和動態(tài)鏈接等特性的強(qiáng)大功能,提高程序的靈活性。
編程語言的設(shè)計者和開發(fā)者。編程語言的設(shè)計者應(yīng)該在構(gòu)建語言和編譯器時了解鏈接器應(yīng)該做什么,以及能做什么。在過去的30年中必須借助手工完成的編程細(xì)節(jié),今天在C 中已經(jīng)可以借助鏈接器自動處理了。(想象一下,如何能在C語言中實現(xiàn)和C 中的模板(template)相同的功能;或者,對于數(shù)百個C語言源文件組成的工程,如何保證這些文件中的初始化例程可以在主函數(shù)開始之前被正確地執(zhí)行。為了做到這些,程序員需要完成大量工作。)有了功能更強(qiáng)大的鏈接器,未來的語言將更加智能,能夠自動完成更多的常規(guī)任務(wù)。由于鏈接器是編譯過程中將整個程序的代碼放在一起處理的階段,因此鏈接器可以將程序作為一個整體進(jìn)行變換處理,也可以引入更多的全局程序優(yōu)化功能。
(編寫鏈接器的人員當(dāng)然都需要本書。但是全球所有的鏈接器設(shè)計者大概只能坐滿一個房間,而且其中至少有一半被邀請作為本書的審閱人,相信他們已經(jīng)看過本書了。)
章節(jié)內(nèi)容
第1章,鏈接和加載。這一章對鏈接的過程進(jìn)行了簡短的歷史回顧,并討論了鏈接過程中的各個階段。后通過一個麻雀雖小,五臟俱全的例子來展示鏈接器的工作過程:對于一個Hello,world程序,我們分析了以編譯好的目標(biāo)文件為輸入,生成一個可執(zhí)行程序的過程。
第2章,體系結(jié)構(gòu)相關(guān)問題。這一章從鏈接器設(shè)計的角度分析了計算機(jī)體系結(jié)構(gòu)的技術(shù)發(fā)展方向。我們分析了典型的精簡指令集體系結(jié)構(gòu)SPARC,古老而富有活力的寄存器內(nèi)存體系結(jié)構(gòu)IBM 360/370,以及自成一派的Intel x86體系結(jié)構(gòu)。對于每種體系結(jié)構(gòu),我們會討論內(nèi)存架構(gòu)、程序?qū)ぶ芳軜?gòu)和指令中的地址格式等重要因素對鏈接器的影響。
第3章,目標(biāo)文件。這一章分析了目標(biāo)文件和可執(zhí)行文件的內(nèi)部結(jié)構(gòu)。本章從分析簡單的MS-DOS的.COM文件開始,進(jìn)而不斷擴(kuò)展到其他復(fù)雜的文件,包括DOS的EXE文件格式、Windows的COFF格式和PE格式(EXE和DLL)、UNIX的a.out格式和ELF格式以及Intel/Microsoft的OMF格式等。
第4章,存儲空間管理。本章介紹了鏈接過程的個階段,即以段為單位為被鏈接的程序分配存儲空間。我們以一個實際使用的鏈接器為例分析了這一過程。
第5章,符號管理。本章介紹了符號綁定和解析的過程,這是一個將符號解析為機(jī)器地址的過程,程序中的符號可能在一個文件中被引用,而它的定義出現(xiàn)在另一個文件中。
第6章,庫。本章介紹了關(guān)于目標(biāo)代碼庫創(chuàng)建和使用的
譯者序
前言
第1章 鏈接和加載1
1.1 鏈接器和加載器做什么1
1.2 從歷史發(fā)展的角度分析地址綁定1
1.3 鏈接與加載3
1.3.1 兩遍鏈接4
1.3.2 目標(biāo)代碼庫5
1.3.3 重定位和代碼修改6
1.4 編譯驅(qū)動器7
1.5 鏈接:一個真實的例子9
1.6 練習(xí)12
第2章 體系結(jié)構(gòu)相關(guān)問題13
2.1 應(yīng)用程序二進(jìn)制接口13
2.2 內(nèi)存地址13
2.3 地址構(gòu)成規(guī)則15
2.4 指令格式15
2.5 過程調(diào)用和可尋址性16
2.6 數(shù)據(jù)訪問和指令引用19
2.6.1 IBM 37019
2.6.2 SPARC21
2.6.3 Intel x8623
2.7 分頁和虛擬內(nèi)存24
2.7.1 程序的地址空間26
2.7.2 文件映射27
2.7.3 共享庫和程序28
2.7.4 位置無關(guān)代碼28
2.8 Intel 386分段29
2.9 嵌入式體系結(jié)構(gòu)31
2.9.1 怪異的地址空間31
2.9.2 非統(tǒng)一內(nèi)存31
2.9.3 內(nèi)存對齊31
2.10 練習(xí)32
第3章 目標(biāo)文件35
3.1 目標(biāo)文件中有什么35
3.2 空目標(biāo)文件格式:MS-DOS的.COM文件36
3.3 代碼分段:UNIX的a.out文件36
3.3.1 a.out文件頭37
3.3.2 與虛擬內(nèi)存的交互38
3.4 重定位:MS-DOS的EXE文件41
3.5 符號和重定位43
3.6 可重定位的a.out格式43
3.6.1 重定位項44
3.6.2 符號和字符串44
3.6.3 a.out格式小結(jié)45
3.7 UNIX ELF格式45
3.7.1 可重定位文件47
3.7.2 ELF可執(zhí)行文件51
3.7.3 ELF格式小結(jié)52
3.8 IBM 360目標(biāo)文件格式52
3.8.1 ESD記錄53
3.8.2 TXT記錄54
3.8.3 RLD記錄54
3.8.4 END記錄55
3.8.5 小結(jié)55
3.9 微軟的可移植可執(zhí)行文件格式55
3.9.1 PE特有區(qū)段59
3.9.2 運(yùn)行PE可執(zhí)行文件60
3.9.3 PE和COFF61
3.9.4 PE文件小結(jié)61
3.10 Intel/Microsoft的OMF文件格式61
3.10.1 OMF記錄62
3.10.2 OMF文件的細(xì)節(jié)63
3.10.3 OMF格式小結(jié)65
3.11 不同目標(biāo)文件格式的比較65
3.12 練習(xí)66
3.13 項目66
第4章 存儲空間管理69
4.1 段和地址69
4.2 簡單的存儲布局69
4.3 多種類型的段70
4.4 段與頁面的對齊72
4.5 公共塊和其他特殊段72
4.5.1 公共塊72
4.5.2 C 重復(fù)代碼消除73
4.5.3 初始化和終結(jié)75
4.5.4 IBM偽寄存器76
4.5.5 專用鏈接表78
4.5.6 x86的存儲分配策略78
4.6 鏈接器控制腳本79
4.7 嵌入式系統(tǒng)的存儲分配81
4.8 實際使用的存儲分配策略81
4.8.1 UNIX a.out鏈接器的存儲分配策略81
4.8.2 ELF文件中的存儲分配策略82
4.8.3 Windows鏈接器的存儲分配策略83
4.9 練習(xí)84
4.10 項目85
第5章 符號管理87
5.1 符號名綁定和解析87
5.2 符號表的格式87
5.2.1 模塊表89
5.2.2 全局符號表90
5.2.3 符號解析91
5.2.4 特殊符號91
5.3 名稱修改92
5.3.1 簡單的C和Fortran名稱修改92
5.3.2 C 類型編碼:類型和范圍93
5.3.3 鏈接時類型檢查95
5.4 弱外部符號和其他類型的符號95
5.5 維護(hù)調(diào)試信息96
5.5.1 行號信息96
5.5.2 符號和變量信息96
5.5.3 實際的問題97
5.6 練習(xí)98
5.7 項目98
第6章 庫99
6.1 庫的目的99
6.2 庫的格式99
6.2.1 使用操作系統(tǒng)99
6.2.2 UNIX和Windows的歸檔文件100
6.2.3 擴(kuò)展到64位102
6.2.4 Intel OMF庫文件102
6.3 創(chuàng)建庫文件103
6.4 搜索庫文件104
6.5 性能問題105
6.6 弱外部符號105
6.7 練習(xí)106
6.8 項目106
第7章 重定位109
7.1 硬件和軟件重定位109
7.2 鏈接時重定位和加載時重定位110
7.3 符號重定位和段重定位110
7.4 基本的重定位技術(shù)111
7.4.1 指令重定位112
7.4.2 ECOFF段重定位114
7.4.3 ELF重定位115
7.4.4 OMF重定位116
7.5 可重鏈接和可重定位的輸出格式116
7.6 重定位項的其他格式117
7.6.1 以鏈表形式組織的引用117
7.6.2 以位圖形式組織的引用117
7.6.3 特殊段117
7.7 特殊情況的重定位118
7.8 練習(xí)118
7.9 項目119
第8章 加載和覆蓋121
8.1 基本的加載過程121
8.2 帶重定位的基本加載過程122
8.3 位置無關(guān)代碼122
8.3.1 TSS/360的位置無關(guān)代碼123
8.3.2 為每個例程建立的指針表123
8.3.3 目錄表123
8.3.4 ELF的位置無關(guān)代碼124
8.3.5 位置無關(guān)代碼的開銷和收益126
8.4 自舉加載127
8.5 基于樹狀結(jié)構(gòu)的覆蓋技術(shù)128
8.5.1 定義覆蓋技術(shù)129
8.5.2 覆蓋技術(shù)的實現(xiàn)131
8.5.3 覆蓋技術(shù)的其他細(xì)節(jié)132
8.5.4 覆蓋技術(shù)小結(jié)132
8.6 練習(xí)13