EternalWindows
DLL / モジュールのマッピング

前節の最後で問題になったのは、EXEで呼び出している関数が実際にはDLLに実装されているため、 EXEを実行したときにエラーが発生するのではないかというものでした。 そこで今回は、関数を呼び出すということがそもそもどういうことなのかを考えてみたいと思います。

#include <windows.h>

void SampleFunction();

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	SampleFunction();

	return 0;
}

void SampleFunction()
{
}

このコードは、自作関数のSampleFunctionという関数を呼び出しています。 単純に考えれば、SampleFunctionの先頭アドレスにアクセスするということになります。 EXEを実行するとそのEXEはメモリにロードされますから、 SampleFunctionを実装しているコードは実際にメモリ上に存在します。 ですから、アクセスが失敗することはありません。 それでは、次のコードはどうでしょうか。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	MessageBox(NULL, TEXT("ボタンを押すと終了します。"), TEXT("OK"), MB_OK);

	return 0;
}

先の例と同じように、このコードはMessageBoxの先頭アドレスにアクセスします。 そして同じく先の例のように考えると、 MessageBoxを実装しているDLLがメモリにロードされていれば、 MessageBoxへのアクセスが失敗することはないはずです。 となると、関数へのアクセスであくまで重要となるのは、 その関数のコードが現在メモリに存在しているかどうかという話で、 EXEに含まれているかどうかは問題ではないということになりそうです。 この見解は確かに真理を得てはいるのですが、 DLLがメモリにロードされているからといって、必ずアクセスできるわけではありません。 アクセスするには、プロセスのアドレス空間にDLLをマッピングするという作業が必要なのです。

詳細は仮想メモリの章に譲りますが、 たとえばある変数のアドレスが0x0012fe94であったとしても、 本当に0x0012fe94というアドレスにその変数の値が格納されているわけではありません。 このアドレスは、プロセスが特定のメモリにアクセスするために使う仮想アドレスです (プロセスは、ここでは実行中のアプリケーションと解釈してください)。 1つのプロセスはページテーブルというものを持っており、 CPUはこのテーブルを参照することにより、仮想アドレスを物理アドレスに変換します。 つまり、実際のメモリのアドレスを取得してそこにアクセスします。

上記の要点は、プロセスがあるメモリにアクセスしたいのであれば、 そのメモリにアクセスするための仮想アドレスが必要となるということです。 DLLをマッピングすることによりプロセスは、 特定のアドレスを使ってDLLにアクセスすることが可能となります。 アクセス可能なアドレスの範囲が増えたと考えてもよいでしょう。 このアドレスの範囲というのが、アドレス空間です。 MessageBoxをエクスポートするDLL(user32.dll)がプロセスのアドレス空間にマッピングされていれば、 MessageBoxの呼び出しは可能になります。

マッピングという考えは、EXEファイルにも適用されています。 EXEファイルを起動するとは、1つのプロセスが作成されることです。 プロセスを作成するとき、システム(正確にはローダ)はそのプロセスのためにアドレス空間を作成し、EXEファイルをマッピングします。 これにより、プロセスは開始時からEXEファイルへのアクセスが可能となります。 マッピングされたアドレスは、次の関数で取得することができます。

HMODULE GetModuleHandle(
  LPCTSTR lpModuleName
);

lpModuleNameは、マッピングされているアドレスを取得したいモジュールの名前を指定します。 EXEやDLLはときとしてモジュールという言葉で表され、 それらのアドレスは特別にHMODULEという型で表されます。

GetModuleHandleにNULLを指定すると、EXEがマッピングされたアドレスが返されます。 多くの場合、返される値は0x00400000となるでしょう。 実はモジュールはベースアドレスというものを持っており、 そのモジュールがマッピングされるべきアドレスを含んでいます。 つまり、モジュールがマッピングされるアドレスは多くの場合決まっているのです。 EXEやDLLはイメージと呼ばれることもありますが、 アドレス空間にマッピングされた場合には、モジュールと呼ぶことが多いと思われます。 また、モジュールがエクスポートしている関数はシンボルと呼ばれたりします。 イメージという言葉は、ディスク上のEXEやDLLを表すときに使われます。

プロセスがDLLに実装されている関数を呼び出す場合は、 そのDLLをアドレス空間にマッピングしていなければなりません。 この作業も、プロセスの開始と共にローダが行っています。 EXEファイルには、インポートセクションと呼ばれるセクション(データ領域)が含まれています。 このセクションには、EXEがインポート(使用、取り込む)している関数が列挙されており、 その関数をエクスポートしているDLLも列挙されます。 ローダは、これらのDLLをアドレス空間にマッピングします。 また、DLLが内部で別のDLLによってエクスポートされている関数を呼び出している場合は、 それらのDLLもマッピングされることになります。 このように、DLLを動的にマッピングすることをダイナミックリンクと呼び、 特にアプリケーションの実行時にマッピングされることを暗黙的リンクと呼びます。

インスタンスハンドルの正体

WinMainの第1引数であるインスタンスハンドルは、 EXEファイルがマッピングされたアドレスを格納しています。 つまり、GetModuleHandleにNULLを指定したときに返る値と同一です。 このため、インスタンスハンドルを必要とする関数を呼び出すために、 無理にインスタンスハンドルをグローバルに宣言する必要はないのです。 GetModuleHandleにNULLを指定するだけで、インスタンスハンドルを取得することができます。 現に、WinMainを呼び出すWinMainCRTStarupという内部エントリポイントは、 WinMainの第1引数をGetModuleHandleの戻り値としているのです。 これらのことから、モジュールのアドレスはHMOULEでもHINSTANCEでも問題ありません。 モジュールのアドレスを取得している場合、GetModuleFileNameからモジュール名を取得することができます。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR szBuf[256];

	GetModuleFileName(hinst, szBuf, sizeof(szBuf) / sizeof(TCHAR));

	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

	return 0;
}

このコードを実行した場合、GetModuleFileNameが返す文字列はEXEファイルのフルパスであるはずです。 GetModuleFileNameに指定できるアドレスは、モジュールがマッピングされているアドレスのみですから、 正にhinstがEXEファイルのマッピングされたアドレスを表していることが分かります。



戻る