次の方法で共有


FLS コールバックが解放されていない場合のスレッド終了時の致命的なエラー

この記事は、C ランタイム ライブラリ (CRT) に静的にリンクされた C++ DLL が、DLL の読み込みまたはアンロード シーケンスがハンドルされない例外によって中断された場合にスレッド終了時に致命的なエラーを引き起こす問題を解決するのに役立ちます。

元の製品バージョン: Visual C++
元の KB 番号: 2754614

現象

C ランタイム ライブラリ (CRT) に静的にリンクされている C++ DLL は、DLL の読み込みまたはアンロード シーケンスがハンドルされない例外によって中断された場合、スレッド終了時に致命的なエラーを引き起こす可能性があります。

C ランタイムと静的にリンクされたネイティブ C++ DLL を動的に読み込み ( LoadLibraryA()を呼び出すなど) 場合、スレッドの終了時にアクセス違反例外 (0xC0000005、EXCEPTION_ACCESS_VIOLATION) でプロセスがクラッシュする可能性があり、DLL は初期化またはシャットダウン中にハンドルされない例外を生成しました。

CRT の起動またはシャットダウン中 (DllMain()DLL_PROCESS_ATTACH中やDLL_PROCESS_DETACH中、グローバル/静的 C++ オブジェクトのコンストラクターまたはデストラクターなど) で DLL がハンドルされない致命的なエラーを生成した場合、LoadLibrary呼び出しは例外を飲み込み、NULL を返します。 DLL の読み込みまたはアンロードが失敗すると、次のようなエラー コードが表示されることがあります。

  • ERROR_NOACCESS (998) またはEXCEPTION_ACCESS_VIOLATION (0xC0000005, 0n3221225477)
  • EXCEPTION_INT_DIVIDE_BY_ZERO (0xC0000094, 0n3221225620)
  • ERROR_STACK_OVERFLOW (1001) またはEXCEPTION_STACK_OVERFLOW (0xC00000FD, 0n3221225725)
  • C++ 例外 (0xE06D7363、0n3765269347)
  • ERROR_DLL_INIT_FAILED (0x8007045A)

このライブラリの起動またはシャットダウンの失敗は、通常、呼び出し元のスレッドが終了するまで、次のような呼び出し履歴を持つ致命的なアクセス違反例外の形式で観察されません。

<Unloaded_TestDll.dll>+0x1642 ntdll!RtlProcessFlsData+0x57 ntdll!LdrShutdownProcess+0xbd
ntdll!RtlExitUserProcess+0x74 kernel32!ExitProcessStub+0x12 TestExe!__crtExitProcess+0x17
TestExe!doexit+0x12a TestExe!exit+0x11 TestExe!__tmainCRTStartup+0x11c
kernel32!BaseThreadInitThunk+0xe ntdll!__RtlUserThreadStart+0x70 ntdll!_RtlUserThreadStart+0x1b

この動作は、Visual Studio の次のコード スニペットを使用して再現できます。

//TestDll.dll: Make sure to use STATIC CRT to compile this DLL (i.e., /MT or /MTd)
#include <Windows.h>
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
        {
            //About to generate an exception
            int* pInt = NULL;
            *pInt = 5;
            break;
        }
    }
    return TRUE;
}

//TestExe.exe:
#include <Windows.h>
#include <stdio.h>
int main(int argc, TCHAR* argv[])
{
     HMODULE hModule = LoadLibrary(TEXT("TestDll.dll"));
     printf("GetLastError = %d\n", GetLastError());
     if (hModule != NULL)
     FreeLibrary(hModule);
     return 0;
     //Access Violation will occur following the above return statement
}

原因

スレッドが終了すると、ファイバー ローカル ストレージ (FLS) コールバック関数が Windows によって呼び出され、その関数のアドレスが有効なプロセス メモリ内にありません。 最も一般的な原因は、途中でアンロードされる DLL で静的 CRT を使用した場合です。

C ランタイムは、DLL の読み込み時に初期化されると、FlsAlloc()の呼び出しによって _freefls() という名前の FLS コールバック関数を登録します。ただし、読み込み中またはアンロード中に DLL でハンドルされない例外が発生した場合、C ランタイムはこの FLS コールバックの登録を解除しません。

