PEファイルフォーマットは Windows のローダが認識してくれる実行可能ファイルのフォーマットの一つです。「Windows のローダが認識してくれる実行可能ファイル」というのは拡張子が EXE, DLL, OCX, VxD などのファイルのことです。
実行可能なのは EXE だけだと思うかもしれませんが、起動可能と実行可能はまた別の意味ということです。まぁ、そういった誤解を避けるために実行可能ファイルのことを一般的にイメージファイルと呼んでいるんですが。もちろん画像ファイルのことではなく、「メモリイメージ」をあらわしたファイルという意味だそうで。仕様書ではこんな説明がされています。
Executable file: either a .EXE file or a DLL. An image file can be thought of as a“memory image.”The term “image file” is usually used instead of “executable file,” because the latter sometimes is taken to mean only a .EXE file.そのイメージファイルという定義をプログラムの観点から見れば CreateProcess() API や LoadLibrary() API, LoadLibraryEx() API でロードできるファイルということになります。あとはカーネルから読み込まれるデバイスドライバなんかも。
PE ファイルの先頭には「MS-DOS ヘッダ」と「MS-DOS Real-Mode Stub Program」があります。これらは Windows の前身である MS-DOS と互換性をとるためだけのものであり、2つのフィールドを除いて Windows では使用されません。
MS-DOS で Windows 用のプログラムを起動すると、「This program cannot be run in DOS mode.」なんて画面に表示されますが、そのメッセージを出すために使われるヘッダおよびコード部分が MS-DOS ヘッダと、Real-Mode Stub Program です。Real-Mode Stub Program のサイズに特に制限はありません(MS-DOSで扱えるサイズまで)。つまり、このヘッダと Stub Program 次第では MS-DOS と Windows で同じ動作をするようなプログラムを作ることができます(やる意味はありませんが)。
MS-DOS ヘッダ は WinNT.h で以下のように定義されています。
WinNT.h
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
e_magic | 必ず 0x5A4D("MZ") というマジックナンバーが格納されていて、このファイルが実行可能ファイルであることを示しています。"MZ" は設計者 Mark Zbikowski 氏のイニシャルです。MS-DOS の COM を除く実行可能ファイルの一番最初には必ずこの "MZ" という文字があります。 |
---|---|
e_lfanew | 新しい形式のヘッダへのファイル内オフセットを示しています。新しい形式のヘッダとは PE ファイルフォーマットなら NT ヘッダのことであり、e_lfanew は NT ヘッダへのオフセットということになります。そして、その NT ヘッダこそが PE ファイルの真のヘッダということになります。 MS-DOS Real-Mode Stub Program のサイズは可変長なので、このオフセットの値から新しい形式のヘッダをたどらなければいけないのです。 |
NT ヘッダはマジックナンバーとヘッダのかたまりで、 WinNT.h で以下のように定義されています。
WinNT.h
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; #ifdef _WIN64 typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS; typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS; #else typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS; typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS; #endif
このファイルが PE ファイルであることを示すシグネチャであり、値はマジックナンバー 0x50450000("PE\0\0") です。これが 0x50450000 でなかった場合は PEフォーマットではなく別のフォーマットである、ということです。各フォーマットのシグネチャは WinNT.h で以下のように定義されています。
WinNT.h
#define IMAGE_DOS_SIGNATURE 0x4D5A // MZ #define IMAGE_OS2_SIGNATURE 0x4E45 // NE #define IMAGE_OS2_SIGNATURE_LE 0x4C45 // LE #define IMAGE_NT_SIGNATURE 0x50450000 // PE00
COFF の説明でも出てきた IMAGE_FILE_HEADER です。とは言え、使われ方は COFF のときと若干異なります。
WinNT.h
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; #define IMAGE_SIZEOF_FILE_HEADER 20
Machine | このファイルがどのマシンを対象として作られたものか表す数値です。WinNT.h に IMAGE_FILE_MACHINE_XXX として定義されています。x86 なら IMAGE_FILE_MACHINE_I386(0x014c) か IMAGE_FILE_MACHINE_UNKNOWN(0) になります。 |
---|---|
NumberOfSections | セクション数。 |
TimeDateStamp | ファイルのタイムスタンプ。 |
PointerToSymbolTable | PE ファイルはシンボルテーブルを含まないので 0 がセットされます。 |
NumberOfSymbols | |
SizeOfOptionalHeader | ファイルヘッダに続くオプショナルヘッダのサイズ。 |
Characteristics | 特性。いくつかのフラグの組み合わせ。重要なフラグを下記リストに挙げました。他のフラグや意味については仕様書を見て下さい。 |
フラグ | 値 | 解説 |
---|---|---|
IMAGE_FILE_RELOCS_STRIPPED | 0x0001 | 再配置情報を含んでおらず、オプショナルヘッダの ImageBase が示すアドレスにロードされなければいけないことを示します。ImageBase が示すアドレスが不正な場合や既に別のイメージがロードされている場合はロードに失敗します。 |
IMAGE_FILE_EXECUTABLE_IMAGE | 0x0002 | 必ずセットされていなければいけません。 |
IMAGE_FILE_32BIT_MACHINE | 0x0100 | 32ビットアーキテクチャのマシン。 |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 0x0400 | セットアッププログラム用。イメージがリムーバブル メディア上にある場合にはスワップ ファイルからコピーして実行します。 |
IMAGE_FILE_SYSTEM | 0x1000 | ファイルがシステムファイルであることを示します。ユーザプログラムではありません。 |
IMAGE_FILE_DLL | 0x2000 | ファイルがDLLであることを示します。 |
オプショナルヘッダという名前のくせに必須のヘッダです。COFF では不要なことからオプショナルという名前がついたらしいです。
フィールド数は多いものの重要な意味をもつフィールドはそんなに多くありません。重要なものにはリストで色をつけてあります。
WinNT.h
// // Optional header format. // typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic | マジックナンバー 0x10b(PE32), 0x20b(PE+), 0x107(ROM) のいずれかの値をとります。通常は 0x10b です。0x20b だと64bit 用のファイルになり、構造やフィールドのサイズが若干異なります。 |
---|---|
MajorLinkerVersion | この実行可能イメージをリンクしたリンカのバージョン。参考値であり、重要な値ではありません。 |
MinorLinkerVersion | |
SizeOfCode | コード領域のサイズ。参考値であり、重要な値ではありません。 |
SizeOfInitializedData | 文字列リテラルなどの値が確定しているデータ領域のサイズ。参考値であり、重要な値ではありません。 |
SizeOfUninitializedData | グローバル変数などの値が確定していないデータ領域のサイズ。参考値であり、重要な値ではありません。 |
AddressOfEntryPoint | エントリポイント(実行開始位置)のアドレス。RVA(relative virtual address:相対仮想アドレス)であらわされています。RVA とは実際のアドレスからイメージのロードアドレスを引いた値のことです。したがって、実際のエントリポイントのアドレスは後述の ImageBase に AddressOfEntryPoint を加算することで得られます。 なお、PE ファイル内で示されるアドレスの大半は RVA で示されます。 |
BaseOfCode | コード領域のベースアドレス。RVA で記述されています。参考値であり、重要な値ではありません。OllyDbg では AddressOfEntryPoint が BaseOfCode から BaseOfCode + SizeOfCode の範囲に入っていないと警告を出しますが。 |
BaseOfData | グローバル変数などの値が確定していないデータ領域のベースアドレス。RVA で記述されています。参考値であり、重要な値ではありません。 |
ImageBase | イメージファイルがロードされるアドレス。イメージファイルが再配置情報を持っていて、このアドレスへのロードに失敗した場合は、別のアドレスにロードされます。 Microsoft が提供する開発環境でのデフォルト値は EXE:0x00400000, DLL:0x10000000 ですが、リンカオプションなどで変更できます。 |
SectionAlignment | 各セクションがメモリに配置される時の境界。
例えば、セクションのサイズそのものが10byteでも、この値が 0x1000 だと次のセクションは 0x1000 に配置されます。また、サイズが 0x1010 の場合には 0x2000 となります。 各セクションはイメージベースで始まるプロセスのアドレス空間に順順にロードされていきます。 |
FileAlignment | ファイル内でセクションデータが配置される時の境界。境界までの隙間は 0 埋めされます。 |
MajorOperatingSystemVersion | 対象とされるオペレーティングシステムのバージョン。参考値であり、重要な値ではありません。 |
MinorOperatingSystemVersion | |
MajorImageVersion | このイメージのバージョン。参考値であり、重要な値ではありません。 |
MinorImageVersion | |
MajorSubsystemVersion | サブシステムのバージョン。参考値であり、重要な値ではありません。 |
MinorSubsystemVersion | |
Reserved1 | 使用しません。 |
SizeOfImage | イメージファイルをロードするために必要なサイズ。 まず最初にセクションが必要とするバイトを求め、それからページ境界に揃え、最終的に SectionAlignment 境界に揃えたサイズの合計から算出されます。 |
SizeOfHeaders | ファイルヘッダ、MS-DOSヘッダ、NT ヘッダ、セクションテーブル全てを含むヘッダのサイズ。 |
CheckSum | 全てのドライバやブート時にロードされるDLLなどのロード時に実行可能ファイルを確認するのに使われるチェックサム。 数値はリンカによってセット・確認されますが、この数値を計算するアルゴリズムは知的財産権で保護されており発表されていません。IMAGEHELP.DLL の中に計算するアルゴリズムがあります。 |
Subsystem | 実行可能ファイルのターゲットとなるサブシステム。このフィールドを IMAGE_SUBSYSTEM_WINDOWS_GUI(2) か IMAGE_SUBSYSTEM_WINDOWS_CUI(3) か確認することでコンソールプログラム(CUI)か、Windows プログラム(GUI)かが分かります。値を IMAGE_SUBSYSTEM_WINDOWS_GUI(2) から IMAGE_SUBSYSTEM_WINDOWS_CUI(3) に変更すると実行時に DOS 窓が表示されるようになったりします。IMAGE_SUBSYSTEM_NATIVE(1) の場合はデバイスドライバになります。 |
DllCharacteristics | DLL の特性をあらわすフラグの組み合わせ。大した意味を持ちません。 |
SizeOfStackReserve | スタック領域として予約されるサイズ。 |
SizeOfStackCommit | スタック領域としてコミットされるサイズ。 |
SizeOfHeapReserve | ヒープ領域として予約されるサイズ。 |
SizeOfHeapCommit | ヒープ領域としてコミットされるサイズ。 |
LoaderFlags | 現在では使われていません。 |
NumberOfRvaAndSizes | イメージデータディレクトリ(IMAGE_DATA_DIRECTORY)の数。普通は IMAGE_NUMBEROF_DIRECTORY_ENTRIES(16)。 |
DataDirectory | IMAGE_DATA_DIRECTORY 構造体の配列で、インポート情報やエクスポート情報、再配置情報などの重要なデータへの RVA とサイズが記述されています。配列のインデックスによって意味が決まっています。 詳しい説明は別の機会に行います。 |
オプショナルヘッダの直後には、セクションテーブルが続きます。基本的に COFF と同じです。
セクションというのはコードや文字列リテラル、変数などその分類ごとに分けられた区画のことです。例えば、コードは読取専用で実行可能な特性をもつセクションに、文字列リテラルやリソースデータは読取専用のセクションに、グローバル変数は読み書き可能なセクションに、といった具合に分けられて配置されます。セクションヘッダの数も COFF と同様にファイルヘッダの NumberOfSections に記述されています。ただし、セクションデータはセクションの数だけあるとは限りません。セクションヘッダの PointerToRawData と SizeOfRawData フィールドに値が入っている場合だけセクションデータが存在します。
セクションの特性やデータへのオフセットなどの情報が収められているのがセクションヘッダで、セクションヘッダの配列がセクションテーブルになります。
WinNT.h
#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; #define IMAGE_SIZEOF_SECTION_HEADER 40
Name | セクション名。".text" や ".idata" など慣例的に付けられる名前がありますが、名前に重要な意味はありません(COFF の時点では名前に重要な意味がある場合があります)。".text" に未初期化変数のセクションを割り当てたり、".idata" にコードを含むセクションを割り当てることもできます。 |
---|---|
VirtualSize | セクションのサイズ。 |
VirtualAddress | セクションの RVA 。 |
PointerToRawData | セクションが初期値となるデータを持つ場合に、ファイル内オフセットが格納されています。未初期化変数などデータが無いようなセクションは 0 が格納されます。 |
PointerToRelocations | PE ファイルでは使用しません。PE ファイルの場合、再配置情報はオプショナルヘッダのデータディレクトリからたどります。 |
PointerToLinenumbers | 行番号情報へのオフセット。行番号情報は古い形式のデバッグ情報です。最近の デバッグ情報は別の形式で生成されるため、通常は 0 です。 cl に /Z7 オプションをつけてコンパイルすると生成されます。 |
NumberOfRelocations | 再配置情報の数。PE ファイルでは常に 0 。 |
NumberOfLinenumbers | 行番号情報の数。 |
Characteristics | セクションの特性を表すフラグの組み合わせ。各フラグは IMAGE_SCN_XXXX として WinNT.h に定義されています。 たとえば、コードが配置されるセクションなら、 コードを含む IMAGE_SCN_CNT_CODE フラグ、読取専用を示す IMAGE_SCN_MEM_READ フラグ、実行可能であることを示す IMAGE_SCN_MEM_EXECUTE フラグがセット されます。 |