EternalWindows
exeファイルとCRT

ここでは、exeファイルのサイズを軽くする方法を項目別に説明します。 この作業は事実上、開発環境の設定を適切に変更するだけで行うことができますが、 本質的な部分を理解するためにも、様々なコードと概念を交えて説明することにします。 難易度としては、DLLの知識が必要になることから高いといえますが、 非常に実用的な話題なので、是非とも理解してください。

CRTとリンク

多くの開発環境は、単純にプログラムのコンパイルとリンクの機能を提供するだけでなく、 プログラムの開発を手助けするための標準関数を用意しています。 これらC言語の標準関数は、正式にはCRT(C Runtim Library)と呼ばれ、 printfやstrlen、fopenなどの一連の関数の実装を指しています。 CRTのソースコードはVisual C++ 2010であれば、 Microsoft Visual Studio 10.0/VC/crt/srcのパスから確認することができます。

exeファイルのサイズを軽くするためには、CRTにダイナミックリンクするところから始まります。 CRTにスタティックリンクした場合は、exeファイルにCRTのソースが格納されてサイズが大きくなりますが、 ダイナミックリンクの場合は動的にDLLをロードすることになりますから、 exeファイルのサイズが増えることはありません。 ただし、CRTにダイナミックリンクするためには、exeファイルを起動するときに必ずCRTのDLLが存在していなければなりません。 Visual C++ 2010で作成されたexeファイルは、msvcr100.dllというCRTにダイナミックリンクすることになっていますが、 このCRTがexeファイルと同じフォルダやsystem32フォルダに存在しない場合は、exeファイルを起動することができなくなります。 msvcr100.dllは、Visual C++ 2010がインストールされている環境において存在するものであるため、 他のPCにもmsvcr100.dllが存在するだろうと考えることはできません。

上記の要点は、CRTのコードをexeファイルに格納したくないものの、 かといってCRTにダイナミックリンクすると、exeファイルの起動がCRTの存在に依存してしまうから困るというものです。 しかし、exeファイルがCRTの関数を一切呼び出すことがなかったら、 exeファイルがCRTにリンクする必要はないはずですから、CRTに依存するという問題は解決することができます。 よって、exeファイルのサイズを軽くして、さらにCRTに依存しないようにするためには、 CRTの関数を呼び出さないようにするのが大原則になります。 CRTを使用する利点というのは、あくまでプログラムにOS依存のコードを加えず移植性を高めるというものですから、 CRTが存在しなければ特定の機能を使用できなくなるようなことはありません。 たとえば、ファイルのオープンはfopenではなくCreateFileで可能ですし、 メモリの確保はmallocではなくHeapAllocで可能ですから、 CRTを使えなくては困るという状況はあまりないはずです。

CRTとマルチスレッドDLL

それでは、具体的な作業に入っていきましょう。 まず、CRTをスタティックリンクではなく、ダイナミックリンクするように開発環境を設定します。 次のダイアログは、ソリューションエクスプローラのプロジェクトのメニューから、 プロパティを選択して表示することができます。

ランタイムライブラリの欄で、「マルチスレッド DLL (/MD)」を指定するようにします。 これにより、CRTのコードがexeファイルに格納されることはなくなります。 ダイナミックリンクにはCRTの存在に依存するという問題がありましたが、 次のようにCRTの関数を一切呼び出さないソースコードを記述すれば、 ダイナミックリンクは発生しないはずです。

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

見て分かるように、このコードではfopenなどのCRT関数は一切呼び出していません。 それでは、このコードをReleaseモードでビルドしてexeファイルを作成し、 exeファイルのIAT(Import Address Table)を調べてみましょう。 IATにはexeファイルが呼び出している関数と、その関数を実装しているDLLの名前が記述されるため、 これを調べることでCRTが本当にリンクされていないかどうかを確認することができます。 IATを調べるアプリケーションとしては、うさみみハリケーンというソフトを使用させていただいています。 次の図は、目的のexeファイルを起動して、そのプロセスのインポート関数を調べた結果です。

当初の予想とは裏腹に、exeファイルには何故かmsvcr100.dllがリンクされるようになっており、msvcr100.dllに依存するという問題を解決できていません。 このことから分かるように、どうやらCRTをダイナミックリンクしないためには、CRTの関数を呼び出さないだけでは不十分であるようです。 ランタイムライブラリの欄で、「マルチスレッド (/MT)」を指定していた場合はmsvcr100.dllへのダイナミックリンクは行われませんが、 この場合はCRTがスタティックリンクされるため、exeファイルにCRTのコードが格納されてしまいます。 これではexeファイルのサイズが重くなってしまいますから、当初の目的を達成しているとはいえません。

