《深入理解計算機系統》第七章鏈接 讀書筆記

0、鏈接的基本概念

        鏈接是將各種代碼和數據部分收集起來組合成爲一個單一文件的過程,這個文件可被加載(拷貝)到存儲器並執行。鏈接可以執行於編譯時(傳統靜態鏈接),也就是在源代碼被翻譯成機器二進制代碼時,也可以執行與加載時(加載時的共享庫動態鏈接),也就是程序被加載器加載到存儲器並執行時,甚至執行於運行時(運行時的共享庫動態鏈接),由應用程序來執行。早起的計算機系統中,鏈接是手動執行的,在現代系統中鏈接是由叫做連接器的程序自動執行的。


1、編譯器驅動程序

1.0、編譯器驅動程序:編譯系統提供的調用預處理器、編譯器、彙編器和鏈接器來構造目標文件的程序。

過程如下圖:

調用GCC驅動程序的命令行:

>gcc -O2 -g -o p  <*.c filename>

分步調用翻譯器(cpp、ccl、as)的命令行:

>cpp   [options]    <*.c filename>   <*.i filename>

>ccl   <*.i filename>   <*.c filename>  -O2 [options] -o   <*.s filename>  

>as [options] -o  <*.o filename>  <*.s filename>

調用鏈接器的命令行:

>ld -o p [system object files and args]  <*.o filename>

運行可執行目標文件:

>./p


2、可重定位目標文件

2.0、ELF格式可重定位目標文件


       ELF頭:描述了生成改文件的系統的字的大小和字節順序、幫助鏈接器語法分析和解釋目標文件的指導信息(ELF頭大小、目標文件類型、機器類型、節頭部表的文件偏移以及節頭部表的條目大小和數量)。所謂“條目”可以理解爲某個格式的數據結構,可以比喻爲通常表格的表頭格式下的一條信息。

      節頭部表:描述各個節的位置和大小

      節:一些包含特定信息的條目的集合


2.1、節

2.1.0.symtab:符號表

1)記錄內容:關於符號的信息(是信息而不是變量本身)

A、由m定義並能被其他模塊引用的全局符號(程序中的定義)

B、由其他模塊定義並被模塊m引用的全局符號(程序中聲明而未定義)

C、只被模塊m定義和引用的本地符號(static函數和變量)

        B類符號在符號表中會有UND標記(ABS:不該被重定位的符號;COM:.bss符號,通過value值給出對齊要求),以期在鏈接時找到唯一定義;C類符號包括static的局部變量,編譯器會給它們唯一的名字。

2).格式:


3)示例:

2.1.1、.rel:重定位表

1)記錄內容:關於鏈接時如何修改重定位項的信息

        當彙編器生成一個目標模塊時,它不知道數據和代碼最終運行時會被放在存儲器的什麼地方,也不知道它引用的外部定義的函數和數據的位置,所以無論何時當彙編器遇到對這樣的位置未知對象的地址引用(直接或間接),就會產生重定位條目。

2)格式


3)示例


2.1.2、關於其他節

.text:已編譯機器代碼

.rodata:只讀數據,包括printf中的格式串、const變量、開關語句跳轉表等

.bss:不佔據實際空間、僅僅是個佔位符

.data:已初始化全局變量,已經爲它開了所需字節的空間並放入初始值(這裏指非重定向項)

.strtab:字符串表,各個符號的名字,每個名字後以NULL結束


3、鏈接的關鍵過程-------符號解析和重定位

3.0、符號解析:解析符號引用,就是保證所有被鏈接的.o文件中每個引用的符號有且僅有一個定義。這個過程由編譯器和鏈接器一起完成,在編譯過程中,當編譯器遇到一個不在當前模塊中定義的符號時,它會產生一個鏈接器符號表條目,交由鏈接器完成解析。

1)  在某個目標模塊內,編譯器要保證

A、不能重複定義符號,也就是每個非UND符號只能有一個(同名的static變量會被編譯器重命名使其名字唯一);

B、每個引用都必須指向符號表中的一個符號(可以是UND的),也就是說使用函數或變量前必須出現定義或者聲明;

2)在被鏈接的各個目標模塊間,鏈接器要保證

A、每個非UND符號的名字必須是唯一的(發現重定義時要按照強弱關係取捨,若無法取捨則會報錯 “重複定義……”)

