Windows 95 で動くプログラムを作る (VS2008編)
「かなり強引な方法で DLL のインポートを横取りする (未参照を防ぐ)」では、VS2005でビルドしたネイティブWindowsアプリをWindows 95でも実行できるようにする手法を紹介しました。
そこでVS2008でも全く同じ手法を用いてテストしたところ、残念ながら動作しませんでした。
結論から言うと、原因は「IsDebuggerPresent」関数だけでなく、以下の点が悪さをしていたためでした。
- CRTが「InitializeCriticalSectionAndSpinCount」関数を利用している
- CRTが「GetModuleHandleW」関数を利用している
- (EXEファイルに刻まれる)Subsystemのバージョンが5.0になっている
「Dependency Walker」で示した画像を以下に貼り付けておきます。
※OSバージョンも「5.0」になっていますが、こちらはそのままでも実行できました。
※なお、GetModuleHandleW関数自体はWindows 95でも定義はされていますが、未実装なために常にNULLを返すので障害になっています。
さてその解決方法ですが、前者2つはIsDebuggerPresent関数のときの対処法と同様にクリアできます。今回は(長いですが)すべてアセンブリで書いたコードを載せてみます。(IsDebuggerPresentを除く)
; プロトタイプ宣言 ; ポインタは型関係なくすべて ptr DWORD とした GetModuleHandleA proto stdcall, lpszModuleName : ptr DWORD GetProcAddress proto stdcall, hInstance : ptr DWORD, lpszProcName : ptr DWORD InitializeCriticalSection proto stdcall, lpCriticalSection : ptr DWORD WideCharToMultiByte proto stdcall, CodePage : DWORD, dwFlags : DWORD, ¥ lpWideCharStr : ptr DWORD, cchWideChar : DWORD, lpMultiByteStr : ptr DWORD, cbMultiByte : DWORD, ¥ lpDefaultChar : ptr DWORD, lpUsedDefaultChar : ptr DWORD .data ; 一度初期化を行ったかどうかのフラグ s_bInitCSSC dd 0 ; 実際の InitializeCriticalSectionAndSpinCount 関数へのポインタ s_pfnMyInitializeCriticalSectionAndSpinCount dd 0 ; ANSI 文字列「kernel32.dll」 kernel32_dll_str db 'kernel32.dll', 0 ; ANSI 文字列「InitializeCriticalSectionAndSpinCount」 InitializeCriticalSectionAndSpinCount_name_str db 'InitializeCriticalSectionAndSpinCount', 0 ; IsDebuggerPresent のときと同様に __imp__XXX を定義する __imp__InitializeCriticalSectionAndSpinCount@8 dd MyInitializeCriticalSectionAndSpinCount EXTERNDEF __imp__InitializeCriticalSectionAndSpinCount@8 : DWORD __imp__GetModuleHandleW@4 dd MyGetModuleHandleW EXTERNDEF __imp__GetModuleHandleW@4 : DWORD .code ; 関数 MyInitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION, DWORD) MyInitializeCriticalSectionAndSpinCount proc ; フラグをチェックする mov eax, s_bInitCSSC test eax, eax jne DoCall ; GetModuleHandle 関数で kernel32.dll のハンドルを取得、 ; GetProcAddress 関数で InitializeCriticalSectionAndSpinCount 関数のアドレス取得を試みる push offset InitializeCriticalSectionAndSpinCount_name_str push offset kernel32_dll_str call GetModuleHandleA push eax call GetProcAddress ; 取得できなかった場合は eax は 0 mov s_pfnMyInitializeCriticalSectionAndSpinCount, eax xor eax, eax inc eax ; 成否に関わらずフラグをセットする mov s_bInitCSSC, eax DoCall: ; InitializeCriticalSectionAndSpinCount 関数が無い場合は自前のコードを呼ぶ mov eax, s_pfnMyInitializeCriticalSectionAndSpinCount test eax, eax je DoOldCall ; スタックをそのままに InitializeCriticalSectionAndSpinCount 関数にジャンプする jmp eax DoOldCall: ; InitializeCriticalSection 関数を呼ぶ mov eax, dword ptr[esp + 4] push eax call InitializeCriticalSection ; InitializeCriticalSection 関数は戻り値が無いので自分でセットする xor eax, eax inc eax ret 8 MyInitializeCriticalSectionAndSpinCount endp ; 関数 MyGetModuleHandleW(LPCWSTR) ; WideChatToMultiByte 関数を用いて文字列を変換し、GetModuleHandleA 関数を呼び出す ; ※ Unicode 文字列の含まれるパスは当然利用できなくなる MyGetModuleHandleW proc push ebp mov ebp, esp push ebx push ecx push edx ; まず WideCharToMultiByte 関数を呼び出して文字列の長さを計算する xor ecx, ecx push ecx ; lpUsedDefaultChar push ecx ; lpDefaultChar push ecx ; cbMultiByte push ecx ; lpMultiByteStr dec ecx push ecx ; cchWideChar mov eax, dword ptr[ebp + 8] push eax ; lpWideCharStr inc ecx push ecx ; dwFlags push ecx ; CodePage = CP_ACP call WideCharToMultiByte ; バッファサイズを 4-byte 境界に丸める mov edx, eax add edx, 3 shr edx, 2 shl edx, 2 ; スタック上にバッファを作る mov eax, esp sub eax, edx mov esp, eax mov ebx, eax ; バッファサイズをプッシュする → (1) push edx ; GetModuleHandleA 関数呼び出しの引数をプッシュする → (2) push ebx ; バッファのポインタを指定して WideCharToMultiByte 関数を呼び出す xor ecx, ecx push ecx ; lpUsedDefaultChar push ecx ; lpDefaultChar push edx ; cbMultiByte push ebx ; lpMultiByteStr dec ecx push ecx ; cchWideChar mov eax, dword ptr[ebp + 8] push eax ; lpWideCharStr inc ecx push ecx ; dwFlags push ecx ; CodePage = CP_ACP call WideCharToMultiByte ; (2) より GetModuleHandleA 関数を呼び出す call GetModuleHandleA ; (1) よりバッファサイズをポップしてスタックポインタを戻す pop edx add esp, edx pop edx pop ecx pop ebx leave ret 4 MyGetModuleHandleW endp
問題は3つ目の「Subsystemのバージョンが5.0になっている」というものです。この値のおかげで、関数の未定義参照をクリアしてもプログラムが実行できません。(試してはいませんが、おそらくWindows 98やMeでも実行できなくなっていると思います。)
これを解決するには、単にバージョンを「4.0」に書き換えればいいのですが、いちいちバイナリエディタを起動して書き換えるのは大変なのに加え、ただ書き換えるだけだとチェックサムが不一致となり、プログラムの実行に支障をきたす可能性があります。
そこで、「AdjustSV.exe」というプログラムを作成しました。「掘り出し物」ページに置いていますので使いたい方はダウンロードをお願いします。使い方は簡単で、コマンドラインで第一引数に実行可能ファイルのパスを指定すると、そのファイルのサブシステムバージョンを「4.0」に書き換えます。その際、チェックサムも正しい値に更新します。(元ファイルのバックアップは作らないので自己責任で利用をお願いします。)
以上の手順を踏まえると以下の画像のようになり、無事実行できるようになります。
最終更新日: 2009/07/10