ELF 執行檔案格式
Executable and Linkable Format——Linux/Unix 的執行檔與共享物件格式。
- MIME 類型
- application/x-elf
- 副檔名
- .elf, .so, .o, .out
- 分類
- 執行檔與位元碼
Magic number(檔案簽章)
| 0x00000000 | 7F454C46 | ·ELF | 0x7F 'ELF' |
關於 ELF 執行檔格式
ELF 是 Linux、Android 原生層、各家 BSD 與多數嵌入式工具鏈的執行檔、共享程式庫、目的檔與 core dump 格式。前 16 個位元組是 e_ident,一段會自我描述的前導:先是 magic,接著一個位元組說明這是 32 位元還是 64 位元檔案,再一個位元組說明是小端序還是大端序——正因如此,同一支解析器讀得懂一個它從來沒跑過的機器架構的二進位檔。e_ident 之後的東西分成兩張表:program header 給核心用來把檔案映射進記憶體,section header 給連結器與除錯器用,而 strip 過的執行檔就算沒有它也活得下去。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 4 | EI_MAG0..3 (magic) | 7F 45 4C 46——刻意選一個非 ASCII 的位元組打頭,後面接 ELF 三個字母,所以檔案被文字模式傳輸弄壞、或被截成 7 位元,立刻就對不上。 |
| 4 | 1 | EI_CLASS | 1 是 ELFCLASS32,2 是 ELFCLASS64。這一個位元組決定標頭後面每個位址欄位的寬度,所以 32 位元檔案的 e_entry、e_phoff、e_shoff 各縮成 4 個位元組,位移 24 之後的所有東西往前挪 12 個位元組。 |
| 5 | 1 | EI_DATA | 1 是二補數小端序,2 是大端序。整個檔案裡每個多位元組欄位都照它解讀,所以一個假設小端序的解析器看 MIPS 或較舊的 PowerPC 執行檔,會覺得整份都是亂碼。 |
| 6 | 1 | EI_VERSION | ELF 標頭版本,永遠是 1。看到 0 或大於 1 的值代表標頭壞了,不代表出現了更新版的 ELF。 |
| 7 | 1 | EI_OSABI | 0 是 UNIX System V,多數 Linux 工具鏈實際上吐的就是這個;3 是 GNU/Linux、9 是 FreeBSD、12 是 OpenBSD。它只影響作業系統專屬的 section 與符號型別怎麼解讀,所以 Linux 執行檔標 0 或標 3 都很正常,不是瑕疵。 |
| 8 | 1 | EI_ABIVERSION | 上面那個 ABI 的子版本,幾乎永遠是 0。 |
| 9 | 7 | EI_PAD | 補零,把 e_ident 湊滿 16 個位元組。這裡出現非零位元組,是「有人手動改過或偽造標頭」的可靠氣味。 |
| 16 | 2 | e_type | 1 是 REL,可重定位目的檔;2 是 EXEC,固定位址的執行檔;3 是 DYN,涵蓋共享物件以及近十年所有位置無關執行檔;4 是 CORE,當機傾印。光看標頭分不出 .so 和 PIE——那要看 dynamic section 裡的 DF_1_PIE。 |
| 18 | 2 | e_machine | 目標 CPU:62 是 x86-64、183 是 AArch64、3 是 i386、40 是 32 位元 ARM、243 是 RISC-V。程式庫載入時報架構不符,要看的就是這個欄位。 |
| 20 | 4 | e_version | 目的檔版本,一樣永遠是 1。它是 EI_VERSION 換個寬度再寫一次,是原始 ABI 留下來的殘留物。 |
| 24 | 8 | e_entry | 核心把映像映射好之後跳過去的虛擬位址;32 位元檔案在同一個位移只有 4 個位元組。共享物件這裡通常是 0,因為它們沒有自己的進入點。 |
| 32 | 8 | e_phoff | program header 表的檔案位移。實務上就是 64——這張表緊接在標頭後面——因為載入器希望在它讀的第一頁裡就看到各個 segment。 |
| 40 | 8 | e_shoff | section header 表的檔案位移,通常就在檔案最尾端。0 代表 section 被 strip 掉了,而超過檔案長度的值,就是被截斷的執行檔的典型模樣。 |
| 48 | 4 | e_flags | 處理器專屬旗標。x86-64 上是空的,但 ARM 與 RISC-V 上它編碼了浮點 ABI 與指令集選項,連結時報 soft-float 與 hard-float 不相容就是從這裡來的。 |
| 52 | 2 | e_ehsize | 這個標頭的大小:64 位元檔案是 64,32 位元是 52。它是個便宜的自洽檢查——和 EI_CLASS 對不上的話,後面的東西就別再信了。 |
| 54 | 2 | e_phentsize | 每筆 program header 的位元組數:64 位元是 56,32 位元是 32。由 ABI 釘死,所以出現別的值代表檔案壞了,不是什麼罕見變體。 |
| 56 | 2 | e_phnum | program header 的數量。通常不到十來個;特殊值 0xFFFF 代表真正的數量放在第一個 section header 裡,這是給 segment 表大得誇張的執行檔用的變通做法。 |
| 58 | 2 | e_shentsize | 每筆 section header 的位元組數:64 位元是 64,32 位元是 40。 |
| 60 | 2 | e_shnum | section header 的數量。把它乘以 e_shentsize 再加上 e_shoff,在 section 表位於最後的完好檔案上,結果剛好等於檔案大小——這是最快的截斷檢查。 |
| 62 | 2 | e_shstrndx | 存放 section 名稱的那個 section 的索引。少了它,每個 section 都沒有名字,所以工具印出一堆有編號、沒名稱的 section,通常代表它讀到的檔案這個索引超出範圍。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| PE(Windows 執行檔) | Windows 那邊的對應物,而混淆是實務上的、不是位元組層級的:交叉編譯的產出會落在同一個目錄裡。PE 開頭是 4D 5A('MZ'),真正的標頭藏在位移 0x3C 的一個 4 位元組指標後面;ELF 寫 7F 45 4C 46,而且什麼都放在固定位移。一個其實是 .dll 的 .so,在位移 0 就露餡。 |
| Mach-O 執行檔 | macOS 上做同樣的事。Mach-O 開頭是 CF FA ED FE(64 位元小端序)或 CE FA ED FE(32 位元),絕不會是 7F 45 4C 46,而 universal binary 開頭是 CA FE BA BE。兩種格式的目的檔都用 .o 副檔名,程式庫則是 .dylib 對 .so,所以副檔名什麼都證明不了,前 4 個位元組什麼都證明得了。 |
| ar archive (.a) | 靜態程式庫不是 ELF 檔案:它是一個 ar 壓縮檔,成員才是 ELF 目的檔。它開頭是 8 個位元組 21 3C 61 72 63 68 3E 0A('!<arch>' 加一個換行),第一個 7F 45 4C 46 要到大約 60 個位元組之後、第一個成員標頭的後面才出現。工具對 .a 回報「不是 ELF 檔案」是對的。 |
| Core dump | core 檔案是貨真價實的 ELF 檔案,標頭一模一樣,所以 file 指令會說它是 ELF。分界在位移 16 的 e_type:4 代表 CORE,而這種檔案有 program header、沒有 section header、也沒有符號,所以除錯器需要原始執行檔擺在旁邊一起用。 |
| PIE | 共享物件和位置無關執行檔的 e_type 都是 3(DYN),所以光看標頭真的分不出來——這不是誰還沒想到的捷徑,是本來就沒有。差別在 dynamic section 的 DT_FLAGS_1 項目裡的 DF_1_PIE,readelf 在印出 'Position-Independent Executable file' 之前檢查的正是它。 |
常見的壞法
- 被 strip 掉,還是 section 壞了。strip 過的執行檔會把 e_shoff 設成 0,而且照樣跑得好好的,因為核心只讀 program header;壞掉的則是 e_shoff 指到檔案結尾之外。兩種都會讓 readelf 印不出有用的東西,而只有 e_shoff 的值能告訴你手上的是哪一種。
- 被截斷。section header 表通常住在 ELF 檔案的最尾端,所以下載中斷第一個弄丟的就是它:program header 解析得動、進入點看起來也正常,而每個吃 section 的工具都失敗。拿 e_shoff 加上 e_shnum 乘 e_shentsize,跟實際檔案大小比一比。
- 文字模式傳輸。用會轉換換行的用戶端複製二進位檔,每個 0D 0A 都變成 0A,第一次出現之後的所有東西全部位移。magic 常常還活著,所以檔案照樣被認成 ELF,而 program header 指向一堆莫名其妙的位移。
- EI_CLASS 或 EI_DATA 位元組錯了。把第 4 或第 5 個位元組翻掉,標頭依然「有效」——解析器只是用錯的寬度或錯的位元組序去讀後面每個欄位,然後報出巨大的位移和荒謬的 section 數量,而不是承認這個檔案壞了。
- 就地修補卻沒有修位移。把 .rodata 裡的字串改成更長的、或在任何地方插入位元組,都會把 section 表與 program header 表往後推,而 e_shoff 和 e_phoff 沒有跟著更新。這種執行檔載入得起來,一路跑到跳進某個已經不是程式碼的地方為止。
這個格式現在還在用嗎
每一支 Linux 程式、共享程式庫與核心模組都是 ELF,Android 的 APK 裡 lib/ 底下裝的是 ELF,而幾乎每套嵌入式工具鏈在把映像壓平成 raw image 或 Intel HEX 之前,吐出來的也是 ELF。在 Unix 上它沒有像樣的對手:a.out 和 COFF 在九零年代中期就消失了,就連那些在「散布」層面取代 ELF 的東西——容器映像、Flatpak、APK——本質上也是裝滿 ELF 檔案的壓縮檔。它碰不到的地方是 Windows(那是 PE)和 Apple 平台(那是 Mach-O),所以一棵交叉編譯的建置目錄裡三種都有,而分辨它們的只有前 4 個位元組。至於 WebAssembly 那一塊,工具鏈在吐出 .wasm 模組之前,內部用的仍然是 ELF——這也是為什麼 emscripten 的建置目錄裡滿是永遠不會出貨的 ELF 目的檔。
在這個站上打開 ELF 執行檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和 ELF 執行檔搞混的格式,可以比對它們的開頭位元組。
| 格式 | 開頭位元組 | 副檔名 |
|---|---|---|
| PE(Windows 執行檔) | 4D 5A | .exe |
| Mach-O 執行檔 | CF FA ED FE | .dylib |
| WebAssembly | 00 61 73 6D | .wasm |
| Android DEX | 64 65 78 0A | .dex |