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