PE(Windows 実行ファイル)ファイル形式
Portable Executable — Windows の .exe/.dll バイナリ形式。
- MIME タイプ
- application/vnd.microsoft.portable-executable
- 拡張子
- .exe, .dll, .sys
- カテゴリ
- 実行ファイル・バイトコード
マジックナンバー(ファイル署名)
| 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 で実行したときにクラッシュではなく 1 行の文章が出るようにするためでした。本当のヘッダにはオフセット 0x3C に置かれたポインタからたどり着きます。その先にあるのが 4 バイトの 'PE\0\0'、マシンとセクション数を示す COFF ファイルヘッダ、そしてイメージにとってはまったく optional ではない optional header で、エントリポイント・希望するロードアドレス・データディレクトリを持っています。
ヘッダーの構造(バイト単位)
| オフセット | 長さ | フィールド | 意味 |
|---|---|---|---|
| 0 | 2 | e_magic ('MZ') | 4D 5A、初期の MS-DOS 設計者の 1 人 Mark Zbikowski のイニシャルです。PE イメージも DOS の実行ファイルも .NET アセンブリも、すべてこの 2 バイトで始まるので、示せるのは一族の名前までで、それ以上具体的なことは分かりません。 |
| 2 | 58 | Rest of the MS-DOS header | ページ数、再配置テーブルのオフセット、リアルモード DOS 用の初期スタックポインタと命令ポインタ。Windows はこれらを一切見ませんし、リンカはテンプレートを書くだけです。意味を持つのは長さだけで、それが本当のヘッダへのポインタの位置を確定させます。 |
| 60 | 4 | e_lfanew | PE 署名のファイルオフセットをリトルエンディアンで。DOS ヘッダの中で Windows が読む唯一のフィールドです。MSVC や GCC の出力では 0x80(128)が定番ですが、本当に可変なので決め打ちしてはいけません。以下の行はすべてこの値からの相対で書いています。 |
| 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 の 2 文字と NUL バイト 2 つ。オフセット 60 に格納された位置にこの 4 バイトが無ければ、それはただの DOS 実行ファイルか壊れた PE であり、ローダーは受け付けません。 |
| e_lfanew + 4 | 2 | Machine | 対象 CPU: x64 が 0x8664、32 ビット x86 が 0x014C、ARM64 が 0xAA64。アーキテクチャが合わないときに出る「有効な Win32 アプリケーションではありません」の背後にあるのがこのフィールドです。 |
| e_lfanew + 6 | 2 | NumberOfSections | optional header の後ろに続くセクションヘッダの数。ふつうは 5 個から 20 個ほどで、パックされたバイナリや保護されたバイナリは .text でも .rdata でもない名前のセクションを 2〜3 個だけ持っていることがよくあります。 |
| e_lfanew + 8 | 4 | TimeDateStamp | リンク時点の 1970 年からの秒数。古いツールチェーンでは本物のタイムスタンプですが、新しいものでは再現可能ビルドのためのハッシュなので、2107 年や 1970 年という日付は破損ではなくビルドシステムの産物です。 |
| e_lfanew + 12 | 8 | PointerToSymbolTable + NumberOfSymbols | COFF シンボルテーブル用の 4 バイトフィールドが 2 つ。イメージでは非推奨で、Microsoft のリンカが作ったものならたいていゼロです。MinGW のバイナリは今も埋めるので、GCC でビルドした .exe には Visual Studio のものには無いシンボルが入っています。 |
| e_lfanew + 20 | 2 | SizeOfOptionalHeader | 後ろに続く optional header のバイト長。32 ビットイメージなら 224、64 ビットなら 240、オブジェクトファイルなら 0。optional header の開始位置にこれを足せば、間にあるものを一切パースせずにセクションテーブルへたどり着けます。 |
| e_lfanew + 22 | 2 | Characteristics | 属性フラグ。0x0002 が実行可能イメージ、0x2000 が DLL であることを示します。これが .dll と .exe の実際の違いです。拡張子はただの慣習にすぎず、このビット 1 つでどちらにでも変わります。 |
| e_lfanew + 24 | 2 | Optional header Magic | PE32 なら 0x010B、64 ビット版の PE32+ なら 0x020B。Machine フィールドとは別物で、両方が矛盾なく揃っている必要があります。PE32+ ではイメージベースといくつかのサイズフィールドが広がるので、PE32+ のヘッダを PE32 として読むと、もっともらしく見えるでたらめが出てきます。 |
| e_lfanew + 24 + SizeOfOptionalHeader | 40 per section | Section table | セクションごとに 40 バイトのエントリが 1 つ。NUL 埋めの 8 バイトの名前、仮想サイズと仮想アドレス、そして生のサイズとファイルオフセット。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 | どちらも同時代の Microsoft のコンテナ形式で、緩い意味ではどちらも「プログラム」を抱えているので、マクロ入りの .doc と .exe は初期の切り分けで混同されがちです。OLE2 の compound file は D0 CF 11 E0 A1 B1 1A E1 で始まり、MZ になることは決してありませんし、構造もメモリへ写像されるセクションではなく、FAT めいたストリームのディレクトリです。 |
| .NET assembly | マネージドの .exe や .dll は、まったく普通の PE ファイルです。MZ も PE 署名もセクションテーブルも同じで、ただネイティブコードがほとんど入っていません。手掛かりはデータディレクトリの 14 番目(CLR ヘッダ)が非ゼロであることと、.text セクションの中身が CIL のメタデータであること。.NET のバイナリが綺麗に逆コンパイルできて、C++ のものはそうならないのはこれが理由です。 |
よくある壊れ方
- e_lfanew がどこも指していない。PE ファイルの前に何かを足すと(HTML のエラーページ、UTF-8 BOM、マージを誤ったパッチ)、MZ のマジックナンバーは無傷のまま、オフセット 0x3C の値だけが間違った基準で測られることになります。ローダーは 'PE\0\0' を探しに行ってセクションのデータを見つけます。ファイルは実行ファイルと識別され、そして起動を拒みます。
- 途中切れ。セクションのデータはすべてのヘッダの後ろにあるので、途中までのダウンロードでもパースは完璧に通ります。マシンもセクション数もエントリポイントも正しく読めて、最後のセクションの生データだけが足りません。Windows は一般的なイメージエラーを報告しますが、診断は各セクションの PointerToRawData + SizeOfRawData を実際のファイルサイズと比べることです。
- Authenticode 署名の失効。除外範囲の外で 1 バイトでも変われば署名ハッシュが壊れるので、パッチを当てた、追記した、インストーラーのスタブを貼り付けた、といったファイルは動きはするものの未署名または改ざん扱いになります。署名を強制しているマシンでは、そもそも動きません。
- テキストモード転送。あらゆるバイナリを台無しにするいつもの CRLF 変換が、最初の 0D 0A より後ろを全部ずらします。PE のヘッダは走査ではなく絶対オフセットでたどり着くものなので、微妙なバグとしてではなく、読めないヘッダとして即座に表面化します。
- 追記されたペイロードを破損と勘違いする。インストーラー、自己展開アーカイブ、一部のライセンスラッパーは最後のセクションの先へ正当にデータを追記します。だからヘッダが記述するサイズよりファイルが大きいのは PE では正常なことで、それだけでは破損の証拠になりません。
この形式の現在地
Windows のプログラム、DLL、カーネルドライバー、.NET アセンブリはすべて PE ですし、UEFI のファームウェアアプリケーションも PE なので、今どきのブートローダーはプラットフォームを問わずたいてい PE ファイルです。Windows 上でこれを置き換えたものはなく、置き換えようとしているものもありません。64 ビットアドレッシング、マネージドコード、ARM64、ARM64EC のハイブリッドイメージまで、互換性を切らさずに吸収してきました。1993 年のバイナリでも、今でも見覚えのあるヘッダを持っています。Windows の外での広がりは Wine と UEFI に限られ、だからクロスプラットフォームのプロジェクトは 1 つの成果物ではなく、同じプログラムの PE と ELF と Mach-O を配ることになります。バイトを読む側にとっての実務上の帰結は、この 3 つのうち本当のヘッダをポインタの奥に隠しているのは 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 |