ByteScope

PE(Windows 執行檔)檔案格式

Portable Executable——Windows 的 .exe/.dll 二進位格式。

MIME 類型
application/vnd.microsoft.portable-executable
副檔名
.exe, .dll, .sys
分類
執行檔與位元碼

Magic number(檔案簽章)

0x000000004D5AMZDOS 'MZ' stub

位移 0x3C 的 4 位元組位移指向 'PE\0\0'(50 45 00 00)簽章。

關於 PE(Windows 執行檔)格式

PE 是 Windows 執行檔、DLL 與驅動程式的格式,而它到今天還穿著 1985 年的戲服:每個 PE 檔案開頭都是一支能跑的 MS-DOS 程式,它唯一的工作是印一行字然後結束,好讓 Windows 執行檔在 DOS 上被執行時吐出一句話而不是當掉。真正的標頭要靠位移 0x3C 存的那個指標才找得到,順著它會看到 4 個位元組 'PE\0\0'、一個指明機器架構與 section 數量的 COFF 檔案標頭,以及一個 optional header——對映像檔來說它一點都不 optional——裡面放著進入點、偏好載入位址與資料目錄。

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

位移長度欄位意義
02e_magic ('MZ')4D 5A,是 MS-DOS 原始架構師之一 Mark Zbikowski 的姓名縮寫。每個 PE 映像、每個 DOS 執行檔、每個 .NET 組件開頭都是這兩個位元組,所以它們只認得出「這一家族」,再細就認不出來了。
258Rest of the MS-DOS header頁數、重定位表位移、真實模式 DOS 的初始堆疊與指令指標。Windows 全部略過;連結器只是填一份範本進去。真正有意義的只有它的長度,因為它決定了「指向真正標頭的那個指標」放在哪。
604e_lfanewPE 簽章的小端序檔案位移——這是 DOS 標頭裡 Windows 唯一會讀的欄位。0x80(128)是 MSVC 與 GCC 產出的經典值,但它真的會變,絕對不要當成固定值。底下所有東西的位移都是相對於它報出來的。
64variableMS-DOS stub program真正的 16 位元程式碼,加上字串 'This program cannot be run in DOS mode'。它是每個 Windows 執行檔到現在還揹著的贅重,結束在 e_lfanew 指到的地方。
e_lfanew + 04PE signature50 45 00 00,PE 兩個字母加兩個 NUL 位元組。如果位移 60 存的那個位置上不是這 4 個位元組,這個檔案要嘛是純 DOS 執行檔、要嘛是壞掉的 PE,載入器會拒收。
e_lfanew + 42Machine目標 CPU:0x8664 是 x64、0x014C 是 32 位元 x86、0xAA64 是 ARM64。架構不符時跳出「不是有效的 Win32 應用程式」,背後就是這個欄位。
e_lfanew + 62NumberOfSectionsoptional header 後面接著幾個 section header。一般是五到二十個;加殼或加保護的執行檔常常只有兩三個,而且名稱不是 .text 或 .rdata。
e_lfanew + 84TimeDateStamp連結當下以 1970 起算的秒數——舊工具鏈寫的是真正的建置時間,新的則寫成可重現建置用的雜湊值,所以看到 2107 年或 1970 年的日期是建置系統的產物,不是損壞。
e_lfanew + 128PointerToSymbolTable + NumberOfSymbols兩個 4 位元組欄位,指向 COFF 符號表。對映像檔而言已經淘汰,微軟連結器產出的東西通常都是 0;MinGW 的執行檔還是會填,所以 GCC 建置出來的 .exe 帶著符號,Visual Studio 建的沒有。
e_lfanew + 202SizeOfOptionalHeader後面那個 optional header 的位元組長度:32 位元映像是 224、64 位元是 240、目的檔是 0。把它加上 optional header 的起點,就能直接找到 section 表,中間什麼都不用解析。
e_lfanew + 222Characteristics屬性旗標。0x0002 標示這是可執行映像,0x2000 標示這是 DLL——.dll 和 .exe 真正的差別就在這裡,副檔名只是慣例,翻這裡一個位元就換邊了。
e_lfanew + 242Optional header Magic0x010B 是 PE32,0x020B 是 64 位元的 PE32+。它和 Machine 欄位是兩回事,而且兩邊必須一致:PE32+ 把映像基底位址和好幾個大小欄位加寬,所以把 PE32+ 標頭當成 PE32 讀,會得到一堆看起來很像樣的胡說八道。
e_lfanew + 24 + SizeOfOptionalHeader40 per sectionSection table每個 section 一筆 40 位元組的項目:8 位元組、以 NUL 補滿的名稱,虛擬大小與虛擬位址,然後是原始大小與檔案位移。這張表就是把「你在 hex 編輯器裡看到的」對應到「行程在記憶體裡看到的」的那張表。

