読者です 読者をやめる 読者になる 読者になる

PE ファイルについて (1) - IMAGE_DOS_HEADER

この記事は Windows & Microsoft技術 基礎 Advent Calendar 2015 の 20 日目です。*1
他の人が書かなそうなネタということで、Windows 実行ファイルで攻めてみることにします。

PE ファイルの概要

Windows で使われる EXE ファイルは、PE (Portable Executable) というフォーマットです。
Portable という名前の通り、同じフォーマットのファイル(同じファイルではありません)が、様々なプラットフォーム上で動きます。
PC でも、スマホでも、Xbox でも、どこでも PE ファイルが使われています。

PE ファイルは、大ざっぱに言うと、以下のような構造をしています。

MS-DOS 領域
NT ヘッダー
セクション データ
その他のデータ

MS-DOS 領域

PE ファイルの先頭には、MS-DOS 領域があります。
適当なファイルをテキスト エディターで開くと、ファイルの先頭付近に "This program cannot be run in DOS mode." という文字列が見えます。
f:id:aetos382:20151221003924p:plain
MS-DOS 領域とは、この EXE を MS-DOS 上で実行したときに、「このプログラムは DOS では実行できません(Windows で実行してね)」というメッセージを表示するだけの小さな MS-DOS プログラムのことらしいです。
残念ながら若輩者のため、実際にこのメッセージが表示されるところを見たことはありません。
おそらく、Windows 1.0 ~ 3.1 の頃、まだ WindowsMS-DOS が混在して使用されていた頃の名残だと思います。

なお、このプログラムはリンカーオプション /STUB で差し替えることが可能です。

ファイルの先頭には "MZ" という文字が見えます。
これは、MicrosoftMS-DOS の設計に携わった Mark Zbikowski のイニシャルに由来すると言われ、このファイルが PE ファイルであると判断する時の証拠の一つです。

IMAGE_DOS_HEADER

ファイルの先頭付近にあるデータ構造は、IMAGE_DOS_HEADER という構造体で表されます。
これは Windows SDK に含まれる 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;

前述の通り、ここは MS-DOS プログラムの一部ですので、このヘッダーの内容は PE ファイルには直接の関係はありません。
重要なメンバーは 2 つだけです。

e_magic

最初の "MZ" にあたります。
これはやはり winnt.h で IMAGE_DOS_SIGNATURE という名前の定数として定義されています。

e_lfanew

これは、MS-DOS 領域の後に来る NT ヘッダーの位置を表しています。

メモリマップドファイルで見る

コマンドラインで指定された PE ファイルをメモリマップドファイルとして開いて見るサンプルです。
異常時の処理は全然していませんのでご注意ください。

int wmain(int argc, wchar_t * argv[])
{
	if (argc < 2)
	{
		return 1;
	}

	auto hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
	auto hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
	auto pvView = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);

	auto pDosHeader = static_cast<IMAGE_DOS_HEADER *>(pvView);
	if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		printf("Invalid MS-DOS signature.");
	}

	auto pNtHeaders = reinterpret_cast<IMAGE_NT_HEADERS *>(static_cast<LPBYTE>(pvView) + pDosHeader->e_lfanew);

	UnmapViewOfFile(pvView);
	CloseHandle(hFileMapping);
	CloseHandle(hFile);

	return 0;
}

なお、NT ヘッダーの位置を得るのには、ImageNtHeader という関数を使うこともできます。
長ったらしいキャストが要らないので、簡潔になるかもしれません。お好みでどうぞ。

今回はとりあえずここまでとさせて頂きます。
一日オーバーしている上に大したことない記事で恐縮ですが、何せ期限過ぎてますので…。年内にあと 2、3 回は書くので勘弁してください。

次からは NT ヘッダーを少しずつ見ていきます。

qiita.com

*1:今まで 21 日目だと思ってましたごめんー!