CRTと__tmainCRTStartup

上図で確認したIATには_commodeなどの関数が記述されていましたが、このような関数が記述される理由はただ1つです。 それは、exeファイルがmsvcr100.dllに実装されている_commodeを呼び出したからです。 しかし、先に示したソースコードにはそのような関数の呼び出しはなかったはずです。 また、CRTでないGetSystemTimeAsFileTimeなどの関数を呼び出した覚えもありませんが、 何故このような呼び出していない関数がIATに格納されているのでしょうか。 理由は、アプリケーションのエントリポイントが既定でWinMainではなく、 CRTの__tmainCRTStartup(crtexe.cに記述)になっているからです。

__tmainCRTStartupの主な作業はCRTの初期化及び、WinMain関数の呼び出しです。 たとえば、CRTのmallocという関数は内部でHeapAllocを呼び出すことになっていますが、 そのときに指定するヒープハンドルは、__tmainCRTStartupが呼び出している関数内で初期化されることになっています。 また、スレッドの同期を行うために必要なTLSの初期化を行い、 SEHを張ることで例外の発生をキャッチするようにもしています。 さらに、グローバルに宣言されたC++オブジェクトのコンストラクタを初期化し、 stdlib.hに定義されている_winmajorのようなグローバル変数も初期化します。 これらの処理は、CRTを利用しないにしても有用といえるものが含まれていますが、 いずれにしてもアプリケーションがCRTを利用するつもりのないのならこの関数の存在は必須ではありませんから、 エントリポイントを明示的にWinMainに設定しても問題はありません。 また、この設定によりCRTに内部的に呼ばれていたWindows APIの呼び出しもカットされることになりますから、 exeファイルのIATはプログラムが呼び出しているWindows APIのみで構成されることになります。

アプリケーションのエントリポイントを設定するには、「エントリ ポイント」という項目にWinMainを指定します。

これにより、このアプリケーションでは__tmainCRTStartupが呼ばれないようになりますから、 この関数におけるCRT関連の処理が行われることはなくなります。 後は、コード上でCRTの関数を1つも呼び出していなければ、CRTがリンクされることはないように思えますが、 まだ1つ考慮すべき点が残っています。 実は、次に示すバッファーセキュリティチェックの項目が「はい」になっているときは、 そのセキュリティチェックのためのCRTコードが暗黙のうちに埋め込まれるため、 必ず「いいえ」に設定しなければなりません。

このようなコード上の問題は、プログラム内でSEHを利用するときにも発生し、 仮想関数による関数のオーバーライドでも発生することが知られています。 また、newやdeleteといった演算子もCRTのコードを発生させるため、 C++特有の一部の機能は制限されていると考えてよいでしょう。 つまり、CRTをリンクしないというのは、CRTの関数を呼び出さないということのみならず、 それを間接的に生じさせるコードもカットしなければならないことを意味しているのです。

それでは改めて、CRTをリンクしないアプリケーションの例を示します。 注意すべきことは、コード内でCRTの関数を呼び出してはならない点と、 エントリポイントが__tmainCRTStartupではなくWinMainになっているため、 必要となるWinMainの引数はコード上で明示的に初期化しなければならないという2点です。

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR       szAppName[] = TEXT("sample");
	HWND        hwnd;
	MSG         msg;
	WNDCLASSEX  wc;
	STARTUPINFO si;

	hinst = GetModuleHandle(NULL);

	si.dwFlags = 0;
	GetStartupInfo(&si);
	nCmdShow = si.dwFlags  & STARTF_USESHOWWINDOW ? si.wShowWindow : SW_SHOWDEFAULT;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	ExitProcess(0);
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

一般のWindowsアプリケーションではウインドウを作成することから、 ウインドウクラスの登録にhinstが、ウインドウを表示するためにnCmdShowが必要となります。 これらはそれぞれ、GetModuleHandleとGetStartupInfoで初期化することができます。 場合によっては、lpszCmdLineが必要となる場合があるかもしれませんが、 そのようなときはGetCommandLineを呼び出すとよいでしょう。 __tmainCRTStartupは、このGetCommandLineが返す文字列にexeファイルの名前が 含まれないように加工してWinMainのlpszCmdLineに指定しているため、 lpszCmdLineをGetCommandLineの戻り値で初期化するアプリケーションは、 その文字列にexeファイルの名前が含まれることを忘れてはいけません。 また、アプリケーションを終了する際には、終了コードをretuenで返すのではなく、 終了コードをExitProcessに指定するようにします。 では最後に、作成されたexeファイルのIATを確認してみましょう。

