標籤:
《深入理解電腦系統》第七章 連結
連結是將各種代碼和資料部分收集起來並組合成為一個單一檔案的過程,這個檔案可被載入(貨被拷貝)到儲存空間並執行。
連結的時機
- 編譯時間,也就是在原始碼被翻譯成機器代碼時
- 載入時,也就是在程式被載入器載入到儲存空間並執行時
- 運行時,由應用程式執行
連結器使分離編譯稱為可能。
連結是將各種代碼和資料部分收集起來並組合成為一個單一檔案的過程,這個檔案可被載入(或拷貝)到儲存空間並執行。
連結可以執行於編譯時間,也就是在原始碼被翻譯成機器代碼時;也可以執行於載入時,也就是在程式被載入器載入到儲存空間並執行時;甚至執行於運行時,由應用程式來執行。
在早期的電腦系統中,連結是手動執行的。在現代系統中,連結是由叫連結器的自動執行的。
7.1 編譯器驅動程式
大多數編譯系統提供編譯驅動程式,它代表使用者在需要時調用語言前置處理器、編譯器、彙編器和連結器。
例子: 函數main()調用swap交換外部全域資料buf中的兩個元素。這個例子貫穿全文,分析連結是如何工作的。
| 123456789101112 |
/* $begin main *//* main.c */void swap(); int buf[2] = {1, 2}; int main(){ swap(); return 0;}/* $end main */ |
| 1234567891011121314151617 |
/* $begin swap *//* swap.c */extern int buf[]; int *bufp0 = &buf[0];int *bufp1; void swap(){ int temp; bufp1 = &buf[1]; temp = *bufp0; *bufp0 = *bufp1; *bufp1 = temp;}/* $end swap */ |
7.2 靜態連結
靜態連結器以一組可重定位目標檔案和命令列參數作為輸入,產生一個完全連結的可以載入和啟動並執行可執行目標檔案作為輸出。輸入的可重定位目標檔案由各種不同的代碼和資料節(section)組成。指令在一個節中,初始化的全域變數在另一個節中,而未初始化的變數又在另外一個節中。
為了構造可執行檔,連結器必須完成兩個任務:符號解析,重定位
- 符號解析 目標檔案定義和引用符號。符號解析的目的是將每個符號引用剛好和一個符號定義聯絡起來。
- 重定位 編譯器和彙編器產生從地址0開始的餓代碼和資料節。連結器通過把每個符號定義與一個儲存空間位置聯絡起來,然後修改所有對這些符號的引用,使得它們指向這個儲存空間位置,從而重定位這些節。
連結器的一些基本事實:目標檔案純粹是位元組塊的集合。這些塊中,有些包含程式碼,有些則包含程式資料,而其他的則包含指導連結器和載入器的資料結構。連結器將這些塊串連起來,確定被串連塊的運行時位置,並且修改代碼和資料區塊中的各種位置。連結器和彙編器已經完成了大部分工作。
目標檔案純粹是位元組快的集合。這些塊中,有些包含程式碼,有些則包含程式資料,而其他的則包括指導連結器和載入器的資料結構。連結器將這些塊連結起來,確定被串連塊的運行時位置,並且修改代碼和資料區塊中的各種位置。連結器對目標機器瞭解甚少。產生目標檔案的編譯器和彙編器已經完成了大部分工作。
7.3 目標檔案1.三種形式
- 可重定位目標檔案。包含二進位代碼和資料,其形式可以在編譯時間與其他可重定位目標檔案合并起來,建立一個可執行目標檔案。
- 可執行目標檔案。包含二進位代碼和資料,其形式可以被直接拷貝到儲存空間並執行。
- 共用目標檔案。一種特殊類型的可重定位目標檔案,可以在載入或者運行地被動態地載入到儲存空間並連結。
編譯器和彙編器產生可重定位目標檔案(包括共用目標檔案)。連結器產生可執行目標檔案。從技術上來說,一個目標模組就是一個位元組序列,而一個目標檔案就是一個存放在磁碟檔案中的目標模組。
編譯器和彙編器產生可重定位目標檔案(包括共用目標檔案)。連結器產生可執行目標檔案。從技術上來說,一個目標模組就是一個位元組序列,而一個目標檔案就是一個存放在磁碟檔案中的目標模組。
編譯器和彙編器產生可重定義目標檔案(包括共用目標檔案)。連結器產生可執行目標檔案。
各個系統之間,目標檔案格式都不相同。
7.4 可重定位目標檔案
一個典型的ELF可重定位目標檔案的格式P451。ELF頭(ELF header)以一個16位元組的序列開始,這個序列描述了產生該檔案的系統的字的大小和位元組順序。ELF頭剩下的部分包含協助連結器文法分析和解釋目標檔案的資訊。其中包括ELF頭的大小、目標檔案的類型(如可重定位、可執行或是共用的)、機器類型(如IA32)、節頭部表的檔案位移,以及節頭部表中的條目大小和數量。不同的節的位置和大小是由節頭部表描述的,其中目標檔案中每個節都有一個固定大小的條目。
夾在ELF頭和節頭部表之間的都是借。一個典型的ELF可重定位目標檔案包含下面幾個節:
- .text 已編譯器的機器代碼
- .rodata 唯讀資料
- .data 已初始化的全域C變數。局部C變數在運行時儲存在棧中,既不出現在.data節中 ,也不出現在.bss節中。
- .bass 未初始化的全域C變數。在目標檔案中這個節不佔據實際的空間,它僅僅是一個預留位置。目標檔案格式區分初始化和未初始設定變數是為了空間效率:在目標檔案中,未初始設定變數不需要佔據任何實際的磁碟空間。
- .symtab 一個符號表,它存放在程式中定義和引用的函數和全域變數的資訊。每個可重定位目標檔案在.symtab中都有一張符號表 。
- .rel.text 一個.text節中位置的列表,當連結器吧這個目標檔案和其他檔案結合時,需要修改這些位置。一般而言,任何調用外部函數或引用全域變數的指令都需要修改。另一方面,調用本地函數的指令則不需要修改。注意,可執行目標檔案中並不需要重定位資訊,因此通常省略,除非使用者顯示第指示連結器包含這些資訊。
- .rel.data 被模組引用或定義的任何全域變數的重定位資訊。一般而言,任何已初始化的全域變數,如果它的初始值是一個全域變數地址或者外部定義函數的地址,都需要被修改。
- .debug 一個偵錯符號表,其條目是程式總定義的局部變數和類型定義,程式中定義和引用的 全域變數,以及原始的C源檔案。
- .line 原始C源檔案中的行號和.text節中機器指令之間的映射。
- .strtab 一個字串表,其內容包括.symtab和.debug節中的符號表,以及節頭部中的節名字。
7.5 符號和符號表
每個可重定位目標模組m都有一個符號表,包含m所定義和引用的符號的資訊。
在連結器的上下文中,三種不同的符號:
1.由m定義並能被其他模組引用的全域符號。全域連結器對應於非靜態C函數以及被定義為Cstatic 屬性的全域變數。
2.由其他模組定義並被模組m以引用的全域符號——外部符號,對應於定義在其他模組中的C函數和變數
3.只被模組m定義和引用的本地符號。
在連結器的上下文中,有三種不同的符號:
- 由m定義並能被其他模組引用的全域符號
- 由其他模組定義並被模組m引用的全域符號
- 只被模組m引用的本地符號
7.6 符號解析7.6.1 連結器如何解析多重定義的全域符號
在編譯是,編譯器向彙編器輸出每個全域符號,或者是強或者是弱,而彙編器把這個資訊隱含地編碼在可重定位目標檔案的符號表裡。函數和已初始化的全域變數時強符號,未初始化的全域變數是弱符號。
根據強弱符號的定義,Unix連結器使用下面的規則來處理多重定義的符號:
- 規則1:不允許有多個強符號。
- 規則2:如果有一個強符號和多個弱符號,那麼選擇強符號。
- 規則3:如果有多個弱符號,那麼從這些弱符號中任意選擇一個。
7.6.2 與靜態庫連結
在Unix系統中,靜態庫以一種稱為存檔的特殊檔案格式村凡在磁碟中。封存檔案是一組串連起來的可重定位目標檔案的集合,有一個頭部用來描述每個成員目標檔案的大小和位置。封存檔案名由尾碼.a標識。
7.6.3 連結器如何使用靜態庫來解析引用
在符號解析的階段,連結器從左至右按照它們在編譯器驅動程式命令列上出現的相同順序來掃描可重定位目標檔案和封存檔案。在這次掃描中,連結器維持一個可重定位目標檔案的集合E(這個集合中的檔案會被合并起來形成可執行檔),一個未解析的符號(即引用了但是尚未定義的符號)集合U,以及一個在前面輸入檔案中已定義的符號集合D。初始時,E、U和D都是空的。
1.對於命令列上的每個輸入檔案f,連結器會判斷f是一個目標檔案還是一個封存檔案。如果f是一個目標檔案,那麼連結器吧f添加到E, 修改U和D來反映f中的符號定義和引用,並繼續下一個輸入檔案。
2.如果f是一個封存檔案,那麼連結器就嘗試匹配U中未解析的符號和由封存檔案成員定義的符號。如果某個封存檔案成員m,定義了一個符號來解析U中的一個引用,那麼就將m加到E中,並且連結器修改U和D來反映m中的符號定義和引用。對封存檔案中所有的成員目標檔案都反覆進行這個過程,直到U和D都不再發生變化。在此時,任何不包含在E中的目標檔案都簡單地被丟棄,而連結器將繼續處理下一個輸入檔案。
3.如果當連結器完成對命令列上輸入檔案的掃描後,U是非空的,那麼連結器就好輸出一個錯誤並終止。否則,它會合并和重定位E中的目標檔案,從而構建輸出的可執行檔。
這種演算法會導致一些令人困擾的連結時錯誤,因為命令列上的庫和目標檔案的順序非常重要。在命令列中,如果定義一個符號的庫出現在引用這個符號的目標檔案之前,那麼引用就不能被解析,連結會失敗。關於庫的一般準則是將它們放在命令列的 結尾。
另一方面,如果庫不是相互獨立的,那麼它們必須排序,使得對於每個被封存檔案的成員外部參考的符號s,在命令列中至少有一個s的定義實在對s的引用之後的。
如果需要滿足依賴需求,可以在命令列上重複庫。
7.7 重定位
一旦連結器完成了符號解析這一步,它就是把代碼中的每個符號引用和確定的一個符號定義(即它的一個輸入目標模組中的一個符號表條目)聯絡起來。在此時,連結器就知道它的輸入目標模組中的代碼節和資料節的確切大小。現在就可以開始重定位了,在這個步驟中,將合并輸入模組,並為每個符號分配運行時地址。
重定位有兩步組成:
1.重定位節和符號定義。在這一步中,連結器將所有相同類型的節合并為同一類型的新的彙總節。然後,連結器將運行時儲存空間地址賦給新的彙總節,賦給輸入模組定義的每個節,以及賦給輸入模組定義的每個符號。當這一步完成時,程式中的每個指令和全域變數都有唯一的運行時儲存空間地址了。
2.重定位節中的符號引用。在這一步中,連結器修改代碼節和資料節中對每個符號的引用,使得它們指向正確的運行時地址。為了執行這一步,連結器依賴於稱為重定位條目的可重定位目標模組中的資料結構。
7.7.1 重定位條目
當彙編器產生一個目標模組時,它並不知道資料和代碼最終存放在儲存空間中的什麼位置。它也不知道這個模組引用的任何外部定義的函數或者全域變數的位置。所以,無論何時彙編器遇到對最終位置位置的目標引用,它就會產生一個重定位條目,告訴連結器在將目標檔案合并成可執行檔時如何修改這個引用。代碼的重定位條目放在.rel.text中。 已初始化的資料的重定位條目放在.rel.data中。
ELF定義了11種不同的重定位類型。我們只關心其中兩種最基本的重定位類型:
- R_386_PC32 重定位一個使用32位PC相對位址的引用。
- R_386_32 重定位一個使用32位絕對位址的引用。
7.7.2 重定位器號引用 7.8 可執行目標檔案
可執行目標檔案的格式類似於可重定位目標檔案的格式。ELF頭部描述檔案的總體格式。它還包括程式的進入點,也就是當程式運行時要執行的第一條指令的地址。.text 、.rodata和.data 節和可重定位目標檔案中的節是相似的,除了這些節已經被重定位到它們最終的運行時儲存空間地址以外。.init節定義了一個小函數,叫做_init,程式的初始化代碼會調用它。因為可執行檔是完全連結的(已被重定位了),所以它不再需要.rel節。
ELF可執行檔被設計得很容易載入到儲存空間,可執行檔的連續的片被映射到連續的儲存空間段。段頭部表描述了這種映射關係。
7.9 載入可執行目標檔案
要運行可執行目標檔案p,可以在Unix外殼的命令列中輸入它的名字:
因為p不是一個內建的外殼命令,所以外殼會認為p是一個可執行目標檔案,通過調用某個駐留在儲存空間中的稱為載入器(loader)的作業系統代碼來運行它。任何Unix程式都可以通過調用execve函數來調用載入器。載入器將可執行目標檔案中的代碼和資料從磁碟拷貝到儲存空間中,然後通過跳轉到程式的第一條指令或進入點來運行該程式。這個將程式拷貝到儲存空間並啟動並執行過程叫做載入。
每個Unix程式都有一個運行時儲存空間映像。例如:在32位Linux系統中,程式碼片段總是從地址(0x8048000)處開始。資料區段是在接下來的下一個4KB對齊的地址處。運行時堆在讀/寫段之後接下來的第一個4KB對齊的地址處,並童工調用malloc庫往上增長。還有一個段是為共用庫保留的。使用者棧總是從最大的合法使用者地址開始,向下增長的(向低儲存空間地方向增長)。從棧的上部開始的段是為作業系統駐留儲存空間的部分(也就是核心)的代碼和資料保留的。
在可執行檔中段頭部表的指導下,載入器將可執行檔的相關內容拷貝到代碼和資料區段。接下來,載入器跳轉到程式的進入點,也就是符號_start的地址。在_start地址處的啟動代碼是在目標檔案ctrl.o中定義的,對所有的C程式都是一樣的。在從.text和.init節中調用了初始化常式後,啟動代碼調用atexti常式,這個程式附加了一系列在應用程式正常中止時應該調用的程式。exit函數運行atexit註冊的函數,然後通過調用_exit將控制返回給作業系統。接著,啟動代碼調用應用程式的main程式,它會開始執行我們的C代碼。在應用程式返回之後,啟動代碼調用_exit程式,它將控制返回給作業系統。
載入的工作流程:
UNIX系統中的每個程式都運行在一個進程上下文中,有自己的虛擬位址空間。當外殼運行一個程式時,父外殼進程產生一個子進程,它是父進程的一個複製品。子進程通過execve系統調用啟動載入器。載入器刪除子進程現有的虛擬儲存空間段,並建立一組新的代碼、資料、堆和棧段、新的棧和堆段被初始化為零。通過將虛擬位址空間中的頁映射到可執行檔的頁大小的片,新的代碼和資料區段被初始化為可執行檔的內容。最後,載入器跳轉到_start地址,它最終會調用應用程式的main函數。除了一些頭部資訊,在載入過程中沒有任何從磁碟到儲存空間的資料拷貝。直到CPU應用一個被映射的虛擬頁才會進行拷貝,此時,作業系統利用它的頁面調度機制自動將頁面從磁碟傳送到儲存空間。
7.10 動態連結共用庫
共用庫是致力與解決靜態庫缺陷的一個現代創新產物。共用庫是一個目標模組,在運行時,可以載入到任意的儲存空間地址,並加一個在儲存空間中的程式連結起來。這個過程稱為動態連結,是由一個叫做動態連結器的程式來執行的。共用庫也稱為共用目標,在Unix系統中通常用.so尾碼來表示。
7.11 從應用程式中載入和連結共用庫
動態連結在現實中的例子:
7.12 與位置無關的代碼(PIC)
PIC資料引用
PIC函數調用
7.13 處理目標檔案的工具
- AR:建立靜態庫,插入、刪除、列出和提取成員。
- STRINGS:列出一個目標檔案中所有可列印的字串。
- STRIP:從目標檔案中刪除符號表資訊。
- NM:列出一個目標檔案中符號表定義的符號。
- SIZE:列出目標檔案中節的名字和大小。
- READELF:能夠顯示一個目標檔案的所有資訊。
- OBJDUMP:反組譯碼
- LDD:列出一個可執行檔運行時需要的共用庫。
每個可重定位目標模組m都有一個符號表,包含m所定義和引用的符號的資訊。
在連結器的上下文中,三種不同的符號:
1.由m定義並能被其他模組引用的全域符號。全域連結器對應於非靜態C函數以及被定義為Cstatic 屬性的全域變數。
2.由其他模組定義並被模組m以引用的全域符號——外部符號,對應於定義在其他模組中的C函數和變數
3.只被模組m定義和引用的本地符號。
《深入理解電腦系統》第七章 連結