概述
●該章節主要講解的是ELF檔案的結構。
●靜態庫的概念
●動態庫(又叫共用庫)的概念,一般用於作業系統,普通應用程式作用不大。
●程式的載入過程。
該書中對連結的解釋也不夠詳細。在章節最後,作者也承認:在電腦系統文獻中並沒有很好的記錄連結。因為連結是處在編譯器、電腦體繫結構和作業系統的交叉點上,他要求理解代碼產生、機器語言編程、程式執行個體化和虛擬儲存空間。它恰好不落在某個通常的電腦系統領域中。
該章節講述Linux的X86系統,使用標準的ELF目標檔案,無論是什麼樣的作業系統,細節可能不盡相同,但是概念是相同的。
讀完這一章節後,對“符號”的概念很是模糊。
7.1編譯驅動程式
這裡再說一下編譯系統。大多數編譯系統提供編譯驅動程式,它代表使用者在需要的時候調用語言預先處理、編譯器、彙編器、和連結器。我自己畫了一個結構圖。
7.2靜態連結
7.3目標檔案 目標檔案有三種:可重定位目標檔案、可執行目標檔案和共用目標檔案(即動態連結程式庫),個個系統上對目標檔案的叫法不一致,Unix叫a.out,Windows NT叫PE(Portable Executable)。現代Unix使用ELF格式(EXecutable and Linkable Format 即可執行和可連結格式)。 下面詳細介紹“可重定位目標檔案”,最左邊的一個圖。 說明了,一個目標檔案產生可執行檔,然後載入到記憶體後的映射等,三個步驟。 ELF頭描述了產生該檔案的系統的字的大小和位元組序。ELF和節頭部表之間每個部分都稱為一個節(section) .text:已編譯器的機器代碼 .rodada:唯讀資料,比如printf語句中的格式串。 .data:已經初始化的全域C變數。局部變數在運行時儲存在棧中。即不再data節也不在bss節 .bss:未初始化的全域C變數。不佔據實際的空間,僅僅是一個預留位置。所以未初始設定變數不需要佔據任何實際的磁碟空間。C++弱化BSS段。可能是沒有,也可能有。 .symtab:一個符號表,它存放“在程式中定義和引用的函數和全域變數的資訊”。 .rel.text:一個.text節中位置的列表。(將來重定位使用的) .rel.data:被模組引用或定義的任何全域變數的重定位資訊。 .debug:偵錯符號表,其內容是程式中定義的局部變數和類型定義。 .line:原始C來源程式的行號和.text節中機器指令之間的映射。 .strtab:一個字串表.
可定位目標檔案的結構:讓你深入瞭解程式段,資料區段,bss段,符號表等等。
7.4可重定位目標檔案——參考7.3
7.5符號和符號表
符號表是一個數組,數組裡存放一個結構體。
typedef struct { int name;/*String table offset*/ int value;/*Section offset, or VM address*/ int size;/*Object size in bytes*/ char type:4,/*Data, fund,section,or src file name (4 bits)*/ binding:4;/* Local of global(4bits)*/ char reserved;/*Unused*/ char section;/*Section header index ABS UNDEF*/}Elf_Symbol;
7.6符號解析
原則是:編譯器只允許每個模組中每個本地符號只有一個定義。而且對全域的符號的解析很棘手,因為多個目標檔案可能會定義相同的符號。C++和Java使用mangling手段來支援重載。
多重定義的全域符號,請看下面的程式:
/*foo.c*/ /*bar.c*/#include <stdio.h> int x;void f(void); void f()int x =15213; {int main() x = 15212;{ } f(); printf("x=%d\n",x); return 0;}
大家能猜到輸出的結果是15212;這是因為:bar.c中的x全域變數沒有初始化,導致函數f中使用的是foo檔案中的x變數。
根據Unix連接器使用下面的規則來處理多重定義的符號:
●規則1:不允許有多個強符號。
●規則2:如果有一個強符號和多個弱符號,那麼選擇強符號(這就是上面這道題的答案,初始化的int x=15213是強符號,而int x;是弱符號)
●規則3:如果有多個弱符號,那麼從這些弱符號中任意選擇一個(多麼可怕啊)
靜態庫
事先寫好的一些可重定位的目標檔案打包成一個單獨的檔案,它可以用作連接器的輸入。當連接器構造一個輸出的可執行檔時,它只拷貝靜態庫裡被應用程式引用的目標模組。(稍後講解動態連結程式庫,也稱之為共用庫)。
在Unix系統中,靜態庫以一種成為存檔(archive)的特殊檔案格式存放在磁碟中。封存檔案是一組串連起來的可重定位目標檔案的集合。有一個頭部用來描述每個成員目標檔案的大小和位置。封存檔案的尾碼是.a標識。是否可以這麼理解.a檔案的結構呢?(自己畫圖)
下面用展示一個靜態庫串連的過程:
7.7重定位
7.8可執行檔
參考7.3節圖中央部分。可執行檔跟可重定位目標檔案非常相似。只是可執行檔多了“init”和“段頭部表"少了,”.rel.text“和”.rel.data“兩個節。
7.9載入可執行檔。
從7.3節圖中可以發現,右部是載入後的程式結構。ELF目標檔案被設計的非常容易載入到儲存空間。需要注意的是Unix中,程式總的程式碼片段總是從0x0804800處開始(這就是虛擬儲存空間的作用)。資料區段是在接下來的下一個4KB對齊的地址處。運行時堆在"讀/寫段"之後接下來的第一個4KB對齊的地址處,並通過malloc庫往上增長。而棧總是往下生長。
7.10動態庫(共用庫)
動態庫是為瞭解決靜態庫的兩個弊端而出現的,靜態庫的兩個弊端:1)靜態庫更新後,程式要獲得該靜態庫然後再編譯。2)不同程式可能使用相同的靜態庫,導致很多靜態庫中的代碼重複被載入到儲存空間中。
共用庫是致力於解決靜態庫的缺陷而出現的現代創新型產物。共用庫是一塊目標模組,在運行時,可以載入到任意的儲存空間地址,並和一個在儲存其中的程式連結起來。這個過程稱之為”動態連結“,是由一個叫做”動態連結器“的程式來完成的。
共用庫是以梁總方式來共用的:1)所有引用該庫的程式都共用一個.so檔案中的代碼和資料,而不是靜態庫一樣拷貝一份。2)在儲存空間中,一個共用庫的.text節的一個副本可以被不同正在啟動並執行進城共用,從而節約寶貴的儲存空間資源。(Unix中動態庫以.so尾碼表示。)
理解動態庫=共用庫的概念非常重要。動態庫一般是大型軟體或者作業系統的最愛,因為對於普通應用來說,沒有那麼多庫給別人使用,絕大多數都是自己用,所以靜態庫就夠了。
7.11從應用程式中載入和連結共用庫
應用程式還可能從應用程式中載入和連結任意共用庫,而無需編譯時間連結那些庫到應用中(這個牛逼大了)!
Windows中的更新大部分是這個技術。另外還有構建高效能web伺服器。
Linux為動態連結器提供了一系列簡單的介面:
#include <dlfcn.h> void *dlopen(const char *filename, int flag);//載入共用庫 void *dlsym(void *handle, char *symbol); //指向一個共用庫的控制代碼和一個符號名字。 int dlclose(void *handle); //下載共用庫 const char *dlerror(void); //容錯
Java定義了一個標準的調用規則,叫做Java本地介面(Java NativeInterface,JNI),它允許Java程式調用本地的C和C++函數。JNI的基本思想是將本地的C函數,如foo,編譯到共用庫中,如foo.so .當一個正在啟動並執行Java程式試圖調用函數foo時,Java解析程式利用dlopen介面(或者類似的介面)動態連結和載入foo.so,然後調用foo。
7.12與位置無關的代碼(PIC)
7.13處理目標檔案的工具