第七章連接css
連接(linking)是將各類代碼和數據部分收集起來並組合成爲一個單一文件的過程,這個文件可被加載(或被拷貝)到存儲並執行。
連接的時機程序員
連接器的關鍵角色:使分離編譯稱爲可能。swift
7.1 編譯器驅動程序函數
驅動程序的工做:一、運行C預處理器,將C源程序(.c)翻譯成一個ASCⅡ碼中間文件(.i);二、運行C編譯器,將.i文件翻譯成彙編文件(.s);三、運行彙編器,翻譯成可重定位目標文件(.o);四、運行連接器程序,將各個部分組合,建立可執行程序。ui
7.2靜態連接編碼
連接器的兩任務:一、符號解析:目標文件定義和引用符號。將每一個符號引用恰好和一;個符號定義聯繫起來;二、重定位:連接器經過把每一個符號定義與一個存儲器位置聯繫起來,而後修改全部對這些符號的引用。spa
基本事實:目標文件純粹是字節塊的集合,操作系統
7.3目標文件命令行
三種形式:翻譯
l 可重定位目標文件:包含二進制代碼和數據,可與其餘可重定位目標文件合併起來,建立可執行目標文件。
l 可執行目標文件: 包含二進制代碼和數據,其形式能夠被直接拷貝到存儲器並行。
l 共享目標文件:特殊類型的可重定位目標文件,能夠在加載或者運行時被動態的加載到存儲器並連接。
編譯器和彙編器生成可重定位目標文件,連接器生成可執行程序。
7.4可重定位目標文件
ELF可重定位目標文件格式
. text: 已編譯程序的機器代碼。
.rodata: 只讀數據,好比printf 語句中的格式串開關語句的跳轉表
.data: 已初始化的全局C 變量
.bss: 未初始化的全局C 變量。在目標文件中這個節不佔據實際的空間,它僅僅是一個佔位符。
.symtab: 一個符號哀,它存放在程序中定義和引用典型的ELF 可重定位目標文件的函數和全局變量的信息。
. rel . text :一個.text 節中位置的列表,當連接器把這個目標文件和其餘文件結合時,須要修改這些位置。
.rel.data: 被模塊引用或定義的任何全局變量的重定位信息。
.debug: 一個調試符號表,其條目是程序中定義的局部變量和類型定義, 程序中定義和引用的全局變量,以及原始的C 源文件。
.line: 原始C 源程序中的行號和.text 節中機器指令之間的映射。
.strtab: 一個字符串表,其內容包括.symtab 和.debug 節中的符號表,以及節頭部中的節名字。
7.5符號和符號表
每一個可重定位目標模塊m 都有一個符號表,它包含m 所定義和引用的符號的信息。在連接器的上下文中,有三種不一樣的符號:
·由m 定義並能被其餘模塊引用的全局符號。全局連接器符號對應於非靜態的C以及被定義爲不帶C static 屬性的全局變量。
·由其餘模塊定義並被模塊m 引用的全局符號。這些符號稱爲外部符號,對應於定義在其餘模塊中的C 函數和變量。
·只被模塊m 定義和引用的本地符號。有的本地連接器符號對應於帶static 屬性的C 函數和全局變量。這些符號在模塊m 中隨處可見,可是不能被其餘模塊引用。目標文件中對應於模塊m 的節和相應的源文件的名字也能得到本地符號。
name 是字符串表中的字節偏移,指向符號的以null 結尾的字符串名字.value 是符號的地址。對於可重定位的模塊來講, value 是距定義目標的節的起始位置的偏移。
每一個符號都和目標文件的某個節相關聯,由section 字段表示,該字段也是一個到節頭部表的索引.有三個特殊的僞節(pseudo section) ,它們在節頭部表中是沒有條目的: ABS 表明不應被重定位的符號; UNDEF 表明未定義的符號,也就是在本目標模塊中引用,可是卻在其餘地方定義的符號; COMMON 表示還未被分配位置的未初始化的數據目標.
7.6符號解析
連接器解析符號引用的方法是將每一個引用與它輸入的可重定位目標文件的符號表中的一個肯定的符號定義聯繫起來.編譯器只容許每一個模塊中每一個本地符號只有一個定義。編譯器還確保靜態本地變量,也會有本地連接器符號,擁有惟一的名字。
根據強弱符號的定義, Unix 連接器使用下面的規則來處理多重定義的符號:
·規則1 :不容許有多個強符號。
·規則2 :若是有一個強符號和多個弱符號,那麼選擇強符號。
·規則3 :若是有多個弱符號,那麼從這些弱符號中任意選擇一個。
全部的編譯系統都提供一種機制,將全部相關的目標模塊打包成爲一個單獨的文件,稱爲靜態庫(static ),它能夠用作連接器的輸入。將全部的標準C 畫數都放在一個單獨的可重定位目標模塊中(如libc中),應用程序員能夠把這個模塊連接到他們的可執行文件中:
unix> gcc main.c /usr/lib/libc.o
一個很大的缺點是系統中每一個可執行文件如今都包含着一份標準函數集合的徹底拷貝,這對磁盤空間是很大的浪費。咱們能夠經過爲每一個標準函數建立一個獨立的可重定位文件,把它們存放在一個爲你們都知道的目錄中來解決其中的一些問題.
unix> gcc main.c /usr/lib/printf.o /usr/lib/scanf.o ...
在Unix 系統中,靜態庫以一種稱爲存檔(archive) 的特殊文件格式存放在磁盤中。。存檔文件是一組鏈接起來的可重定位目標文件的集合,有一個頭部用來描述每一個成員目標文件的大小和位置。存檔文件名由後綴.a 標識。爲了建立這個可執行文件,咱們要編譯和連接輸入文件main.o
unix> gcc -02 -c main2 .c
unix> gcc -static -0 p2 main2 .o ./libvector.a
重定位由兩步組成:
·重定位節和符號定義。在這一步中,連接器將全部相同類型的節合併爲同一類型的新的聚合節。 ·重定位節中的符號引用。在這一步中,連接器修改代碼節和數據節中對每一個符號的引用,使得它們指向正確的運行時地址。
當彙編器生成一個目標模塊時,它並不知道數據和代碼最終將存放在存儲器中的什麼位置。它也不知道這個模塊引用的任何外部定義的函數或者全局變量的位置。代碼的重定位條目放在.rel.text 中。已初始化數據的重定位
條目放在.rel.data 中。
中兩種最基本的重定位類型:
R_386_PC32: 重定位一個使用32 位PC 相對地址的引用.一個PC 相對地址就是距程序計數器(PC) 的當前運行時值的偏移量。當CPU 執行一條使用PC 相對尋址的指令時,它就將在指令中編碼的32 位值上加上PC 的當前運行時值,獲得有效地址。 R_386_32: 重定位一個使用32 位絕對地址的引用。經過絕對尋址, CPU 直接使用在指令中編碼的32 位值做爲有效地址,不須要進一步修改。
執行目標文件的格式相似於可重定位目標文件的格式。ELF 頭部描述文件的整體格式。它還包括程序的入口點(Centry point) ,也就是當程序運行時要執行的第一條指令的地址。
.init 節定義了一個小函數,叫作init ,程序的初始化代碼會調用它。由於可執行文件是徹底連接的(已被重定位了),因此它再也不須要.rel。
要運行可執行目標文件p ,能夠在Unix 外殼的命令行中輸入它的名字:
unix> . /p
由於p 不是一個內置的外殼命令,因此外殼會認爲p 是一個可執行目標文件,經過調用某個駐留在存儲器中稱爲加載器(loader) 的操做系統代碼來運行它.加載器將可執行目標文件中的代碼和數據從磁盤拷貝到存儲器中,而後經過跳轉到程序的第一條指令或入口點來運行該程序。這個將程序拷貝到存儲器並運行的過程叫作加截(loading) 。
在32 位Linu系統中,代碼段老是從地址Ox08048000 處開始。數據段是在接下來的下一個4KB 對齊的地址處。運行時堆在讀/寫段以後接下來的第一個4KB 對齊的地址處,並經過調用malloc 庫往上增加。
共享庫是致力於解決靜態庫缺陷的一個現代創新產物。共享庫是一個目標模
塊,在運行時,能夠加載到任意的存儲器地址,並和一個在存儲器中的程序連接起來。這個過程稱爲動態連接(dynamic linking) ,是由一個叫作動態連接器(dynamic linker) 的程序來執行的。
共享庫也稱爲共享目標 ,在Unix 系統中一般用. s。後綴來表示。微軟的操做系統大量地利用了共享庫,它們稱爲DLL (動態連接庫)。
首先,在任何給定的文件系統中,對於一個庫只有一個.5。文件。全部引用該庫的可執行目標文件共享這個.5。文件中的代碼和數據,而不是像靜態庫的內容那樣被拷貝和嵌人到引用它們的可執行的文件中。其次,在存儲中,一個共享庫的.text 節的一個副本能夠被不一樣的正在運行的進程共享.
給連接器以下特殊指令:
unix> gcc -sbared -ÎPIC -0 libvector.so addvec.c multvec.c//-fPIC 選項指示編譯器生戚與位置無關的代碼 -5hared 選項指示連接器建立一個享的目標文件。 unix> gcc -0'p2 main2 .c ./libvector.so//這樣就建立了一個可執行目標文件p2 ,而此文件的形式使得它在運行時
而後,動態連接器經過執行下面的重定位完成連接任務:
.重定位libc.5 。的文本和數據到某個存儲器段。 ·重定位lib飛Tector.5 。的文本和數據到另外一個存儲器段。 ·重定位p2 中全部對由libc.5。和libvector.5。定義的符號的引用。
#include <dlfcn.h> void *dlopen(const char *filename , int flag); 返回若成功則爲指向句柄的指針,若出錯則爲NULL 。
dlopen 函數加載和連接共享庫filename 。用之前帶RTLD GLOBAL 選項打開的庫解析filename 中的外部符號。若是當前可執行文件是帶rdynamic 選項編譯的,那麼對符號解析而言,它的全局符號也是可用的。
#include <dlfcn.h> void *dlsym(void *handle , char *symbol); 返回若成功爲指向符號的指針,若出錯則爲NULL 。
dlsym 函數的輸入是一個指向前面已經打開共享庫的句柄和一個符號名字,若是該符號存在,就返回符號的地址,不然返回NULL 。
#include <dlfcn.h> int dlclose (void *handle); 返回:若成功爲0 ,若出錯則爲1.
若是沒有其餘共享庫正在使用這個共享庫, dlclose 函數就卸載該共享庫。
#include <dlfcn.h> const char *dlerror(void); 返回如採前面對dlopen 、dlsym 或dlclose 的調用失敗,則爲錯誤消息,若是前面的調用成功,則爲NULL.
dlerror 函數返回一個字符串,它描述的是調用dlopen 、dlsym 或者dlclose 函數時發生的最近的錯誤,若是沒有錯誤發生,就返回NULL 。
多個進程是如何共享程序的一個拷貝的呢?一種方法是給每一個共享庫分配一個事先預備的專用的地址空間片(chunk) ,而後要求加載器老是在這個地址加載共享庫。一種更好的方法是編譯庫代碼,使得不須要連接器修改庫代碼就能夠在任何地址加載和執行這些代碼。這樣的代碼叫作與位置無關的代碼.
不管咱們在存儲器中的何處加載一個目標模塊〈包括共享目標模塊),數據段老是被分配成緊隨在代碼段後面。爲了運用這個事實,編譯器在數據段開始的地方建立了一個表,叫作全局偏移量.