《深刻理解計算機系統》 第六週讀書筆記 第七章 連接

 《深刻理解計算機系統》第七章 連接

  • 連接是將各類代碼和數據部分收集起來並組合成爲一個單一文件的過程,這個文件可被加載(或拷貝)到存儲器並執行。
  • 連接能夠執行於編譯時,也就是在源代碼被翻譯成機器代碼時;也能夠執行於加載時,也就是在程序被加載器加載到存儲器並執行時;甚至執行於運行時,由應用程序來執行。
  • 在早期的計算機系統中,連接是手動執行的。在現代系統中,連接是由叫連接器的自動執行的。

7.1 編譯器驅動程序

       大多數編譯系統提供編譯驅動程序,它表明用戶在須要時調用語言預處理器、編譯器、彙編器和連接器。算法

7.2 靜態連接

       像Unix ld程序這樣的靜態連接器以一組可重定位目標文件和命令行參數做爲輸入,生成一個徹底連接的能夠加載和運行的可執行目標文件做爲輸出。輸入的可重定位目標文件由各類不一樣的代碼和數據節組成。指令在一個節中,初始化的全局變量在另外一個節中,而未初始化的變量又在另一個節中。服務器

       爲了構造可執行文件,連接器必須完成兩個主要任務數據結構

  • 符號解析 目標文件定義和引用符號。符號解析的目的是將每一個符號引用恰好和一個符號定義聯繫起來。
  • 重定位     編譯器和彙編器生成從地址0開始的餓代碼和數據節。連接器經過把每一個符號定義與一個存儲器位置聯繫起來,而後修改全部對這些符號的引用,使得它們指向這個存儲器位置,從而重定位這些節。

       連接器的一些基本事實:目標文件純粹是字節塊的集合。這些塊中,有些包含程序代碼,有些則包含程序數據,而其餘的則包含指導連接器和加載器的數據結構。連接器將這些塊鏈接起來,肯定被鏈接塊的運行時位置,而且修改代碼和數據塊中的各類位置。連接器和彙編器已經完成了大部分工做。函數

  • 目標文件純粹是字節快的集合。這些塊中,有些包含程序代碼,有些則包含程序數據,而其餘的則包括指導連接器和加載器的數據結構。連接器將這些塊連接起來,肯定被鏈接塊的運行時位置,而且修改代碼和數據塊中的各類位置。連接器對目標機器瞭解甚少。產生目標文件的編譯器和彙編器已經完成了大部分工做。

7.3 目標文件

  • 編譯器和彙編器生成可重定位目標文件(包括共享目標文件)。連接器生成可執行目標文件。從技術上來講,一個目標模塊就是一個字節序列,而一個目標文件就是一個存放在磁盤文件中的目標模塊。
  • 編譯器和彙編器生成可重定義目標文件(包括共享目標文件)。連接器生成可執行目標文件。
  • 各個系統之間,目標文件格式都不相同。

7.4 可重定位目標文件

      一個典型的ELF可重定位目標文件的格式P451。ELF頭(ELF header)以一個16字節的序列開始,這個序列描述了生成該文件的系統的字的大小和字節順序。ELF頭剩下的部分包含幫助連接器語法分析和解釋目標文件的信息。其中包括ELF頭的大小、目標文件的類型(如可重定位、可執行或是共享的)、機器類型(如IA32)、節頭部表的文件偏移,以及節頭部表中的條目大小和數量。不一樣的節的位置和大小是由節頭部表描述的,其中目標文件中每一個節都有一個固定大小的條目。工具

      夾在ELF頭和節頭部表之間的都是借。一個典型的ELF可重定位目標文件包含下面幾個節:post

  • .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引用的全局符號
  • 只被模塊m引用的本地符號

7.6 符號解析

7.6.1 連接器如何解析多重定義的全局符號     

       在編譯是,編譯器向彙編器輸出每一個全局符號,或者是強或者是弱,而彙編器把這個信息隱含地編碼在可重定位目標文件的符號表裏。函數和已初始化的全局變量時強符號,未初始化的全局變量是弱符號。編碼

       根據強弱符號的定義,Unix連接器使用下面的規則來處理多重定義的符號:spa

  • 規則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 加載可執行目標文件

       每一個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 從應用程序中加載和連接共享庫

動態連接在現實中的例子:

  • 分發軟件
  • 構建高性能Web服務器

7.12 與位置無關的代碼(PIC)

PIC數據引用

PIC函數調用

7.13 處理目標文件的工具

  • AR:建立靜態庫,插入、刪除、列出和提取成員。
  • STRINGS:列出一個目標文件中全部可打印的字符串。
  • STRIP:從目標文件中刪除符號表信息。
  • NM:列出一個目標文件中符號表定義的符號。
  • SIZE:列出目標文件中節的名字和大小。
  • READELF:可以顯示一個目標文件的全部信息。
  • OBJDUMP:反彙編
  • LDD:列出一個可執行文件運行時須要的共享庫。

7.14 小結

      連接能夠在編譯時由靜態編譯器來完成,也能夠在加載時和運行時由動態連接器來完成。連接器處理稱爲目標文件的二進制文件,它有三種不一樣的形式:可重定位的、可撕的和共享的:可重定位的目標文件由靜態連接器合併成一個可執行的目標文件,它能夠加載到存儲器中並執行。
      共享目標文件(共享庫)是在運行時由動態連接器連接和加載的,或者隱含地在調用程序被加載和開始執行時或者根據須要在程序調用dopen庫的函數時。連接器的兩個主要任務是符號解析和重定位,符號解析將目標文件中的每一個全局符號都綁定到一個惟一的定義,而重定位肯定每一個符號的最終存儲器地址,並修改對那些目標的引用。靜態連接器是由像G∝這樣的編譯驅動器調用的。它們將多個可重定位目標文件合併成個單獨的可執行目標文件。多個目標文件能夠定義相同的符號,而連接器用來悄悄地解析這些多重定義的規則可能在用戶程序中引入的微妙錯誤。 
相關文章
相關標籤/搜索