標籤:
第七章 連結
連結是將各種代碼和資料部分收集起來並組合成為一個單一檔案的過程,這個檔案可被載入(或拷貝)到儲存空間並執行。連結可以執行於編譯時間,也就是在原始碼被翻譯成機器代碼時;也可以執行於載入時,也就是在程式被載入器載入到儲存空間並執行時;甚至執行於運行時,由應用程式來執行。在早期的電腦系統中,連結是手動執行的。在現代系統中,連結是由叫連結器的自動執行的。
- 理解連結器將協助構造大型程式
- 理解連結器將協助避免一些危險的編程錯誤
- 理解連結器將協助語言的範圍規則是如何?的
- 理解連結器將協助其他重要的系統概念
- 理解連結器便於利用共用庫
一、編譯器驅動程式
大多數編譯系統提供編譯驅動程式,它代表使用者在需要時調用語言前置處理器、編譯器、彙編器和連結器。
gcc -O2 -g -o p main.c swap.c
以上代碼將樣本程式從ASCII碼源檔案翻譯成可執行目標檔案,具體行為如:
二、靜態連結
像Unix ld程式這樣的靜態連結器以一組可重定位目標檔案和命令列參數作為輸入,產生一個完全連結的可以載入和啟動並執行可執行目標檔案作為輸出。輸入的可重定位目標檔案由各種不同的代碼和資料節組成。指令在一個節中,初始化的全域變數在另一個節中,而未初始化的變數又在另外一個節中。
1. 為了構造可執行檔,連結器必須完成兩個主要任務:
- 符號解析:目標檔案定義和引用符號。符號解析的目的是將每個符號引用剛好和一個符號定義聯絡起來
- 重定位:編譯器和彙編器產生從地址0開始的餓代碼和資料節。連結器通過把每個符號定義與一個儲存空間位置聯絡起來,然後修改所有對這些符號的引用,使得它們指向這個儲存空間位置,從而重定位這些節
2. 連結器的一些基本事實:目標檔案純粹是位元組塊的集合。這些塊中,有些包含程式碼,有些則包含程式資料,而其他的則包含指導連結器和載入器的資料結構。連結器將這些塊串連起來,確定被串連塊的運行時位置,並且修改代碼和資料區塊中的各種位置。連結器和彙編器已經完成了大部分工作。
三、目標檔案
1. 目標檔案的三種形式:
- 可重定位目標檔案。包含二進位代碼和資料,其形式可以在編譯時間與其他可重定位目標檔案合并起來,建立一個可執行目標檔案
- 可執行目標檔案:包含二進位代碼和資料,其形式可以被直接拷貝到儲存空間並執行
- 共用目標檔案:一種特殊類型的可重定位目標檔案,可以在載入或運行時被動態地載入到儲存空間並連結
2. 編譯器和彙編器產生可重定位目標檔案(包括共用目標檔案)。連結器產生可執行目標檔案。從技術上來說,一個目標模組就是一個位元組序列,而一個目標檔案就是一個存放在磁碟檔案中的目標模組。
四、可重定位目標檔案
1. 一個典型的ELF可重定位目標檔案的格式P451。ELF頭(ELF header)以一個16位元組的序列開始,這個序列描述了產生該檔案的系統的字的大小和位元組順序。ELF頭剩下的部分包含協助連結器文法分析和解釋目標檔案的資訊。其中包括ELF頭的大小、目標檔案的類型(如可重定位、可執行或是共用的)、機器類型(如IA32)、節頭部表的檔案位移,以及節頭部表中的條目大小和數量。不同的節的位置和大小是由節頭部表描述的,其中目標檔案中每個節都有一個固定大小的條目。
2. 夾在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節中的符號表,以及節頭部中的節名字。
五、符號和符號表
1. 每個可重定位目標模組m都有一個符號表,它包含m所定義和引用的符號的資訊。
2. 連結器上下文中三種不同的符號:
- 由m定義並能被其他模組引用的全域符號。全域連結器符號對應於非靜態C函數以及被定義為不帶C static屬性的全域變數
- 由其他模組定義並被模組m引用的全域符號。這些符號稱為外部符號,對應於定義在其它函數中的C函數和變數
- 只被模組m定義和引用的本地符號。有的本地連結器符號對應於帶static屬性的C函數和全域變數。這些符號在模組m中隨處可見,但是不能被其他模組引用。目標檔案中對應於模組m的節和相應的源檔案的名字也能獲得本地符號
3. 定義為帶有C static屬性的本地過程變數不在棧中管理。編譯器在.data和.bss中為每個定義分配空間,並在符號表中建立一個唯一有名字的本地連結器符號。
4. 利用static屬性隱藏變數和函數名字。任何聲明帶有static屬性的全域變數或函數都是模組私人的。
- name是字串表中的位元組位移,指向符號的以null結尾的字串名字
- value是符號的地址。對於可重定位的模組而言,value是距定義目標的節的起始位置的位移。對於可執行目標檔案而言,該值是一個絕對運行時地址
- size是目標的大小,以位元組為單位
- type是資料或函數
- binding欄位表示符號是本地的還是全域的
- section欄位表示一個到節頭部表的索引
六、符號解析6.1 連結器如何解析多重定義的全域符號
1. 在編譯時間,編譯器向彙編器輸出每個全域符號,或者是強或者是弱,而彙編器把這個資訊隱含地編碼在可重定位目標檔案的符號表裡。函數和已初始化的全域變數時強符號,未初始化的全域變數是弱符號。
2. 根據強弱符號的定義,Unix連結器使用下面的規則來處理多重定義的符號:
- 規則1:不允許有多個強符號
- 規則2:如果有一個強符號和多個弱符號,那麼選擇強符號
- 規則3:如果有多個弱符號,那麼從這些弱符號中任意選擇一個
6.2 與靜態庫連結
1. 使用標準C庫和數學庫中函數的程式可以用形式如下的命令列來編譯和連結:
gcc main.c /usr/lib/libm.a /usr/lib/libc.a
2. 在Unix系統中,靜態庫以一種稱為存檔的特殊檔案格式村凡在磁碟中。封存檔案是一組串連起來的可重定位目標檔案的集合,有一個頭部用來描述每個成員目標檔案的大小和位置。封存檔案名由尾碼.a標識。
- 與靜態庫連結的過程
6.3 連結器如何使用靜態庫來解析引用
1. 在符號解析的階段,連結器從左至右按照它們在編譯器驅動程式命令列上出現的相同順序來掃描可重定位目標檔案和封存檔案。在這次掃描中,連結器維持一個可重定位目標檔案的集合E(這個集合中的檔案會被合并起來形成可執行檔),一個未解析的符號(即引用了但是尚未定義的符號)集合U,以及一個在前面輸入檔案中已定義的符號集合D。初始時,E、U和D都是空的。
對於命令列上的每個輸入檔案f,連結器會判斷f是一個目標檔案還是一個封存檔案。如果f是一個目標檔案,那麼連結器把f添加到E, 修改U和D來反映f中的符號定義和引用,並繼續下一個輸入檔案
如果f是一個封存檔案,那麼連結器就嘗試匹配U中未解析的符號和由封存檔案成員定義的符號。如果某個封存檔案成員m,定義了一個符號來解析U中的一個引用,那麼就將m加到E中,並且連結器修改U和D來反映m中的符號定義和引用。對封存檔案中所有的成員目標檔案都反覆進行這個過程,直到U和D都不再發生變化。在此時,任何不包含在E中的目標檔案都簡單地被丟棄,而連結器將繼續處理下一個輸入檔案
如果當連結器完成對命令列上輸入檔案的掃描後,U是非空的,那麼連結器就好輸出一個錯誤並終止。否則,它會合并和重定位E中的目標檔案,從而構建輸出的可執行檔
2. 這種演算法會導致一些令人困擾的連結時錯誤,因為命令列上的庫和目標檔案的順序非常重要。在命令列中,如果定義一個符號的庫出現在引用這個符號的目標檔案之前,那麼引用就不能被解析,連結會失敗。關於庫的一般準則是將它們放在命令列的結尾。
3. 另一方面,如果庫不是相互獨立的,那麼它們必須排序,使得對於每個被封存檔案的成員外部參考的符號s,在命令列中至少有一個s的定義實在對s的引用之後的。
4. 如果需要滿足依賴需求,可以在命令列上重複庫。
七、重定位
1. 一旦連結器完成了符號解析這一步,它就是把代碼中的每個符號引用和確定的一個符號定義(即它的一個輸入目標模組中的一個符號表條目)聯絡起來。在此時,連結器就知道它的輸入目標模組中的代碼節和資料節的確切大小。現在就可以開始重定位了,在這個步驟中,將合并輸入模組,並為每個符號分配運行時地址。重定位由兩步組成:
重定位節和符號定義。在這一步中,連結器將所有相同類型的節合并為同一類型的新的彙總節。然後,連結器將運行時儲存空間地址賦給新的彙總節,賦給輸入模組定義的每個節,以及賦給輸入模組定義的每個符號。當這一步完成時,程式中的每個指令和全域變數都有唯一的運行時儲存空間地址。
重定位節中的符號引用。在這一步中,連結器修改代碼節和資料節中對每個符號的引用,使得它們指向正確的運行時地址。為了執行這一步,連結器依賴於稱為重定位條目的可重定位目標模組中的資料結構
7.1 重定位條目
1. 當彙編器產生一個目標模組時,它並不知道資料和代碼最終存放在儲存空間中的什麼位置。它也不知道這個模組引用的任何外部定義的函數或者全域變數的位置。所以,無論何時彙編器遇到對最終位置位置的目標引用,它就會產生一個重定位條目,告訴連結器在將目標檔案合并成可執行檔時如何修改這個引用。代碼的重定位條目放在.rel.text中。 已初始化的資料的重定位條目放在.rel.data中。
2. ELF定義了11種不同的重定位類型。其中兩種最基本的重定位類型:
- R_ 386_PC32 重定位一個使用32位PC相對位址的引用。
- R_ 386_32 重定位一個使用32位絕對位址的引用。
7.2 重定位器號引用
連結器修改代碼節和資料節中對每個符號的引用,使得他們指向正確的運行時地址。
- 重定位PC相對參照
- 重定位絕對引用
八、可執行目標檔案
- 可執行目標檔案的格式類似於可重定位目標檔案的格式。ELF頭部描述檔案的總體格式。它還包括程式的進入點,也就是當程式運行時要執行的第一條指令的地址。.text 、.rodata和.data 節和可重定位目標檔案中的節是相似的,除了這些節已經被重定位到它們最終的運行時儲存空間地址以外。.init節定義了一個小函數,叫做_init,程式的初始化代碼會調用它。因為可執行檔是完全連結的(已被重定位了),所以它不再需要.rel節。
- ELF可執行檔被設計得很容易載入到儲存空間,可執行檔的連續的片被映射到連續的儲存空間段。段頭部表描述了這種映射關係。
九、載入可執行目標檔案
1. 要運行可執行目標檔案p,可以在Unix外殼的命令列中輸入它的名字:
unix> ./p
因為p不是一個內建的外殼命令,所以外殼會認為p是一個可執行目標檔案,通過調用某個駐留在儲存空間中的稱為載入器(loader)的作業系統代碼來運行它。任何Unix程式都可以通過調用execve函數來調用載入器。載入器將可執行目標檔案中的代碼和資料從磁碟拷貝到儲存空間中,然後通過跳轉到程式的第一條指令或進入點來運行該程式。這個將程式拷貝到儲存空間並啟動並執行過程叫做載入。
2. 每個Unix程式都有一個運行時儲存空間映像。例如:在32位Linux系統中,程式碼片段總是從地址(0x8048000)處開始。資料區段是在接下來的下一個4KB對齊的地址處。運行時堆在讀/寫段之後接下來的第一個4KB對齊的地址處,並童工調用malloc庫往上增長。還有一個段是為共用庫保留的。使用者棧總是從最大的合法使用者地址開始,向下增長的(向低儲存空間地方向增長)。從棧的上部開始的段是為作業系統駐留儲存空間的部分(也就是核心)的代碼和資料保留的。
3. 在可執行檔中段頭部表的指導下,載入器將可執行檔的相關內容拷貝到代碼和資料區段。接下來,載入器跳轉到程式的進入點,也就是符號_ start的地址。在_ start地址處的啟動代碼是在目標檔案ctrl.o中定義的,對所有的C程式都是一樣的。在從.text和.init節中調用了初始化常式後,啟動代碼調用atexti常式,這個程式附加了一系列在應用程式正常中止時應該調用的程式。exit函數運行atexit註冊的函數,然後通過調用_ exit將控制返回給作業系統。接著,啟動代碼調用應用程式的main程式,它會開始執行我們的C代碼。在應用程式返回之後,啟動代碼調用_ exit程式,它將控制返回給作業系統。
十、動態連結共用庫
1. 概念:共用庫是致力與解決靜態庫缺陷的一個現代創新產物。共用庫是一個目標模組,在運行時,可以載入到任意的儲存空間地址,並加一個在儲存空間中的程式連結起來。這個過程稱為動態連結,是由一個叫做動態連結器的程式來執行的。共用庫也稱為共用目標,在Unix系統中通常用.so尾碼來表示。
調用編譯器構造向量運算樣本程式的共用庫libvector.so:gcc -shared -fPIC -o libvector.so addvec.c multvec.c將庫連結到程式中,建立一個可執行目標檔案p2:gcc -o p2 main.c /libvector.so
2. 動態連結器通過執行下面的重定位完成連結任務:
- 重定位libc.so的文本和資料到某個儲存空間段
- 重定位libvector.so的文本和資料到另一個儲存空間段
- 重定位p2中所有對libc.so和libvector.so定義的符號的引用
- 最後動態連結器將控制傳遞給應用程式,此時共用庫的位置已固定,並且在程式執行的過程中不會改變
十一、從應用程式中載入和連結共用庫
思路:將產生動態內容的每個函數打包在共用庫中。
十二、與位置無關的代碼
概念:編譯庫代碼,使不需要連結器修改庫代碼就可以在任何地址載入和執行這些代碼。這樣的代碼叫做與位置無關的代碼(PIC)。
- PIC資料引用
- PIC函數調用
十三、處理目標檔案的工具
《深入理解電腦系統》第七章學習筆記(初稿)