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