PE(Portable Executable)ファイルフォーマット その1 ファイルの基本構造

 PE ファイルに関していまさら私が語るようなことなど一つもないんですが、リンカもどきを作る上では避けては通れないと思ったので・・・。しばらく PE ファイルの構造や各種データの構造や取り出し方を説明したいと思います。

PEファイルフォーマットって何?

 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 でロードできるファイルということになります。あとはカーネルから読み込まれるデバイスドライバなんかも。
 で、先ほどは拡張子が EXE や DLL と言いましたが実行可能ファイルであることと拡張子が何であるかはローダには関係ありません。ローダにとって重要なのはファイルの中身です。ローダはファイルの中身を見て、それが DLL なのか EXE なのかそれとも他のファイルなのか、といった判断を行います。例えば EXE や DLL の拡張子を bin などに変更しても、CreateProcess() や LoadLibrary() でロードすることが可能です。
 ローダが「このファイルは実行可能ファイルだ」と認識できる様式(フォーマット)はいくつかあって、現在主流となっているのが PE ファイルフォーマットです。他にも LE フォーマットや NE フォーマットなんかがありますが今では絶滅危惧種となってしまいました。私は NE フォーマットでかかれたイメージファイルなら、古い古い自己解凍書庫 EXE で見たことがあります。それぐらい PE ファイルばかりになってしまいました。

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


 上の画像は PE ファイルの構造を表しています。大きく分ければ「MS-DOS 用ヘッダおよびプログラム」、「NT ヘッダ」、「セクションテーブルおよびセクションデータ」の3つの単純な構造です。インポートやエクスポート、リソースなどの構造まで話をすすめると若干ややこしくはなりますが。

では、一番上から順に見ていきましょう。

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 です。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;

 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

 Signature(シグネチャ), FileHeader(ファイルヘッダ), OptionalHeader(オプショナルヘッダ)の3つを持っています。

シグネチャ

 このファイルが 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_STRIPPED0x0001 再配置情報を含んでおらず、オプショナルヘッダの ImageBase が示すアドレスにロードされなければいけないことを示します。ImageBase が示すアドレスが不正な場合や既に別のイメージがロードされている場合はロードに失敗します。
IMAGE_FILE_EXECUTABLE_IMAGE0x0002 必ずセットされていなければいけません。
IMAGE_FILE_32BIT_MACHINE0x0100 32ビットアーキテクチャのマシン。
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP0x0400 セットアッププログラム用。イメージがリムーバブル メディア上にある場合にはスワップ ファイルからコピーして実行します。
IMAGE_FILE_SYSTEM0x1000 ファイルがシステムファイルであることを示します。ユーザプログラムではありません。
IMAGE_FILE_DLL0x2000 ファイルが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 ヘッダ、セクションテーブル全てを含むヘッダのサイズ。SectionAlignment FileAlignment の倍数である必要があります。- 2010/06/02 修正
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 とサイズが記述されています。配列のインデックスによって意味が決まっています。
 詳しい説明は別の機会に行います。
 AddressOfEntryPoint の項でも説明していますが、アドレスのほとんどの値は RVA(relative virtual address:相対仮想アドレス)であらわされているので注意してください。
 確か、RVA で書かれていないアドレスは ImageBase と TLS ディレクトリのフィールドだけだった筈です。

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

 オプショナルヘッダの直後には、セクションテーブルが続きます。基本的に COFF と同じです。

 セクションというのはコードや文字列リテラル、変数などその分類ごとに分けられた区画のことです。例えば、コードは読取専用で実行可能な特性をもつセクションに、文字列リテラルやリソースデータは読取専用のセクションに、グローバル変数は読み書き可能なセクションに、といった具合に分けられて配置されます。
 セクションの特性やデータへのオフセットなどの情報が収められているのがセクションヘッダで、セクションヘッダの配列がセクションテーブルになります。
 セクションヘッダの数も COFF と同様にファイルヘッダの NumberOfSections に記述されています。ただし、セクションデータはセクションの数だけあるとは限りません。セクションヘッダの PointerToRawData と SizeOfRawData フィールドに値が入っている場合だけセクションデータが存在します。
 あと、COFF のときはセクションが細かく分かれていて、一つのファイルにいくつものセクションがありましたが、PE ファイルでは同じ名前や特性をもつセクションは一つのセクションにまとめられてしまいます。ですので、普通はセクション数が2桁いくことはありません。

 セクションヘッダは IMAGE_SECTION_HEADER として WinNT.h に定義されています。
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 フラグがセット されます。

最後に

 基本はこれで終わりです。サンプルプログラムもなく、この知識がどう役に立つのか良く分からないままつらつらと読まなければいけないのでつまらなかったんじゃないでしょうか。
 次回からインポート関数の列挙や API フック、エクスポート関数の列挙や GetProcAddress() の実装などを行っていきます。