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

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

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

現象

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

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

CRT の起動中またはシャットダウン中 (または のDllMain()DLL_PROCESS_ATTACHDLL_PROCESS_DETACHまたはグローバル/静的 C++ オブジェクトのコンストラクターまたはデストラクターなど) 中に、DLL でハンドルされない致命的なエラーが生成された場合、呼び出しは例外を飲み込み、LoadLibraryNULL を返します。 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 (バッファー セキュリティ チェック) が使用されている場合に問題を引き起こす可能性があります。 このオプションを選択した場合は、デバッグ ビルドから例外処理 (または を#ifdef使用#if) を除外します。 再構築できない 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 コールバックの登録が解除されるようにすることです。

注:

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