目標檔案是指:編譯器編譯原始碼後生成的檔案,那麼目標檔案裡面到底存放的是什麼呢?或者說我們的原始碼在經過編譯以後是怎麼樣儲存的呢?

目標檔案從結構上將,它是已經編譯後的可執行檔案格式,只是好沒有經過連結的過程,其中可能有些符號或有些地址還沒有被調整。其實,目標檔案本身就是按照可執行檔案格式儲存的,只是跟真正的可執行檔案在結構上稍有不同。

可執行檔案格式涵蓋了程式的編譯、連結、裝載和執行的各個方面。瞭解可執行檔案的格式對認識系統,瞭解整合編譯器背後的執行機理,還是很有好處的。

1.目標檔案的格式是什麼樣的?

現在PC平臺流行的可執行檔案格式(Executable),主要是Windows下的PE(Portable Executable)和linux的ELF (Executable Linkable Format),他們都是COFF(Common File Format)格式的變種。目標檔案就是原始碼編譯後為進行連結的那些中間檔案(Windows下面為.obj檔案;Linux下面為.o檔案),它和可執行檔案的內容和結構很相似,所以一般和可執行檔案採用同一種格式進行儲存。從廣義上來講,目標檔案與可執行檔案的格式其實幾乎是一模一樣的,所以,我們可以廣義的將目標檔案和可執行檔案看成是同一種類型的檔案。在Windows下,我們把目標檔案和可執行檔案都統一稱為PE-COFF檔案,在Linux下,我們把它們統稱為ELF檔案。
當然,事情沒有這麼簡單!不光是可執行檔案(Windows下面的.exe和Linux下面的ELF檔案)按照可執行檔案格式儲存。動態連結庫(DLL,dynamic linking library)[Windows下面的.dll檔案和Linux下面的.a檔案]以及靜態連結庫(Static linking Library)[Windows下面的.lib檔案和Linux下面的.a檔案]都是按照可執行檔案格式儲存的。只不過,在Windows平臺下,他們按照PE-COFF格式儲存,而在Linux平臺下按照ELF格式進行儲存。
ELF檔案標準裡面把系統中採用ELF格式的檔案歸為以下四類:
ELF檔案型別 說明 例項
可重定位檔案
Relocatable File
這類檔案包含了程式碼和資料,可以被用來連結成可執行檔案
或共享目標檔案,靜態連結庫也歸屬於這一類
Linux的.o檔案
Windows下的.obj檔案
可執行檔案
Executable File
這類檔案包含了可以直接執行的程式,它的代表就是ELF文
件,他們一般都沒有副檔名
比如bin/bash檔案
Windows的.exe檔案
共享目標檔案
shared Object File
這種檔案包含了程式碼和資料,可以在以下兩種情況下使用,一
種是連結器可以直接使用這種檔案跟其他的可重定位檔案和共享
目標檔案連結,產生新的目標檔案。第二種是動態連結器
可以將幾個這樣的共享目標檔案與可執行檔案結合,
作為程序對映的一部分來執行。
Linux的.so檔案
Windows下面的.dll檔案
核心轉儲檔案
Core Dump File
當程序意外終止時,系統可以將該程序的地址空間的
內容及終止時的一些其他資訊轉儲到核心轉儲檔案中
Linux下面的core dump

2.目標檔案與可執行檔案格式的小歷史

目標檔案與可執行檔案格式與作業系統和編譯器密切相關,所以不同的系統平臺下會有不同的格式,但這些格式又大同小異,目標檔案格式與可執行檔案格式的歷史幾乎就是作業系統的發展史。
COFF是由Unix System VRelease 3首先提出並且使用的檔案規範,後來微軟公司基於COFF格式,制定了PE格式標準,並將其用於當時的Windows NT系統。System VRelease 4在COFF的基礎上引入了ELF格式,目前流行的Linux系統也是以ELF作為基本的可執行檔案格式。這也能解釋為什麼目前PE和ELF如此相似的主要原因,因為他們都是來源於同一種可執行檔案格式COFF。

3.目標檔案到底是什麼樣的?

