【學習時間:1小時30分 撰寫博客時間:2小時20分鐘】算法
連接是將各類代碼和數據部分收集起來並組合成爲一個單一文件的過程,這個文件可被加載(或拷貝)到存儲器並執行。連接能夠執行於編譯時,也就是在源代碼被翻譯成機器代碼時;也能夠執行於加載時,也就是在程序被加載器加載到存儲器並執行時;甚至執行於運行時,由應用程序來執行。在早期的計算機系統中,連接是手動執行的。在現代系統中,連接是由叫連接器的自動執行的。編程
大多數編譯系統提供編譯驅動程序,它表明用戶在須要時調用語言預處理器、編譯器、彙編器和連接器。數據結構
gcc -O2 -g -o p main.c swap.c
以上代碼將示例程序從ASCII碼源文件翻譯成可執行目標文件,具體行爲以下圖:函數
像Unix ld程序這樣的靜態連接器以一組可重定位目標文件和命令行參數做爲輸入,生成一個徹底連接的能夠加載和運行的可執行目標文件做爲輸出。輸入的可重定位目標文件由各類不一樣的代碼和數據節組成。指令在一個節中,初始化的全局變量在另外一個節中,而未初始化的變量又在另一個節中。工具
1. 爲了構造可執行文件,連接器必須完成兩個主要任務:學習
2. 連接器的一些基本事實:目標文件純粹是字節塊的集合。這些塊中,有些包含程序代碼,有些則包含程序數據,而其餘的則包含指導連接器和加載器的數據結構。連接器將這些塊鏈接起來,肯定被鏈接塊的運行時位置,而且修改代碼和數據塊中的各類位置。連接器和彙編器已經完成了大部分工做。編碼
1. 目標文件的三種形式:spa
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. 連接器上下文中三種不一樣的符號:
3. 定義爲帶有C static屬性的本地過程變量不在棧中管理。編譯器在.data和.bss中爲每一個定義分配空間,並在符號表中建立一個惟一有名字的本地連接器符號。
4. 利用static屬性隱藏變量和函數名字。任何聲明帶有static屬性的全局變量或函數都是模塊私有的。
1. 在編譯時,編譯器向彙編器輸出每一個全局符號,或者是強或者是弱,而彙編器把這個信息隱含地編碼在可重定位目標文件的符號表裏。函數和已初始化的全局變量時強符號,未初始化的全局變量是弱符號。
2. 根據強弱符號的定義,Unix連接器使用下面的規則來處理多重定義的符號:
1. 使用標準C庫和數學庫中函數的程序能夠用形式以下的命令行來編譯和連接:
gcc main.c /usr/lib/libm.a /usr/lib/libc.a
2. 在Unix系統中,靜態庫以一種稱爲存檔的特殊文件格式村凡在磁盤中。存檔文件是一組鏈接起來的可重定位目標文件的集合,有一個頭部用來描述每一個成員目標文件的大小和位置。存檔文件名由後綴.a標識。
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. 一旦連接器完成了符號解析這一步,它就是把代碼中的每一個符號引用和肯定的一個符號定義(即它的一個輸入目標模塊中的一個符號表條目)聯繫起來。在此時,連接器就知道它的輸入目標模塊中的代碼節和數據節的確切大小。如今就能夠開始重定位了,在這個步驟中,將合併輸入模塊,併爲每一個符號分配運行時地址。重定位由兩步組成:
重定位節和符號定義。在這一步中,連接器將全部相同類型的節合併爲同一類型的新的聚合節。而後,連接器將運行時存儲器地址賦給新的聚合節,賦給輸入模塊定義的每一個節,以及賦給輸入模塊定義的每一個符號。當這一步完成時,程序中的每一個指令和全局變量都有惟一的運行時存儲器地址。
重定位節中的符號引用。在這一步中,連接器修改代碼節和數據節中對每一個符號的引用,使得它們指向正確的運行時地址。爲了執行這一步,連接器依賴於稱爲重定位條目的可重定位目標模塊中的數據結構
1. 當彙編器生成一個目標模塊時,它並不知道數據和代碼最終存放在存儲器中的什麼位置。它也不知道這個模塊引用的任何外部定義的函數或者全局變量的位置。因此,不管什麼時候彙編器遇到對最終位置位置的目標引用,它就會生成一個重定位條目,告訴連接器在將目標文件合併成可執行文件時如何修改這個引用。代碼的重定位條目放在.rel.text中。 已初始化的數據的重定位條目放在.rel.data中。
2. ELF定義了11種不一樣的重定位類型。其中兩種最基本的重定位類型:
連接器修改代碼節和數據節中對每一個符號的引用,使得他們指向正確的運行時地址。
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. 動態連接器經過執行下面的重定位完成連接任務:
思路:將生成動態內容的每一個函數打包在共享庫中。
概念:編譯庫代碼,使不須要連接器修改庫代碼就能夠在任何地址加載和執行這些代碼。這樣的代碼叫作與位置無關的代碼(PIC)。
在Unix系統中有大量可用的工具能夠幫助你理解和處理目標文件。
Unix系統爲操做共享庫還提供了LDD程序:
本章主要講述了連接的意義、靜態連接和動態連接、目標文件、符號和符號表等內容。連接能夠在編譯時由靜態編譯器來完成,也能夠在加載或運行時由動態連接器完成。連接器處理被稱爲目標文件的二進制文件,它有三種不一樣形式:可重定位的、可執行的、共享的。連接器有兩個主要任務:符號解析和重定位,符號解析將目標文件中的每一個全局符號都綁定到一個惟一的定義,重定位肯定每一個符號的最終存儲器地址並修改對那些目標的引用。靜態連接器將多個可重定位目標文件合併成一個單獨的可執行文件。而動態連接器經過加載共享庫和重定位程序中的引用來完成連接。