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

2022-05-05 14:48:06 字數 4712 閱讀 5235

前言:鏈結是將各種**和資料部分收集起來並組合成為乙個單一檔案的過程,這個檔案可被載入(或拷貝)到儲存器並執行。鏈結可以執行於編譯時,也就是在源**被翻譯成機器**時;也可以執行於載入時,也就是在程式被載入器載入到儲存器並執行時;甚至執行於執行時,由應用程式來執行。在早期的計算機系統中,鏈結是手動執行的。在現代系統中,鏈結是由叫鏈結器的自動執行的。
大多數編譯系統提供編譯驅動程式,它代表使用者在需要時呼叫語言預處理器、編譯器、彙編器和鏈結器。

1.像unix ld程式這樣的靜態鏈結器以一組可重定位目標檔案和命令列引數作為輸入,生成乙個完全鏈結的可以載入和執行的可執行目標檔案作為輸出。輸入的可重定位目標檔案由各種不同的**和資料節組成。指令在乙個節中,初始化的全域性變數在另乙個節中,而未初始化的變數又在另外乙個節中。

3.鏈結器的一些基本事實:目標檔案純粹是位元組塊的集合。這些塊中,有些包含程式**,有些則包含程式資料,而其他的則包含指導鏈結器和載入器的資料結構。鏈結器將這些塊連線起來,確定被連線塊的執行時位置,並且修改**和資料塊中的各種位置。鏈結器和彙編器已經完成了大部分工作。

編譯器和彙編器生成可重定位目標檔案(包括共享目標檔案)。鏈結器生成可執行目標檔案。從技術上來說,乙個目標模組就是乙個位元組序列,而乙個目標檔案就是乙個存放在磁碟檔案中的目標模組。

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節中的符號表,以及節頭部中的節名字。

7.6.1 鏈結器如何解析多重定義的全域性符號     

1.在編譯是,編譯器向彙編器輸出每個全域性符號,或者是強或者是弱,而彙編器把這個資訊隱含地編碼在可重定位目標檔案的符號表裡。函式和已初始化的全域性變數時強符號,未初始化的全域性變數是弱符號。

規則1:不允許有多個強符號。

規則2:如果有乙個強符號和多個弱符號,那麼選擇強符號。

規則3:如果有多個弱符號,那麼從這些弱符號中任意選擇乙個。

7.6.2 與靜態庫鏈結

在unix系統中,靜態庫以一種稱為存檔的特殊檔案格式村凡在磁碟中。存檔檔案是一組連線起來的可重定位目標檔案的集合,有乙個頭部用來描述每個成員目標檔案的大小和位置。存檔檔名由字尾.a標識。

7.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的引用之後的。如果需要滿足依賴需求,可以在命令列上重複庫。

一旦鏈結器完成了符號解析這一步,它就是把**中的每個符號引用和確定的乙個符號定義(即它的乙個輸入目標模組中的乙個符號表條目)聯絡起來。在此時,鏈結器就知道它的輸入目標模組中的**節和資料節的確切大小。現在就可以開始重定位了,在這個步驟中,將合併輸入模組,並為每個符號分配執行時位址。重定位有兩步組成:

*重定位節和符號定義。在這一步中,鏈結器將所有相同型別的節合併為同一型別的新的聚合節。然後,鏈結器將執行時儲存器位址賦給新的聚合節,賦給輸入模組定義的每個節,以及賦給輸入模組定義的每個符號。當這一步完成時,程式中的每個指令和全域性變數都有唯一的執行時儲存器位址了。

*重定位節中的符號引用。在這一步中,鏈結器修改**節和資料節中對每個符號的引用,使得它們指向正確的執行時位址。為了執行這一步,鏈結器依賴於稱為重定位條目的可重定位目標模組中的資料結構。

7.7.1 重定位條目

1.當彙編器生成乙個目標模組時,它並不知道資料和**最終存放在儲存器中的什麼位置。它也不知道這個模組引用的任何外部定義的函式或者全域性變數的位置。所以,無論何時彙編器遇到對最終位置位置的目標引用,它就會生成乙個重定位條目,告訴鏈結器在將目標檔案合併成可執行檔案時如何修改這個引用。**的重定位條目放在.rel.text中。  已初始化的資料的重定位條目放在.rel.data中。

