Programming Field - プログラミング Tips

Windows 95 で動くプログラムを作る (VS2008編)

「かなり強引な方法で DLL のインポートを横取りする (未参照を防ぐ)」では、VS2005でビルドしたネイティブWindowsアプリをWindows 95でも実行できるようにする手法を紹介しました。

そこでVS2008でも全く同じ手法を用いてテストしたところ、残念ながら動作しませんでした。

結論から言うと、原因は「IsDebuggerPresent」関数だけでなく、以下の点が悪さをしていたためでした。

「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