見て分かるように、msvcr100.dllの関数を一切記述されておらず、exeファイルがCRTに依存していないことが分かります。 また、IATに記述されている関数が、コード上で呼び出している関数だけで構成されていることも分かります。

CRTとメモリ関数

CRTの呼び出しを避ける場合は、これまでCRTで実現していたことをWindows APIで実現することになります。 たとえば、メモリの開放ならばmallocではなく、HeapAllocを呼び出すという感じです。 こうした要領に基づき、メモリのコピーならばmemcpyではなくCopyMemory、 メモリを0に初期化するならばmemsetではなくZeroMemoryという要領になりますが、 これらのAPIには少し例外があります。 それは、次のヘッダーファイルを見るとよく分かります。

#define MoveMemory RtlMoveMemory
#define CopyMemory RtlCopyMemory
#define FillMemory RtlFillMemory
#define ZeroMemory RtlZeroMemory
#define SecureZeroMemory RtlSecureZeroMemory
#define CaptureStackBackTrace RtlCaptureStackBackTrace

※winbase.hより抜粋

Visual StudioでZeroMemoryを検索してみると、winbase.hの次の部分が表示されます。 これはつまり、ZeroMemoryの呼び出しが実際にはRtlZeroMemoryの呼び出しになっているということです。 それでは次に、RtlZeroMemoryで検索してみます。

#define RtlEqualMemory(Destination,Source,Length) (!memcmp((Destination),(Source),(Length)))
#define RtlMoveMemory(Destination,Source,Length) memmove((Destination),(Source),(Length))
#define RtlCopyMemory(Destination,Source,Length) memcpy((Destination),(Source),(Length))
#define RtlFillMemory(Destination,Length,Fill) memset((Destination),(Fill),(Length))
#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))

※winnt.hより抜粋

見て分かるように、RtlZeroMemoryの呼び出しがmemsetの呼び出しになっています。 これはつまり、実際にコード上にZeroMemoryを記述しても、それはmemsetを記述したものと解釈されるということであり、 結局CRTに依存してしまうことになります。 こうした問題を回避する策として考えられるのは、処理をアプリケーションが明示的に行うことです。 たとえば、メモリを0に初期化するコードは次のように記述できます。

void MemoryClear(LPVOID lpdest, int n)
{
	int    i;
	LPBYTE lp = (LPBYTE)lpdest;

	for (i = 0; i < n; i++)
		lp[i] = 0;
}

上記のような関数を作成し、ZeroMemoryの代わりに呼び出せばmemsetが使用されることはないように思えますが、 その限りではありません。 実際にMemoryClearを呼び出して、初期化されたバッファを参照するコードを記述したところ、 IATにmemsetの呼び出しが含まれていることを確認しました。 つまり、コンパイル時に、コードが0クリアの処理と解釈され、それがmemsetの呼び出しに置き換わったように思えます。 Visual Studioのプロパティの設定などで、こうした部分を上手くできないかと思いましたが、 今のところよい案は見つかっていない状態です。

少し複雑な方法ですが、kernel32.dll(及びntdll.dll)がエクスポートしているRtlZeroMemoryを呼び出すという方法があります。 この方法ならば、CRTの呼び出しが入る余地はないはずです。

typedef VOID (WINAPI *LPFNRTLZEROMEMORY)(VOID *, SIZE_T);

void MemoryClear(LPVOID lpdest, int n)
{
	HMODULE           hmod;
	LPFNRTLZEROMEMORY lpfnRtlZeroMemory;

	hmod = LoadLibrary(TEXT("kernel32.dll"));
	lpfnRtlZeroMemory = (LPFNRTLZEROMEMORY)GetProcAddress(hmod, "RtlZeroMemory");
	lpfnRtlZeroMemory(lpdest, n);
	FreeLibrary(hmod);
}

まず、LoadLibraryでモジュールのハンドルを取得します。 kernel32.dll(及びntdll.dll)はアドレス空間に必ずロードされているので、GetModuleHandleで取得しても構いません。 この場合、ハンドルの開放は不要になります。 GetProcAddressに"RtlZeroMemory"を指定してアドレスを取得し、lpfnRtlZeroMemoryを使用して呼び出します。 これで、メモリは0に初期化されたことになります。 CopyMemoryやMoveMemoryを呼び出す場合は、RtlMoveMemoryにリンクするようにします。


戻る