2.elf定義了11種不同的重定位型別。我們只關心其中兩種最基本的重定位型別:

*r_386_pc32  重定位乙個使用32位pc相對位址的引用。

*r_386_32 重定位乙個使用32位絕對位址的引用。

7.7.2 重定位符號引用 p4621.可執行目標檔案的格式類似於可重定位目標檔案的格式。elf頭部描述檔案的總體格式。它還包括程式的入口點,也就是當程式執行時要執行的第一條指令的位址。.text 、.rodata和.data 節和可重定位目標檔案中的節是相似的,除了這些節已經被重定位到它們最終的執行時儲存器位址以外。.init節定義了乙個小函式,叫做_init,程式的初始化**會呼叫它。因為可執行檔案是完全鏈結的(已被重定位了),所以它不再需要.rel節。

2.elf可執行檔案被設計得很容易載入到儲存器,可執行檔案的連續的片被對映到連續的儲存器段。段頭部表描述了這種對映關係。

1.要執行可執行目標檔案p,可以在unix外殼的命令列中輸入它的名字:

unix> ./p

因為p不是乙個內建的外殼命令,所以外殼會認為p是乙個可執行目標檔案,通過呼叫某個駐留在儲存器中的稱為載入器(loader)的作業系統**來執行它。任何unix程式都可以通過呼叫execve函式來呼叫載入器。載入器將可執行目標檔案中的**和資料從磁碟拷貝到儲存器中,然後通過跳轉到程式的第一條指令或入口點來執行該程式。這個將程式拷貝到儲存器並執行的過程叫做載入。
2.每個unix程式都有乙個執行時儲存器映像。例如:在32位linux系統中,**段總是從位址(0x8048000)處開始。資料段是在接下來的下乙個4kb對齊的位址處。執行時堆在讀/寫段之後接下來的第乙個4kb對齊的位址處,並童工呼叫malloc庫往上增長。還有乙個段是為共享庫保留的。使用者棧總是從最大的合法使用者位址開始,向下增長的(向低儲存器地方向增長)。從棧的上部開始的段是為作業系統駐留儲存器的部分(也就是核心)的**和資料保留的。

共享庫是致力於解決靜態庫缺陷的乙個現代創新產物。共享庫是乙個目標模組,在執行時,可以載入到任意的儲存器位址,並加乙個在儲存器中的程式鏈結起來。這個過程稱為動態鏈結,是由乙個叫做動態鏈結器的程式來執行的。共享庫也稱為共享目標,在unix系統中通常用.so字尾來表示。

深入理解計算機系統 第七章 鏈結

這一章重讀讀了比較久。按照書裡參考文獻說明這一欄的說明,鏈結處在編譯器 計算機體系結構和作業系統的交叉點上,要求理解 生成 機器語言程式設計 程式例項化和虛擬儲存器。這一章和上一章的風格相去甚遠,上一章給我留下的就是不斷的計算 更優的計算,這一章記憶為主。現在重讀,感覺有些工具是可以記錄一下的。這一...

深入理解計算機第七章

在第七章鏈結中,鏈結可以在編譯時由經他編譯器完成,也可以在載入時和執行時由動態鏈結器來完成。鏈結器處理可以為目標檔案的二進位制檔案,它有三種不同的形式 可重定位和可執行和共享的。靜態鏈結是由像gcc這樣的編譯驅動器呼叫的。多個目標檔案可以被絨對映到儲存器中,並執行這個程式。載入器將可執行檔案的內容對...

第七周讀書筆記 深入理解計算機系統

課程已經來到了第七周,經過了兩次大專案。在這兩次個人 結對專案裡,我深刻的感受到了自己在軟體工程 架構 對計算機系統的了解上所存在的不足。這一周我看的書是深入理解計算機系統,這本書從某種意義上來說,也可以說是計算機體系結構的教材之一,之所以選擇這本書,不僅是因為我對這個方面的很多知識很感興趣,而且也...