猜也可以猜到,目標檔案中的內容至少有編譯後的機器指令程式碼、資料。沒錯,除了這些內容以外,目標檔案中還包含了連結時所需要的一些資訊,比如符號表、除錯資訊、字串等。一般目標檔案將這些資訊按不同的屬性,以“節”(section)的形式儲存,有時候也叫“段”(segment),在一般情況下,他們都表示一個一定長度的區域,基本上不加以區別,唯一的區別是在ELF的連結檢視和裝載檢視的時候,需要特別注意。
程式原始碼編譯後的機器指令經常放在程式碼段(Code Section)裡,程式碼段常見的字有“.code”和“.text”;全域性變數和區域性變數資料經常放在資料段(Data Section),資料段的一段名字都叫“.data”。下面是一個簡單的程式被編譯成目標後的結構,如下圖所示:

假設上圖的可執行檔案格式是ELF,從圖中可以看到,ELF檔案的開頭是一個“檔案頭”,他描述了整個檔案的檔案屬性,包括檔案是否可執行、是靜態連結還是動態連結以及入口地址(如果是可執行檔案)、目標硬體、目標作業系統等資訊。標頭檔案包含一個段表(Section Table),段表事實是一個描述檔案中各個段的陣列。段表描述了檔案中各個段在檔案中的偏移位置及段的屬性,從段表裡面可以得到每個段的所有資訊。檔案頭後面就是各個段的內容,比如程式碼段儲存的就是程式的指令,資料段裡面儲存的就是程式的靜態變數等。
從上圖我們也能看出,
一般C語言的編譯後執行語句都編譯成機器程式碼,儲存在.text段;
已初始化的全域性變數和區域性靜態變數都儲存在.data段;
未初始化的全域性變數和區域性靜態變數一般放在一個叫.bss的段裡。
我們知道未初始化的全域性變數和區域性靜態變數預設值都為零,本來他們也可以被放在.data段裡的,但是因為他們都是0,所以為他們在.data段裡分配空間並且存放資料0是沒有必要的。程式執行的時候他們的確是要佔用記憶體空間的,並且可執行檔案必須記錄所有未初始化的全域性變數和區域性靜態變數的大小總和,記為“.bss”段。所以,.bss段只是為未初始化的全域性變數和區域性靜態變數預留位置而已。他並沒有內容,所以他在檔案中也不佔據空間

4.程式的指令和資料為什麼要分開存放?

總體來說,程式原始碼被編譯以後主要分成兩種段:程式指令和程式資料。程式碼段屬於程式指令,而資料段和.bss段屬於程式資料。
這樣,我們可能就會產生疑問:為什麼要這樣麻煩,把程式的指令和資料的儲存分開?混雜的放在一個段裡面豈不是會更簡單?其實資料和指令分段的好處有很多,主要如下:
#1:當程式被裝載後,資料和指令分別被對映到兩個虛存區域。由於資料區域對於程序來說是可讀寫的,而指令區域對於程序來說是隻讀的,所以這兩個虛存區域的許可權可以被分別設定成可讀寫或只讀。這樣可以防止程式的指令被有意或無意地改寫
#2:對現在的CPU來說,他們有著極為強大的快取(Cache)體系。由於快取在現代的計算機中地位非常重要,所以程式必須儘量提高快取的命中率。指令區和資料區的分離有利於提高程式的侷限性。現在CPU的快取一般都被設計成資料快取和指令快取分離,所以程式的指令和資料被分開存放對CPU的快取命中率提高有好處。
#3:這是最重要的原因!就是當系統中執行著多個改程式的副本時,他們的指令都是一樣的,所以記憶體中只需儲存一份該程式的指令部分。對於指令這種制度的區域來說是這樣的,對於其他的只讀資料也是這樣的。比如,很多程式裡面帶有的圖示、圖片、文字等資源也是屬於可以共享的。當然每個副本程序的資料區域是不一樣的,他們是程序私有的。在動態連結的系統中,該種方式可以節省大量記憶體。比如我們常用的Windows Internet Explorer 7.0 執行起來以後,他的總虛存空間為112844KB,他的私有資料是15944KB,既有96900KB的空間是共享部分!!!