B、每個UND符號都必須在其他模塊找到定義,也就是在其他模塊的符號表中找到相同名字而非UND的項(如果找不到定義,那麼會出現正常編譯而鏈接出錯“無法解析的……”的現象)


3.1、重定位:由下面兩步完成

A、重定位節和符號定義。在這一步中,鏈接器將所有同類型的節合併爲同意類型的新的聚合節,將運行時存儲器地址付給新的聚合節(賦給輸入模塊定義的每個節,賦給輸入模塊定義的每個符號),這一步完成時,程序中每個指令和全局變量就都有唯一的運行時存儲器地址了。

B、重定位節中的符號引用。在這一步中,鏈接器修改代碼節和數據節中對每個符號的引用(依賴於重定位表中條目的指導),使它們指向正確的運行時地址。

ELF定義了十一種重定位方式,其中最基本的有兩種:R_386_PC32(相對)和R_386_32(絕對)

B過程的算法如下(假定A過程已經完成):

註解:第1、2行表示在每個節s以及與每個節相關的重定位條目r上迭代地執行

            第8行可以改寫爲(unsigned)ADDR(r.symbol)-(refaddr-*refptr)       在32位機器上*refptr一般爲-4,(refaddr-*refptr)  就表示要修改處的下一條指令位置

            第13行的*refptr一般存着要訪問的數據結構內部的成員的偏移量


        經過鏈接,就將可重定位目標文件合成了可執行目標文件可以在存儲器中運行了,按照鏈接進行的階段不同,大方向上有兩種具體的實現方式——靜態鏈接和動態鏈接,接下來討論。

4、靜態鏈接 

4.0 一般可重定位目標文件的靜態鏈接:

>gcc <*.o filename>,對編譯器驅動程序命令行上出現的文件從左至右進行符號解析和重定位。

4.1、與靜態庫的鏈接

4.1.1、靜態庫概念的必要性:

       將相關目標模塊打包成爲一個單獨的文件,成爲靜態庫文件,Unix中靜態庫是以.a作爲後綴的存檔文件。靜態庫可以用作鏈接器的輸入,鏈接器構造一個可執行文件時,它只拷貝靜態庫裏被應用程序引用的目標模塊,再將目標模塊合成可執行目標文件。

      靜態庫鏈接,是取完全獨立使用各目標模塊和將所有目標模塊打包成一個單獨文件與主調程序鏈接的折中方法(分類合併),也是完全由編譯器識別所需目標模塊和完全人工識別所需目標模塊的折中方法(人工決定使用哪個庫,鏈接器找到使用的具體模塊再拷貝出來)。完全獨立使用各目標模塊,若由程序員負責識別和鏈接,他將不得不記住各函數所在模塊的名字,當這種函數很多時(比如說libc.a中提供的常用函數),將造成極大的不便;若由編譯器識別,編譯器的實現將會非常複雜,並且受到庫更新的影響。所有目標模塊打包成一個單獨文件,太大,將對內存資源造成不必要的浪費,而且因爲某個無關模塊的更新也不得不重新編譯整個文件。

       使用靜態庫是在程序員使用庫函數的便捷性和鏈接效率、資源利用率上儘量取平衡。

4.1.2、靜態庫的鏈接

       創建靜態庫:ar -rcs  <*.o filename>

       靜態庫鏈接的過程:

       

4.1.3、靜態庫符號解析過程

      在符號解析階段,鏈接器從左到右按照它們在編譯器驅動程序命令行上出現的相同順序來掃描可重定位目標文件與存檔文件(驅動程序自動將命令行中所有.c文件翻譯爲.o文件。)在這次掃描中,鏈接器維持着一個可重定位目標文件的集合E(這個集合中的文件會被合併起來形成可執行文件),一個未解析的符號(即引用了尚未定義的符號)集合U,以及一個在前面輸入文件中已經定義的符號集合D。初始時,E、D都是空的。

     A、對於命令行上的每個輸入文件f,鏈接器會判斷f是一個目標文件還是一個存檔文件。如果是前者,那麼鏈接器把f添加到E中,修改U和D,然後繼續下一個輸入文件。

     B、如果f是個存檔文件,那麼鏈接器就嘗試匹配U中未解析的符號和由存檔文件成員定義的符號。如果某個存檔文件成員m定義了一個符號來解析U中的一個引用,那麼就將m加到E中,並且鏈接器修改U和D。對存檔文件中所有的成員目標文件反覆進行這個過程,知道U和D都不在發生變化。此事,任何E之外的成員目標文件都簡單地被丟棄,鏈接器接着處理下一個輸入文件。

     按從左到右的順序處理輸入文件,也就是說,每個符號引用必須保證在它的右邊存在一個定義。一般講庫放在命令行靠右處,必要時可以重複庫或合併庫來滿足依賴要求。

