鷲ノ巣

C# とか PowerShell とか。当ブログの記事は(特に公開直後は)頻繁に改定される場合があることをご了承ください。

PE ファイルについて (6) - 補足

第 6 回です。
今まで適当に流してきたというか、言う機会を逸していたことを整理しておきます。
ちょっと雑多な内容になってしまいますが、お付き合いください。

モジュールハンドル=ベースアドレス

そうなのです。

これまで、何度かこんなコードを載せてきました。

// pvBase はメモリマップドファイルの先頭を指すポインターだとする
auto pDosHeader = static_cast<IMAGE_DOS_HEADER *>(pvBase);
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
	printf("Invalid MS-DOS signature.");
	return;
}

// ここが NT ヘッダー
auto pNtHeaders = reinterpret_cast<IMAGE_NT_HEADERS *>(static_cast<LPBYTE>(pvBase) + pDosHeader->e_lfanew);

この pvBase を得るために、第 1 回では CreateFileMapping 関数を使って PE ファイルをメモリ上にマップしていました。
この代わりに、LoadLibrary 等を使うこともできます。
つまり、こう。

HMODULE hModule = LoadLibrary("hoge.dll"); // 後で FreeLibrary しておいてくださいね
auto pvBase = reinterpret_cast<LPVOID>(hModule);

モジュールの読み込み方

これまでも何度か出てきましたが、PE ファイルの読み込み方には 2 種類あります。
単にバイナリ データとして読み込む方法と、実行するためにロードする方法です。
前者はもちろんファイルと同じデータが読み込まれますが、後者の場合、セクションが配置される位置がファイルと異なります(ヘッダーは同じです)。

バイナリ データとして読み込む場合、各セクションは IMAGE_OPTIONAL_HEADER::FileAlignment の倍数のアドレスから始まります。
その値は、具体的には IMAGE_SECTION_HEADER::PointerToRawData で示されます。
実行のためにロードした場合、各セクションは IMAGE_OPTIONAL_HEADER::SectionAlignment の倍数のアドレスに配置されます。
具体的な位置は、IMAGE_SECTION_HEADER::VirtualAddress によって示されます。

バイナリ データとして読み込む方法には、以下のようなものがあります。

  • CreateFileMapping 関数
  • ReadFile 関数

実行のためにロードする方法には、以下のようなものがあります。

  • LoadLibrary 関数

ただし、いずれにも例外があります。
CreateFileMapping に SEC_IMAGE フラグを付けると、実行のためのロードと同じセクション配置でファイル マッピング オブジェクトが作られます。
一方、LoadLibraryEx に LOAD_LIBRARY_AS_DATAFILE フラグを付けると、ファイルと同じレイアウトでロードされます。
ただし、LoadLibraryEx を LOAD_LIBRARY_AS_DATAFILE フラグ付きで呼び出した場合、データ ファイルとして読み込まれていることを示すフラグとして、HMODULE 値の最下位ビットが 1 になっています。
ベース アドレスとして使用する場合には、最下位ビットをマスクする必要があります。

残念ながら、ベース アドレスから、現在 PE ファイルがどちらの方法で読み込まれているのかを判断する方法は、おそらくありません。

実行のためのロード=メモリ マップド ファイル

実行のためにロードする場合、実際にはファイルはメモリ マップド ファイルとして読み込まれています。
その証拠に、マップされているファイル名を取得する GetMappedFileName という関数に HMODULE 値を渡すと、ファイル名が取得できます。

これは考えてみれば当然の話です。
ReadFile 関数などで読み込むと、メモリ上にファイルの内容のコピーが作られます。
KERNEL32.DLL など、多くのプロセスで読み込まれる DLL でそれをやってしまうと、各プロセスのメモリ空間に、同じ内容の PE ファイル イメージが重複して読み込まれてしまい、メモリを無駄にしてしまいます。
メモリ マップド ファイルであれば、同じファイルを多くのプロセスで読み込んでも、実際に物理メモリは消費しませんから、メモリの節約になります。

メモリ上のセクション レイアウトは、ファイルの時とは異なります。
おそらく内部的には、セクションごとに個別にメモリにマップしているのでしょう。
そのため、IMAGE_SECTION_HEADER の Characteristics 値には、CreateFileMapping のフラグに相当するものがあります。

通常、メモリ マップド ファイルを書き換えると、元になるファイル自体も書き換わってしまいます。
しかし、PE ファイルの内容はロードしても変わりません。
これは、メモリ マップド ファイルの Copy on Write という機能を使って実現されています。
Copy on Write とは、メモリ マップド ファイルを書き換えようとした時に初めて、その領域のコピーをメモリ上に作り、そのコピーを書き換えるという手法です。
これにより、元になる PE ファイルを書き換えることなく、また、読み取りしかしない多くの領域はメモリ上にコピーを作らないので、物理メモリを節約することができるわけです。

相対仮想アドレス

今後、相対仮想アドレス(Relative Virtual Address:RVA)という言葉を積極的に使っていきます。
これは、PE ファイルがロードされているベース アドレスからの相対アドレスという意味です。
ベース アドレスは必ずしも事前に想定した位置になるとは限らないので、どこにロードされても大丈夫なように、PE ファイル内では各種アドレスは基本的に相対アドレスで管理されています。

これに対して、ベース アドレス値を加算した、実際のデータのポインターを、仮想アドレス(Virtual Address:VA)と呼びます。
RVA から VA を得るためには、ImageRvaToVa という関数もあるのですが、単純な目的の割に引数が多く、また、LoadLibrary 等でロードしたメモリ イメージには使えないため注意が必要です。