瞭解世界的起源,是一個永遠的題目~
ELF (Executable and Linkable Format) ,是一種常用在 Linux 的檔案格式。 就如同名稱所表示的一樣,是用來描述一個可以用來執行的檔案格式,或者當檔案執行時, 用來動態/靜態連結時使用的檔案。也跟名字描述的一樣,同一個檔案可以用兩種角度來解釋: 連結觀點 (Linking View) 以及執行觀點 (Execution View)。
ELF Format
一個 ELF 的檔案,一定符合某種特定的格式:開頭的檔頭 (Header) 大小與不同的平台有關, 根據使用的平台為 32/64 位元,有 56/64 Bytes 兩種大小,而不管是哪一種格式, 開頭都會有一組固定大小的資料用來描述最基本的資訊:前面的 4 Bytes 的資訊來判斷, 用來判斷是否是一個 ELF 檔案 (Magic Number),接下來則是用來判斷是哪一種平台 (32/64)。而剩下來的欄位,則是讓 OS 來判斷要用哪種方式來解析接下來的資訊,其中包含 位元順序 (Endianness) 、 ABI (Application Binary Interface) 及使用的版本 。根據這些資訊,我們就可以解析接下來的欄位了。
\x7FELF ## Magic
\x02 ## Class
\x01 ## Endianness
\x01 ## version
\x00 ## OS ABI
\x00 ## OS ABI version
\x00\x00\x00\x00\x00\x00\x00
在 64-bit 的 ELF 檔案架構中,整個標頭總共含有 64 Bytes,其中 16 Byte 已經介紹過了, 剩下的則是用來描述如何解析各種 View 所需知道的欄位:根據接下來的兩個 Byte,描述這個 ELF 本身具有的能力:其中包含了 物件檔 (Object) 、可執行檔 (Execute)、共用函示 (Shared Library)、 核心檔案 (Core File) 以及其他 OS/CPU 相關的檔案。 接下來的兩個 Byte 就可以知道之後的 機械碼 (Machine Code) 用在哪個平台。 因為最終 OS 會讀 ELF 檔案裡面的機械碼並且執行,根據不同的 CPU 架構提供不同的能力 。因為這十分依賴平台的能力,所以需要注意不同的 ELF 檔並不能在不同平台上執行。
之後接下來的三個欄位,則會根據不同的平台 (32/64 bits) 而有不同的大小, 原因在於這三個欄位都是用來描述位址:第一個是用來描述程式的進入點, 這也表示所有的機械碼都是從這個位址開始。需要注意這個數值跟很多地方都會有關聯, 包含了接下來在程式標頭 (Segment Header) 區塊以及接下來機械碼會使用到的位址。 而接下來的兩個值,則是用來描述程式標頭與分節區塊 (Section Header) 在這整的檔案的偏移量 (Offset)。 剩下來的欄位,則是用來描述額外的旗標值 (FLags)、程式標頭與分節標頭的基本描述 (大小與數量),最後一個則是用來給標注共用字串表 (Shared String Table) 的索引。
\x02\x00 ## Type of ELF
\x3E\x00 ## Machine
\x01\x00\x00\x00 ## version
\x78\x00\x40\x00\x00\x00\x00\x00 ## Entry Address
\x40\x00\x00\x00\x00\x00\x00\x00 ## Segment Offset
\x00\x00\x00\x00\x00\x00\x00\x00 ## Section Offset
\x00\x00\x00\x00 ## Flags
\x40\x00 ## Size of this header
\x38\x00 ## Segment header size
\x01\x00 ## Number of segment header
\x00\x00 ## Section header size
\x00\x00 ## Number of section header
\x00\x00 ## Index of the .shstrtab section
對一個可以執行的檔案來說,分節區段並非必需存在。在執行階段來看, 所需要的除了必須的標頭之外,還需要的是執行區段,執行區段包含著很多區塊, 每個區塊都有各自的用途以及含義,其中必需的型態則為 LOAD。而每一個區塊都有各自的屬性, 用來描述他在記憶體空間儲存的內容,是否可以被讀取 (R)、寫入 (W) 或執行 (X)。
\x01\x00\x00\x00 ## Segment type
\x05\x00\x00\x00 ## Segment flags
\x00\x00\x00\x00\x00\x00\x00\x00 ## Segment offset
\x00\x00\x10\x00\x00\x00\x00\x00 ## Segment virtual address
\x00\x00\x10\x00\x00\x00\x00\x00 ## Segment physical address
\x84\x00\x00\x00\x00\x00\x00\x00 ## Segment size in file
\x84\x00\x00\x00\x00\x00\x00\x00 ## Segment size in memory
\x00\x10\x00\x00\x00\x00\x00\x00 ## Segment alignment
而在分節區塊中,因為在這 ELF 101 中用不到,僅紀錄每個欄位的用途。
\x00\x00\x00\x00 ## Section name of index in shstrtab
\x00\x00\x00\x00 ## Type
\x00\x00\x00\x00\x00\x00\x00\x00 ## Flags
\x00\x00\x00\x00\x00\x00\x00\x00 ## Virtaul address at execution
\x00\x00\x00\x00\x00\x00\x00\x00 ## Section offset in file
\x00\x00\x00\x00\x00\x00\x00\x00 ## Section size in byte
\x00\x00\x00\x00 ## Link to another section
\x00\x00\x00\x00 ## Addition section information
\x00\x00\x00\x00\x00\x00\x00\x00 ## Section alignment
\x00\x00\x00\x00\x00\x00\x00\x00 ## Entry size if section holds table
最後一個部分則是機器碼。這個部分根據程式的需求,由組合語言經過組譯器編譯之後獲得 ,這部分根據不同的 CPU 架構而有不同的結果,實際上會對應到底層的電子線路操作。 一個簡單例子則是正常的結束程式:呼叫系統提供的 exit 指令並填入適當的回傳值之後。 下面是一個簡單的 x86_64 的結束程式的組合語言與機械碼。
mov 60 rax ## \x48\x31\xFF
xor rdi rdi ## \x48\xC7\xC0\x3C\x00\x00
syscall ## \x0F\x05
最後,根據上述的幾個描述,我們就可以憑空撰寫一個 ELF 可執行檔。
0000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
0000010: 0200 3e00 0100 0000 7800 1000 0000 0000 ..>.....x.......
0000020: 4000 0000 0000 0000 0000 0000 0000 0000 @...............
0000030: 0000 0000 4000 3800 0100 0000 0000 0000 ....@.8.........
0000040: 0100 0000 0500 0000 0000 0000 0000 0000 ................
0000050: 0000 1000 0000 0000 0000 1000 0000 0000 ................
0000060: 8400 0000 0000 0000 8400 0000 0000 0000 ................
0000070: 0010 0000 0000 0000 4831 ff48 c7c0 3c00 ........H1.H..<.
0000080: 0000 0f05 0a .....>