この記事は 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." という文字列が見えます。
MS-DOS 領域とは、この EXE を MS-DOS 上で実行したときに、「このプログラムは DOS では実行できません(Windows で実行してね)」というメッセージを表示するだけの小さな MS-DOS プログラムのことらしいです。
残念ながら若輩者のため、実際にこのメッセージが表示されるところを見たことはありません。
おそらく、Windows 1.0 ~ 3.1 の頃、まだ Windows と MS-DOS が混在して使用されていた頃の名残だと思います。
なお、このプログラムはリンカーオプション /STUB で差し替えることが可能です。
ファイルの先頭には "MZ" という文字が見えます。
これは、Microsoft で MS-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 ヘッダーを少しずつ見ていきます。
*1:今まで 21 日目だと思ってましたごめんー!