4.2、靜態鏈接輸出的可執行目標文件

4.2.0、ELF格式可執行目標文件


        靜態鏈接(>gcc  <*.c filename>  <*.o filename>)在編譯過程中完成,輸出可執行目標文件。

        與可重定位目標文件結構相比,可執行目標文件多出了段頭部表和.init節,少了.rel.data和.rel.text節,ELF頭部還指出了程序的入口點,也就是當程序運行時要執行的第一條指令的地址。.init節定義了一個小函數叫做_init,程序的初始化代碼會調用它,因爲可執行文件已經被重定位,所以它不再需要.rel節。

        段頭部表描述了可執行文件連續的片與運行時存儲器空間的映射關係,也就是“可執行文件中偏移量爲……的……字節片拷貝到存儲器中自地址……起的……字節的空間中)

段頭部表的結構如下:



4.2.1、加載可執行目標文件

>./p

        通過調用某個主流在存儲器中稱爲加載器的操作系統代碼來運行這個可執行文件。加載器會按照段頭部表的描述將代碼和數據從磁盤拷貝到存儲器中,若是靜態鏈接,之後將跳轉到程序的第一條指令或入口點來運行該程序,動態鏈接不一樣。


5、動態鏈接共享庫

5.0、共享庫概念的必要性:

       靜態庫存在着缺陷:程序員必須實時地瞭解靜態庫的更新情況,然後將他們的程序重新與新的靜態庫鏈接;多個程序的可執行目標文件中可能含有相同庫文件的拷貝,這樣不必要的重複浪費了存儲器資源。共享庫解決了靜態庫的這些缺陷。

      共享庫是一個目標模塊,在unix中一般以.so作爲後綴。在運行時,共享庫可以加載到任意的存儲器地址,並和一個在存儲器中的程序鏈接起來。這個過程稱爲動態鏈接,是由一個叫動態鏈接器的程序來執行的。需要注意的是,由於庫是“共享”的,共享庫和程序的目標模塊載入存儲器的時間可能相差相當長,可能程序載入之時共享庫已經在存儲器中(被其他程序使用),也可能程序先載入,再由動態鏈接器載入共享庫,因而鏈接是在運行時(而非存儲器外的編譯時)進行的,是在存儲器中進行的。

5.1、動態鏈接共享庫的過程:


       符號解析在ld處完成,輸出部分鏈接的可執行文件p2,剩餘的鏈接工作由動態鏈接器完成。當p2被加載到存儲器中時,加載器不再將控制傳遞給應用,而是加載和運行動態鏈接器,動態鏈接器將完成下面的重定位任務:

A、將共享庫的文本和數據載入一個存儲器段(如果共享庫本不再存儲器中的話)。

B、重定位p2對共享庫符號的引用(這是問題的關鍵,記住現在p2已經在存儲器中了,.text是可讀可執行不可寫的,那麼怎樣重定位呢?見5.3)

5.2、與位置無關的代碼(PIC)

        爲了解決共享庫重定位的問題,一個簡單的想法是給每個共享庫專用的地址空間片。但這種方法的缺陷是對地址的使用效率不高,即使一個進程不適用這個庫,那部分空間還是會被分配出來;其次難於管理,當一個庫修改時,必須確認它的已分配片還適合它,否則就要重新安排專用地址空間片。

        一種更好的情況是通過某種機制使不需要鏈接器修改代碼就可以在任何地址加載和執行庫代碼,即位置無關代碼(PIC),模塊內的調用是PC相對的,本身即是PIC,難點在處理引用模塊外定義符號的代碼。

       可以使用GOT(全局偏移量表)來實現PIC,GOT是在.data部分的,在存儲器中可以被動態鏈接器修改,從而實現重定位。

       書上具體介紹了使用GOT來實現PIC的數據引用和符號調用的方式,在原書(第二版)P471,這裏就不拷上來了:)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章