PE(Portable Executable)ファイルフォーマットの概要

PEファイルフォーマットとは

PEファイルフォーマットは Windows のローダが認識してくれる実行可能ファイルの主流フォーマットである。 「Windows のローダが認識してくれる実行可能ファイル」というのは拡張子が EXE, DLL などのファイルのことである。

PEファイルは、下図に示すように、 大きく分ければ「MS-DOS 用ヘッダおよびプログラム」、「NT ヘッダ」、 「セクションテーブルおよびセクションデータ」の3つの構造から成る。


PE ファイルフォーマットの構造

MS-DOS ヘッダ + MS-DOS Real−Mode Stub Program

 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 である。

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;

 Windows では MS-DOS ヘッダの最初の e_magic と最後の e_lfanew が重要な項目となる。

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 ヘッダ

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") である。

ファイルヘッダ

[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;
Machine
このファイルがどのマシンを対象として作られたものか表す。x86 なら 0x014c。
NumberOfSections
セクション数。
TimeDateStamp
ファイルのタイムスタンプ。
PointerToSymbolTable, NumberOfSymbols
PE ファイルはシンボルテーブルを含まないので 0 がセットされる。
SizeOfOptionalHeader
ファイルヘッダに続くオプショナルヘッダのサイズ。
Characteristics
特性。いくつかのフラグの組み合わせ。重要なフラグは下記リストの通り。
ファイルヘッダの特性フラグ(抜粋)。
フラグ解説
IMAGE_FILE_RELOCS_STRIPPED0x0001再配置情報を含んでおらず、オプショナルヘッダの ImageBase が示すアドレスにロードされなければいけないことを示します。ImageBase が示すアドレスが不正な場合や既に別のイメージがロードされている場合はロードに失敗します。
IMAGE_FILE_EXECUTABLE_IMAGE0x0002必ずセットされていなければいけない。
IMAGE_FILE_32BIT_MACHINE0x010032ビットアーキテクチャのマシン。
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP0x0400セットアッププログラム用。イメージがリムーバブル メディア上にある場合にはスワップ ファイルからコピーして実行します。
IMAGE_FILE_SYSTEM0x1000ファイルがシステムファイルであることを示す。ユーザプログラムではない。
IMAGE_FILE_DLL0x2000ファイルがDLLであることを示す。

オプショナルヘッダ

オプショナルヘッダという名前であるが PE では必須のヘッダである。  フィールド数は多いものの重要な意味をもつフィールドはそんなに多くない。 重要なものにはリストで色をつけてある。

[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 用のファイルになり、構造やフィールドのサイズが若干異なる。 0x010B
MajorLinkerVersion  この実行可能イメージをリンクしたリンカのバージョン。参考値であり、重要な値ではない。 0
MinorLinkerVersion 0
SizeOfCode  コード領域のサイズ。参考値であり、重要な値ではない。 0
SizeOfInitializedData  文字列リテラルなどの値が確定しているデータ領域のサイズ。参考値であり、重要な値ではない。 0
SizeOfUninitializedData  グローバル変数などの値が確定していないデータ領域のサイズ。参考値であり、重要な値ではない。 0
AddressOfEntryPoint エントリポイント(実行開始位置)のアドレス。 RVA(relative virtual address:相対仮想アドレス)であらわされる。 RVA とは実際のアドレスからイメージのロードアドレスを引いた値のことである。 したがって、実際のエントリポイントのアドレスは後述の ImageBase に AddressOfEntryPoint を加算することで得られる。
 なお、PE ファイル内で示されるアドレスの大半は RVA で示される。
0x11E0
BaseOfCode  コード領域のベースアドレス。RVA で記述されている。参考値であり、重要な値ではない。 0x1000
BaseOfData  グローバル変数などの値が確定していないデータ領域のベースアドレス。RVA で記述されている。 参考値であり、重要な値ではない。 0x2000
ImageBase  イメージファイルがロードされるアドレス。 イメージファイルが再配置情報を持っていて、このアドレスへのロードに失敗した場合は、別のアドレスにロードされる。 Microsoft が提供する開発環境でのデフォルト値は EXE:0x00400000, DLL:0x10000000 である。 0x00400000
SectionAlignment  各セクションがメモリに配置される時の境界。 例えば、セクションのサイズそのものが10byteでも、この値が 0x1000 だと次のセクションは 0x1000 に配置される。 また、サイズが 0x1010 の場合には次のセクションは 0x2000 となる。 0x1000
FileAlignment  ファイル内でセクションデータが配置される時の境界。境界までの隙間は 0 で埋められる。 0x0200
MajorOperatingSystemVersion  対象とされるオペレーティングシステムのバージョン。参考値であり、重要な値ではない。 4
MinorOperatingSystemVersion0
MajorImageVersion  このイメージのバージョン。参考値であり、重要な値ではない。 0
MinorImageVersion0
MajorSubsystemVersion  サブシステムのバージョン。参考値であり、重要な値ではない。 4
MinorSubsystemVersion0
Reserved1  使用しない。 0
SizeOfImage  イメージファイルをロードするために必要なサイズ。 まず最初にセクションが必要とするバイトを求め、それからページ境界に揃え、 最終的に SectionAlignment 境界に揃えたサイズの合計から算出される。 0x3000
SizeOfHeaders ファイルヘッダ、MS-DOSヘッダ、NT ヘッダ、セクションテーブル全てを含むヘッダのサイズ。 FileAlignment の倍数である必要がある。 0x0200
CheckSum  全てのドライバやブート時にロードされるDLLなどのロード時に実行可能ファイルを確認するのに使われるチェックサム。
 数値はリンカによってセット・確認されるが、 この数値を計算するアルゴリズムは知的財産権で保護されており発表されていない。 IMAGEHELP.DLL の中に計算するアルゴリズムがある。
0
Subsystem  実行可能ファイルのターゲットとなるサブシステム。Windows APP のとき 2、Console APP のとき 3。 デバイスドライバのとき 1 になる。 3
DllCharacteristics  DLL の特性をあらわすフラグの組み合わせ。大した意味を持ちません。 0
SizeOfStackReserve  スタック領域として予約されるサイズ。 0x100000
SizeOfStackCommit  スタック領域としてコミットされるサイズ。 0x001000
SizeOfHeapReserve  ヒープ領域として予約されるサイズ。 0x100000
SizeOfHeapCommit  ヒープ領域としてコミットされるサイズ。 0x001000
LoaderFlags  現在では使われていません。 0
NumberOfRvaAndSizes  イメージデータディレクトリ(IMAGE_DATA_DIRECTORY)の数。普通は IMAGE_NUMBEROF_DIRECTORY_ENTRIES(16)。 0x0010
DataDirectory  IMAGE_DATA_DIRECTORY 構造体の配列で、 インポート情報やエクスポート情報、再配置情報などの重要なデータへの RVA とサイズが記述されている。 配列のインデックスによって意味が決まっている。
後述

セクションテーブルとセクションデータ

オプショナルヘッダの直後には、セクションテーブルが続く。

[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" など慣例的に付けられる名前があるが、名前に重要な意味はない。
VirtualSize
セクションのサイズ。
VirtualAddress
セクションの RVA 。
PointerToRawData
セクションが初期値となるデータを持つ場合に、ファイル内オフセットが格納されている。 未初期化変数などデータが無いようなセクションは 0 が格納される。
PointerToRelocations
PE ファイルでは使用しない。
PointerToLinenumbers
行番号情報へのオフセット。行番号情報は古い形式のデバッグ情報です。 最近の デバッグ情報は別の形式で生成されるため、通常は 0 。
NumberOfRelocations
再配置情報の数。PE ファイルでは常に 0 。
NumberOfLinenumbers
行番号情報の数。
Characteristics
セクションの特性を表すフラグの組み合わせ。各フラグは IMAGE_SCN_XXXX として WinNT.h に定義されている。

PEヘッダ生成プログラム

void writeHeader(int app) {	// 2:gui, 3:console
    IMAGE_DOS_HEADER dosHeader = { 	// 0x0000..0x003F   64 byte
         0x5A4D, 0x0090, 0x0003, 0, 4,       // "MZ"
         0x0000, 0xFFFF, 0x0000, 0x00B8,     // minalloc, maxalloc, ss, sp
         0, 0, 0, 0x40, 0, 0,0,0,0, 0, 0, 0,0,0,0,0, 0,0,0,0,0,
         0x0080,          // PEヘッダの位置
    };                                       // 0x0040..0x0078
    char *dos = "\x0E\x1F\xBA\x0E\x00\xB4\x09\xCD\x21\xB8\x01\x4C\xCD\x21"
                "This program cannot be run in DOS mode.\r\r\n$";
    IMAGE_NT_HEADERS peHeader = {            // 0x0080..0x0177
        0x00004550,                          // "PE\0\0"
        {   // IMAGE_FILE_HEADER;
             0x014c, 0x0002,                 // Intel 386以降, セクション数
            (DWORD)time(NULL), 0, 0,         // EXEファイル作成日時
            sizeof(IMAGE_OPTIONAL_HEADER),   // 0x00E0
            0x030F 
        }, 
        {   // IMAGE_OPTIONAL_HEADER32
             0x010B, 0x06, 0x00,             // Magic, Linker Version
             0, 0, 0, entryPoint,            // AddressOfEntryPoint
             memCodeAddr, memDataAddr, base, // Base of Code, Data, ImageBase
             0x00001000, 0x00000200,         // Section-, File-Alignment
             4, 0,  0, 0,  4, 0, 0,          // OS, Image, Subsystem Version
             sizeImage, 0x00000200, 0,       // Image size, Header size
             3, 0,                           // 2/3: gui/console, DllCharacteristics
             0x00100000, 0x1000, 0x00100000, 0x1000, // stack & heap reserve, commit
             0, 0x00000010,
        }   // IMAGE_DATA_DIRECTORY配列の初期化は後で行う 
    };  //-------------- peHeader の初期化終了 ------------------
    IMAGE_SECTION_HEADER headerText = {      // 0x0178
         ".text\0\0", {memCodeSize}, memCodeAddr, rawCodeSize, rawCodeAddr,
         0, 0, 0, 0, 0x60000020         // Code | Executable | Readable
    };
    IMAGE_SECTION_HEADER headerData = {      // 0x1A0
         ".data\0\0", {memDataSize}, memDataAddr, rawDataSize, rawDataAddr,
         0, 0, 0, 0, 0xC0000040         // Initialized Data | Readable | Writeable
    };
    // IMAGE_DATA_DIRECTORY配列の初期化
    IMAGE_DATA_DIRECTORY *imp = peHeader.OptionalHeader.DataDirectory;
    imp[1].VirtualAddress = memImptAddr;
    imp[1].Size = 20 * nDLL + 20;         
    imp[12].VirtualAddress = memImptAddr+imp[1].Size; // Import Address Table
    imp[12].Size = dllFuncs * 4;
    peHeader.OptionalHeader.Subsystem = app;
    writeExe(&dosHeader, sizeof(dosHeader)); // 64 byte
    writeExe(dos, 57);                       // 57 byte
    writeExeAt(0x80, &peHeader, sizeof(peHeader));
    writeExe(&headerText, sizeof(headerText));
    writeExe(&headerData, sizeof(headerData));
}

参考文献

  1. 技術/Windows/PE(Portable Executable)フォーマットの実験