怎麼跟長得像的格式分開

容易搞混的怎麼分
MS-DOS executable真正的 16 位元 DOS 程式開頭也是 4D 5A,而且完全沒有 PE 標頭——位移 0x3C 那幾個位元組只是它標頭的一部分,不是指標。順著指標走:如果目標落在檔案結尾之外、或那裡的 4 個位元組不是 50 45 00 00,你手上的就是純 DOS 或壞掉的檔案。這正是為什麼偵測器對一個光禿禿的 MZ 檔案只能說「大概是 PE」。
ELF 執行檔位元組層級毫無懸念——4D 5A 對上 7F 45 4C 46——但這是交叉編譯建置目錄裡最常見的混淆,.so 和 .dll 並排躺著,兩邊都可能叫 lib 開頭的名字。相信副檔名之前,先讀位移 0。
OLE2 / CFB兩者都是同一個年代的微軟容器格式,廣義來說也都裝著「一支程式」,所以分類時常把有巨集的 .doc 和 .exe 搞混。OLE2 compound file 開頭是 D0 CF 11 E0 A1 B1 1A E1,絕不是 MZ,而且它的結構是一棵類 FAT 的串流目錄,不是映射進記憶體的 section。
.NET assembly受管理的 .exe 或 .dll 就是一個完全正常的 PE 檔案——同樣的 MZ、同樣的 PE 簽章、同樣的 section 表——只是幾乎不含原生程式碼。線索是資料目錄第 14 項(CLR 標頭)非零,而且 .text section 的內容是 CIL 中繼資料;這就是為什麼 .NET 二進位反編譯得乾乾淨淨,C++ 的不行。

常見的壞法

  • e_lfanew 指到不存在的地方。在 PE 檔案前面塞任何東西——一頁 HTML 錯誤頁、一個 UTF-8 BOM、一個合併錯的修補檔——MZ magic 還在,位移 0x3C 的位移卻是從錯的基準量的,於是載入器去找 'PE\0\0',找到的是 section 資料。檔案被認成執行檔,然後拒絕執行。
  • 被截斷。section 資料排在所有標頭後面,所以下載一半的檔案解析起來毫無問題:機器架構、section 數量、進入點都讀得正確,只有最後一個 section 的原始資料短了一截。Windows 只會報一個籠統的映像錯誤,診斷方式是把每個 section 的 PointerToRawData 加 SizeOfRawData 和實際檔案大小比對。
  • Authenticode 簽章失效。只要在排除範圍之外改動任何一個位元組,簽署過的雜湊就對不上,所以被修補過、被追加過、或被釘上安裝程式外殼的檔案照樣跑得動,卻被回報為未簽署或已遭竄改——而在有強制簽章的機器上,根本跑不起來。
  • 文字模式傳輸。那個會毀掉任何二進位檔的 CRLF 轉換,會讓第一個 0D 0A 之後的所有東西位移。因為 PE 標頭是用絕對位移找到的、不是掃出來的,失敗會立刻表現成「標頭讀不了」,而不是某個隱晦的 bug。
  • 把追加在後面的內容當成損壞。安裝程式、自解壓縮檔和某些授權外殼,本來就會合法地在最後一個 section 之後追加資料,所以檔案比標頭描述的還大,對 PE 來說很正常,光憑這件事不能當作損壞的證據。

這個格式現在還在用嗎

每一支 Windows 程式、DLL、核心驅動程式與 .NET 組件都是 PE,而且 UEFI 韌體應用程式也用它,所以任何平台上的現代開機載入器通常就是一個 PE 檔案。在 Windows 上沒有東西取代它,也沒有東西打算取代:這個格式一路吸收了 64 位元定址、受管理程式碼、ARM64 與 ARM64EC 混合映像,相容性從來沒有斷過,所以 1993 年的執行檔到現在標頭還認得出來。它在 Windows 之外的觸及範圍只有 Wine 和 UEFI,所以跨平台專案是同一支程式各出一份 PE、ELF 和 Mach-O,而不是一份成品打天下。對逐位元組讀檔的人來說,實務上的結論是:這三種格式裡只有 PE 把真正的標頭藏在一個指標後面——有意思的東西全都從位移 0x3C 說的那個地方開始。

在這個站上打開 PE(Windows 執行檔)檔

這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。

相關格式

容易和 PE(Windows 執行檔)搞混的格式,可以比對它們的開頭位元組。

格式開頭位元組副檔名
ELF 執行檔7F 45 4C 46.elf
Mach-O 執行檔CF FA ED FE.dylib
OLE2 / CFBD0 CF 11 E0 A1 B1.doc

常見問題

檔案的 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 仍然可能夾帶惡意內容。辨識真實類型是有用的第一道檢查,尤其在副檔名和位元組不一致的時候,但它不是惡意軟體掃描。

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