C ランタイムは DLL 内で静的にリンクされているため、その DLL 自体にその FLS コールバックが実装されます。 この DLL がハンドルされない例外によって読み込みまたはアンロードに失敗した場合、DLL は自動的にアンロードされるだけでなく、DLL がアンロードされた後も C ランタイムの FLS コールバックは OS に登録されたままになります。 スレッドが終了すると (たとえば、EXE の main() 関数が返されるときに)、OS は登録済みの FLS コールバック関数 (この場合は_freefls ) を呼び出そうとします。これにより、マップされていないプロセス領域が指され、最終的にアクセス違反例外が発生します。

解決方法

VC++ 11.0 CRT (VS 2012 の場合) で、DLL の起動中にハンドルされない例外の FLS コールバック クリーンアップに対応するように変更が行われました。 そのため、ソース コードにアクセスでき、そのため再コンパイルできる DLL の場合は、次のオプションを試すことができます。

  1. 最新の VC11 CRT を使用して DLL をコンパイルします (たとえば、VS2012 RTM を使用して DLL をビルドします)。
  2. DLL のコンパイル中に C ランタイムに静的リンクするのではなく、CRT DLL を使用します。/MT または /MTd の代わりに、/MD または /MDd を使用
  3. 可能であれば、ハンドルされない例外の原因を修正し、例外が発生しやすいコードを DllMainから削除するか、例外を適切に処理します。
  4. カスタム DLL エントリ ポイント関数を実装し、CRT の初期化とコードをラップして、DLL の起動時に例外が発生した場合に CRT の FLS コールバックを登録解除します。 エントリ ポイントに関するこの例外処理により、デバッグ ビルドで /GS (バッファー セキュリティ チェック) が使用されている場合に問題が発生する可能性があります。 このオプションを選択した場合は、デバッグ ビルドから例外処理 ( #if または #ifdefを使用) を除外します。 再構築できない DLL の場合、現在、この動作を修正する方法はありません。

詳細

この動作は、アンロードされたモジュールに FLS コールバックの登録を解除できなかったことが原因で発生するため、DLL CRT の起動またはシャットダウン中にハンドルされない例外が発生しただけでなく、次に示すように FLS コールバックを設定し、DLL がアンロードされる前に登録を解除しないことが原因で発生します。

//TestDll.dll: To reproduce the problem, compile with static CRT (/MT or /MTd)
#include <Windows.h>

VOID WINAPI MyFlsCallback(PVOID lpFlsData)
{
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
         case DLL_PROCESS_ATTACH:
         {
             //Leaking FLS callback and rather setting an invalid callback.
             DWORD dwFlsIndex = FlsAlloc(MyFlsCallback);
             FlsSetValue(dwFlsIndex, (PVOID)5);
             break;
         }
    }
    return TRUE;
}

//TestExe.exe:
#include <Windows.h>
#include <stdio.h>

int main(int argc, TCHAR* argv[])
{
     HMODULE hModule = LoadLibrary(TEXT("TestDll.dll"));
     printf("GetLastError = %d \n", GetLastError());
     if (hModule != NULL)
     FreeLibrary(hModule);
     return 0;
     //Access Violation will occur following the above return statement
}

FLS コールバック関数は、FLS クリーンアップを実行するために OS によって呼び出されることになっているので、上記の無効な関数ポインターはアクセス違反例外になります。 したがって、この問題の理想的な解決策は、コード自体を修正し、DLL がアンロードされる前に FLS コールバックが登録解除されるようにすることです。

Note

ほとんどのプロセスに実行時に DLL を挿入するサードパーティ製品がランタイム コンピューターに登録されている可能性があります。 このような場合、製品開発の外部で影響を受ける DLL は、スレッドの終了時にこのエラーにつながる可能性があります。 上記のガイダンスに従ってこのような DLL を再構築する立場にない場合は、製品のベンダーに連絡してそのような修正プログラムを要求するか、サードパーティ製品をアンインストールすることが唯一の選択肢である可能性があります。