プログラムの説明
メモリーマップトファイルの利用
DbgHelpを利用してイメージを操作するには、いくつかの方法があります。一番簡単なのは、DbgHelpのMapAndLoad
関数を利用することですが、ここでは通常のファイルとしてオープンし、マップする方法を利用します。
メモリーマップトファイルを利用すると、ファイルからバッファにシーケンシャルにデータを読み込むのと異なり、見かけ上連続したメモリー上でファイル内の移動ができます。読み込み専用で利用する分には、通常のファイルより簡単に扱えるため、ぜひとも呼び出し方法を覚えておきましょう。
ソースファイルの説明
ソースファイルは、ヘッダファイルのプリコンパイル用の「stdafx.cpp」、利用するヘッダを定義した「stdafx.h」、そしてプログラム本体である「showexports.cpp」の3つのファイルから構成されています。
「showexports.cpp」は、2つの部分に分かれています。
- ファイルの前半は、
DllFile
というクラスの定義です。 - ファイルの後半は、
main
関数とエラー出力とDllFile
を呼び出す関数の実装です。
DllFile
は、オープン済みファイルハンドルをコンストラクタで受け取り、エクスポートしている関数を列挙するメソッドを提供します。main関数
int _tmain(int argc, _TCHAR* argv[]) { if (argc != 2) { fprintf(stderr, "usage: proxygen dll\n"); return 1; } HANDLE hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile != INVALID_HANDLE_VALUE) { try { DllFile file(hFile); file.EnumExports(printExport); } catch (LPTSTR ploc) { print_error(ploc); } CloseHandle(hFile); } else { print_error(_T("Open")); } return 0; }
main
では、与えられた引数を利用してDLLをオープンします。成功した場合、DllFile
クラスのインスタンスを作成し、EnumExports
メソッドを呼び出して関数名を表示します。
printExport関数
static BOOL printExport(WORD ordinal, LPSTR pfun) { printf("%d fname=%s\n", ordinal, pfun); return TRUE; }
printExport
関数は、DllFile#EnumExports
メソッドへの引数としてmain
関数で指定している関数です。ここでは、単に序数と関数名を標準出力へ出力します。
なお、DllFile#EnumExports
はメソッドの引数として与えられた関数を、エクスポートしている関数の序数と関数名を引数にして呼び出すメソッドです。
print_error関数
static void print_error(LPTSTR loc) { DWORD error = GetLastError(); if (!error) { _ftprintf(stderr, _T("%s error\n"), loc); } else { LPTSTR lpMsgBuf = NULL; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 32, NULL); _ftprintf(stderr, _T("%s error: %s\n"), loc, lpMsgBuf); LocalFree(lpMsgBuf); } }
DllFile
クラスが処理中にスローした例外などによって呼び出されるエラー表示関数です。
GetLastError
の結果が0の場合は、DllFile
がDLL以外のファイルをコンストラクタへ与えたなどの理由でスローしたエラーなので、単にそのメッセージを表示します。
GetLastError
の結果が0以外の場合は、API呼び出しでエラーになったことを意味するため、FormatMessage
関数でエラーメッセージを取り出して表示します。
DllFile::DllFile
DllFile(HANDLE hFile) : pFile(NULL), hMap(NULL) { HANDLE h = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (!h) { throw _T("CreateMapping"); } LPBYTE pfbase = (LPBYTE)MapViewOfFile(h, FILE_MAP_READ, 0, 0, 0); if (!pfbase) { CloseHandle(h); throw _T("Mapping View"); } check_coff(pfbase); hMap = h; pFile = pfbase; }
CreateFileMapping
関数を呼び出してメモリーマップトファイルを作成します。次に、MapViewOfFile
関数を呼び出して実際にマッピングし、先頭のポインタを取得します。最後に、check_coff
メソッドを呼び出して、与えられたファイルがDLLのイメージかどうかを検証します。
COFF(Common Object File Format)とは、PEファイルより前にマイクロソフト社が利用していたイメージ/オブジェクトファイルのフォーマットです。PEファイルは、COFFのファイルヘッダ構造体をcheck_coff
メソッドでの検証対象であるIMAGE_FILE_HEADER
構造体として利用しているため、ここでは「check_coff」という名称を利用しています。
すべてが完了したら、ハンドルなどをインスタンス変数に保持します。ここで保持したハンドルなどはデストラクタで解放します。
DllFile::~DllFile
virtual ~DllFile() { if (pFile) { UnmapViewOfFile(pFile); } if (hMap) { CloseHandle(hMap); } }
デストラクタでは、コンストラクタで設定したメモリーマップトファイルのハンドルなどのリソースを解放します。解放が必須なリソースの処理を行っているため、派生クラスで横取りされないようにvirtual
として定義します。
なお、UnmapViewOfFile
関数の引数の値は、MapViewOfFile
の結果と一致している必要がある点には気をつけてください。例えば、処理の都合でpFile++
というように直接ポインタを操作する場合には、元のポインタ値を別に保存しておきUnmapViewOfFile
関数に与えるようにします。
DllFile::check_coffメソッド
void check_coff(LPBYTE pfbase) { PIMAGE_NT_HEADERS pnt = ImageNtHeader(pfbase); if (pnt->Signature != *(DWORD*)PEMARK) { throw _T("bad signature"); } if (pnt->FileHeader.Machine != IMAGE_FILE_MACHINE_I386 || pnt->FileHeader.SizeOfOptionalHeader == 0 || (pnt->FileHeader.Characteristics & DLL_CHARACTERISTIC) != DLL_CHARACTERISTIC || pnt->OptionalHeader.Magic != PE32) { throw _T("bad module"); } }
このメソッド内で呼び出しているImageNtHeader
関数は、ファイルの先頭ポインタを与えるとDOSスタブなどをスキップしてIMAGE_NT_HEADERS
構造体をポイントして返すDbgHelpのAPIです。
ここではシグネチャのPE\0\0 4バイトおよび、マシン種別、イメージかどうか、DLLかどうかといった条件を検証します。
DllFile::EnumExportsメソッド
int EnumExports(BOOL (*func)(WORD, LPSTR)) { ULONG size; PIMAGE_SECTION_HEADER psc; PIMAGE_NT_HEADERS pnt = ImageNtHeader(pFile); PIMAGE_EXPORT_DIRECTORY ped = (PIMAGE_EXPORT_DIRECTORY) ImageDirectoryEntryToDataEx(pFile, FALSE, IMAGE_DIRECTORY_ENTRY_EXPORT, &size, &psc); if (!ped) { throw _T("no exported functions"); } LPCSTR pname = (LPCSTR)ImageRvaToVa(pnt, pFile, ped->Name, &psc); ULONG* ppfuname = (ULONG*)ImageRvaToVa(pnt, pFile, ped->AddressOfNames, &psc); WORD* pordinal = (WORD*)Ima geRvaToVa(pnt, pFile, ped->AddressOfNameOrdinals, &psc); for (DWORD i = 0; i < ped->NumberOfNames; i++, ppfuname++, pordinal++) { LPSTR pfun = (LPSTR)ImageRvaToVa(pnt, pFile, *ppfuname, &psc); if (!func(*pordinal + (WORD)ped->Base, pfun)) { return -((int)i + 1); } } return ped->NumberOfNames; }
エクスポートされた序数と関数名を列挙して、引数で与えられた関数を呼び出します。
ImageDirectoryEntryToDataEx
関数は、IMAGE_OPTIONAL_HEADER
構造体から指定されたインデックスのディレクトリ情報(PEファイル内の特定データの位置を示す情報)を返すDbgHelpのAPIです。
ここでは、エクスポート情報のディレクトリ情報であるIMAGE_DIRECTORY_ENTRY_EXPORT
を指定して呼び出します。最後の引数のpsc
は、関連するIMAGE_SECTION_HEADER
構造体をポイントさせるために指定します。ここで返されたIMAGE_SECTION_HEADER
を次に呼び出しているImageRvaToVa
関数に与えると、RVA変換処理が多少高速化されます。
以降、関数名の配列(IMAGE_EXPORT_DIRECTORY
構造体のAddressOfNames
フィールド)、序数の配列(IMAGE_EXPORT_DIRECTORY
配列のAddressOfNameOrdinals
フィールド)のRVAから実際の仮想アドレス(VA)をImageRvaToVa
関数を呼び出して求めます。
最後に、関数名を同様にImageRvaToVa
関数で取り出して、序数と共に引数で与えられたユーザー関数を呼び出します。なお、序数のみが定義されていて関数名が定義されていないDLLでは、IMAGE_EXPORT_DIRECTORY
構造体のNumberOfNames
フィールドの値よりNumberOfFunctions
フィールドの値の方が大きくなります。
ImageRvaToVa
関数を呼び出す必要はありません。ここでの例のように、すべてのRVAが同一のIMAGE_SECTION_HEADER
構造体によってポイントされているセクション内に含まれている場合は、バイアスを1個見つければ他のRVAに対してもそれを適用することができるからです。RVAからVAへの変換方法については、参考資料のJay Krell氏の投稿を参照してください。
まとめ
PEファイルはプログラムですが、その内容をプログラムから直接データとして扱えます。これらの情報を適切に利用することで、C#のようにリッチなメタデータを持たないC/C++であっても、それなりの情報を得てメタな操作を行うことが可能となります。
イメージからどのような処理を作ることができるかは、プログラムの課題として興味深いものがあります。しかもWindowsでは、ここで紹介したDbgHelpの他にもImageHlpなどのライブラリが提供されているため、イメージを直接扱うことはそれほど困難ではありません。
イメージを扱うプログラムを作成する時に、本記事が少しでも参考になれば幸いです。
参考資料
- WHDC 『Microsoft Portable Executable and Common Object File Format Specification』、2006年5月
- 「Visual Studio, Microsoft Portable Executable and Common Object File Format Specification
- MSDN Library
- 『DOTNETメーリングリストへのJay Krell氏の投稿』