ByteScope

ELF 執行檔案格式

Executable and Linkable Format——Linux/Unix 的執行檔與共享物件格式。

MIME 類型
application/x-elf
副檔名
.elf, .so, .o, .out
分類
執行檔與位元碼

Magic number(檔案簽章)

0x000000007F454C46·ELF0x7F 'ELF'

關於 ELF 執行檔格式

ELF 是 Linux、Android 原生層、各家 BSD 與多數嵌入式工具鏈的執行檔、共享程式庫、目的檔與 core dump 格式。前 16 個位元組是 e_ident,一段會自我描述的前導:先是 magic,接著一個位元組說明這是 32 位元還是 64 位元檔案,再一個位元組說明是小端序還是大端序——正因如此,同一支解析器讀得懂一個它從來沒跑過的機器架構的二進位檔。e_ident 之後的東西分成兩張表:program header 給核心用來把檔案映射進記憶體,section header 給連結器與除錯器用,而 strip 過的執行檔就算沒有它也活得下去。

檔頭結構,一個位元組一個位元組看

位移長度欄位意義
04EI_MAG0..3 (magic)7F 45 4C 46——刻意選一個非 ASCII 的位元組打頭,後面接 ELF 三個字母,所以檔案被文字模式傳輸弄壞、或被截成 7 位元,立刻就對不上。
41EI_CLASS1 是 ELFCLASS32,2 是 ELFCLASS64。這一個位元組決定標頭後面每個位址欄位的寬度,所以 32 位元檔案的 e_entry、e_phoff、e_shoff 各縮成 4 個位元組,位移 24 之後的所有東西往前挪 12 個位元組。
51EI_DATA1 是二補數小端序,2 是大端序。整個檔案裡每個多位元組欄位都照它解讀,所以一個假設小端序的解析器看 MIPS 或較舊的 PowerPC 執行檔,會覺得整份都是亂碼。
61EI_VERSIONELF 標頭版本,永遠是 1。看到 0 或大於 1 的值代表標頭壞了,不代表出現了更新版的 ELF。
71EI_OSABI0 是 UNIX System V,多數 Linux 工具鏈實際上吐的就是這個;3 是 GNU/Linux、9 是 FreeBSD、12 是 OpenBSD。它只影響作業系統專屬的 section 與符號型別怎麼解讀,所以 Linux 執行檔標 0 或標 3 都很正常,不是瑕疵。
81EI_ABIVERSION上面那個 ABI 的子版本,幾乎永遠是 0。
97EI_PAD補零,把 e_ident 湊滿 16 個位元組。這裡出現非零位元組,是「有人手動改過或偽造標頭」的可靠氣味。
162e_type1 是 REL,可重定位目的檔;2 是 EXEC,固定位址的執行檔;3 是 DYN,涵蓋共享物件以及近十年所有位置無關執行檔;4 是 CORE,當機傾印。光看標頭分不出 .so 和 PIE——那要看 dynamic section 裡的 DF_1_PIE。
182e_machine目標 CPU:62 是 x86-64、183 是 AArch64、3 是 i386、40 是 32 位元 ARM、243 是 RISC-V。程式庫載入時報架構不符,要看的就是這個欄位。
204e_version目的檔版本,一樣永遠是 1。它是 EI_VERSION 換個寬度再寫一次,是原始 ABI 留下來的殘留物。
248e_entry核心把映像映射好之後跳過去的虛擬位址;32 位元檔案在同一個位移只有 4 個位元組。共享物件這裡通常是 0,因為它們沒有自己的進入點。
328e_phoffprogram header 表的檔案位移。實務上就是 64——這張表緊接在標頭後面——因為載入器希望在它讀的第一頁裡就看到各個 segment。
408e_shoffsection header 表的檔案位移,通常就在檔案最尾端。0 代表 section 被 strip 掉了,而超過檔案長度的值,就是被截斷的執行檔的典型模樣。
484e_flags處理器專屬旗標。x86-64 上是空的,但 ARM 與 RISC-V 上它編碼了浮點 ABI 與指令集選項,連結時報 soft-float 與 hard-float 不相容就是從這裡來的。
522e_ehsize這個標頭的大小:64 位元檔案是 64,32 位元是 52。它是個便宜的自洽檢查——和 EI_CLASS 對不上的話,後面的東西就別再信了。
542e_phentsize每筆 program header 的位元組數:64 位元是 56,32 位元是 32。由 ABI 釘死,所以出現別的值代表檔案壞了,不是什麼罕見變體。
562e_phnumprogram header 的數量。通常不到十來個;特殊值 0xFFFF 代表真正的數量放在第一個 section header 裡,這是給 segment 表大得誇張的執行檔用的變通做法。
582e_shentsize每筆 section header 的位元組數:64 位元是 64,32 位元是 40。
602e_shnumsection header 的數量。把它乘以 e_shentsize 再加上 e_shoff,在 section 表位於最後的完好檔案上,結果剛好等於檔案大小——這是最快的截斷檢查。
622e_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 dumpcore 檔案是貨真價實的 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
WebAssembly00 61 73 6D.wasm
Android DEX64 65 78 0A.dex

常見問題

檔案的 magic number 是什麼?

檔案開頭的前幾個位元組,不管檔名寫什麼,它標明的是真正的格式。PNG 一定以 89 50 4E 47 開頭,ZIP 則是 50 4B 03 04。作業系統、瀏覽器和 file 指令讀的都是這幾個位元組,而不是相信副檔名 — 這就是為什麼把檔案改名並不會改變它實際上是什麼。

我要怎麼自己檢查 magic number?

在 macOS 或 Linux 上,xxd -l 16 檔名 會印出前 16 個位元組;在 Windows 上,PowerShell 的 Format-Hex -Path 檔名 -Count 16。或者把檔案丟進本站的檔案類型辨識工具,它會在本機讀取位元組並告訴你是什麼格式,不會上傳任何東西。

為什麼兩種格式的 magic number 會一樣?

因為其中一種是建立在另一種之上。docx、xlsx、pptx、apk、jar 與 epub 骨子裡都是 ZIP 壓縮檔,所以全都以 ZIP 的簽章開頭;要分辨它們得看裡面的項目。Java class 檔和 macOS 的通用二進位檔都以 CA FE BA BE 開頭,靠緊接其後的欄位區分。

有完全沒有 magic number 的檔案嗎?

有。XML、HTML、JSON、SVG、CSV 這類以文字為基礎的格式沒有固定的位元組簽章,所以改用結構來辨識:開頭的 <?xml 宣告、<svg 元素,或是 { 後面接著帶引號的鍵。這種偵測是合理推測而非確定,因此本參考頁會把這些格式標示為結構偵測。

magic number 相符就代表檔案安全嗎?

不代表。它告訴你的是格式,不是內容是否無害 — 一個貨真價實的 PDF 或 docx 仍然可能夾帶惡意內容。辨識真實類型是有用的第一道檢查,尤其在副檔名和位元組不一致的時候,但它不是惡意軟體掃描。

手上有檔案可能是 ELF 執行檔?用檔案類型辨識工具查出來 — 它在本機讀取 magic number,不上傳任何東西。或者瀏覽所有格式。