Schwerwiegender Fehler beim Beenden des Threads, wenn der FLS-Rückruf nicht freigegeben wird

Dieser Artikel hilft Ihnen bei der Lösung des Problems, bei dem eine C++-DLL statisch mit der C-Laufzeitbibliothek (C Run-Time Library, CRT) verknüpft ist, einen schwerwiegenden Fehler beim Beenden des Threads verursacht, wenn die DLL-Lade- oder -Entladen-Sequenz durch eine unbehandelte Ausnahme unterbrochen wird.

Originalversion des Produkts:   Visual C++
Ursprüngliche KB-Nummer:   2754614

Problembeschreibung

Eine statisch mit der C-Laufzeitbibliothek (C Run-Time Library, CRT) verknüpfte C++-DLL kann beim Beenden des Threads einen schwerwiegenden Fehler verursachen, wenn die DLL-Lade- oder -Unload-Sequenz durch eine unbehandelte Ausnahme unterbrochen wird.

Ein Prozess kann beim Threadabbruch mit einer Access-Verletzungsausnahme (0xC0000005, EXCEPTION_ACCESS_VIOLATION) abstürzen, wenn er dynamisch geladen wurde (z. B. durch Aufrufen von LoadLibraryA()) eine systemeigene C++-DLL, die statisch mit C-Runtime verknüpft war, und die DLL während ihrer Initialisierung oder beim Herunterfahren eine unbehandelte Ausnahme generiert hat.

Während des CRT-Starts oder -Herunterfahrens (z. B. während DLL_PROCESS_ATTACH oder DLL_PROCESS_DETACH in DllMain(), oder im Konstruktor oder Destruktor eines globalen/statischen C++-Objekts), wenn die DLL einen schwerwiegenden Fehler generiert, der unbehandelt ist, verschluckt der LoadLibrary Aufruf nur die Ausnahme und gibt mit NULL zurück. Wenn beim Laden oder Entladen der DLL ein Fehler auftritt, können folgende Fehlercodes auftreten:

  • ERROR_NOACCESS (998) oder EXCEPTION_ACCESS_VIOLATION (0xC0000005, 0n3221225477)
  • EXCEPTION_INT_DIVIDE_BY_ZERO (0xC0000094, 0n3221225620)
  • ERROR_STACK_OVERFLOW (1001) oder EXCEPTION_STACK_OVERFLOW (0xC00000FD, 0n3221225725)
  • C++-Ausnahme (0xE06D7363, 0n3765269347)
  • ERROR_DLL_INIT_FAILED (0x8007045A)

Dieser Fehler beim Starten oder Herunterfahren der Bibliothek wird in der Regel erst beobachtet, wenn der aufrufende Thread in Form einer tödlichen Access-Verletzungsausnahme mit einem ähnlichen Aufrufstapel wie unten beendet wird:

<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

Dieses Verhalten kann mit dem folgenden Codeausschnitt in Visual Studio reproduziert werden:

//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
}

Ursache

Eine FLS-Rückruffunktion (Fiber Local Storage) wird von Windows aufgerufen, wenn der Thread beendet wird, und die Adresse dieser Funktion befindet sich nicht mehr im gültigen Prozessspeicher. Die häufigste Ursache ist die Verwendung von statischem CRT in einer DLL, die vorzeitig entladen wird.

Wenn die C-Runtime zur DLL-Ladezeit initialisiert wird, registriert sie eine FLS-Rückruffunktion namens _freefls() über einen Aufruf von FlsAlloc(); Die C-Runtime hebt die Registrierung dieses FLS-Rückrufs jedoch nicht auf, wenn eine unbehandelte Ausnahme in der DLL auftritt, während sie geladen oder entladen wird.

Da die C-Runtime statisch in der DLL verknüpft ist, wird der FLS-Rückruf in dieser DLL selbst implementiert. Wenn diese DLL aufgrund einer unbehandelten Ausnahme nicht geladen oder entladen werden kann, wird die DLL nicht nur automatisch entladen, sondern der FLS-Rückruf der C-Runtime bleibt auch nach dem Entladen der DLL beim Betriebssystem registriert. Wenn der Thread beendet wird (z. B. wenn die EXE-Funktion main() zurückgegeben wird), versucht das Betriebssystem, die registrierte FLS-Rückruffunktion aufzurufen (_freefls in diesem Fall), die jetzt auf nicht zugeordneten Prozessbereich verweist und letztendlich zu einer Ausnahme von Zugriffsverletzungen führt.

Lösung

Im VC++ 11.0 CRT (in VS 2012) wurde eine Änderung vorgenommen, um die FLS-Rückrufbereinigung bei unbehandelten Ausnahmen beim DLL-Start besser zu adressieren. Daher können für DLLs, auf deren Quellcode zugegriffen werden kann und daher neu kompiliert werden könnte, die folgenden Optionen ausprobiert werden:

  1. Kompilieren Sie die DLL mit der neuesten VC11 CRT (z. B. erstellen Sie die DLL mit VS2012 RTM).
  2. Verwenden Sie die CRT-DLL, anstatt statische Verknüpfungen mit C-Runtime beim Kompilieren Ihrer DLL zu erstellen. verwenden Sie /MD oder /MDd anstelle von /MT oder /MTd.
  3. Korrigieren Sie nach Möglichkeit die Ursache der unbehandelten Ausnahme, entfernen Sie den ausnahmeanfälligen Codeabschnitt aus DllMain, und/oder behandeln Sie die Ausnahme ordnungsgemäß.
  4. Implementieren Sie eine benutzerdefinierte DLL-Einstiegspunktfunktion, indem Sie die Initialisierung und den Code von CRT umschließen, um die Registrierung des FLS-Rückrufs des CRT aufzuheben, wenn während des DLL-Starts eine Ausnahme auftritt. Diese Ausnahmebehandlung um den Einstiegspunkt kann ein Problem verursachen, wenn /GS (Puffersicherheitsprüfungen) in einem Debugbuild verwendet wird. Wenn Sie diese Option auswählen, schließen Sie die Ausnahmebehandlung (mit #if oder #ifdef) aus Debugbuilds aus. Für DLLs, die nicht neu erstellt werden können, gibt es derzeit keine Möglichkeit, dieses Verhalten zu korrigieren.

Weitere Informationen

Dieses Verhalten wird durch einen Fehler beim Aufheben der Registrierung eines FLS-Rückrufs in einem Modul verursacht, das entladen wurde. Dies wird nicht nur durch eine unbehandelte Ausnahme beim Starten oder Herunterfahren der DLL-CRT verursacht, sondern auch durch das Einrichten eines FLS-Rückrufs wie unten dargestellt und nicht durch aufheben der Registrierung, bevor die DLL entladen wird:

//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
}

Da die FLS-Rückruffunktion vom Betriebssystem aufgerufen werden soll, um die FLS-Bereinigung durchzuführen, führt der oben genannte ungültige Funktionszeiger zu einer Access-Verletzungs-Ausnahme. Daher besteht die ideale Lösung für dieses Problem darin, den Code selbst zu korrigieren und sicherzustellen, dass die Registrierung des FLS-Rückrufs aufgehoben wird, bevor die DLL entladen wird.

Hinweis

Möglicherweise sind Auf dem Laufzeitcomputer Produkte von Drittanbietern registriert, die DLLs zur Laufzeit in die meisten Prozesse einfügen. In solchen Fällen kann eine betroffene DLL außerhalb Ihrer Produktentwicklung zu diesem Fehler beim Thread-Exit führen. Wenn Sie nicht in der Lage sind, solche DLLs gemäß den oben vorgeschlagenen Anleitungen neu zu erstellen, können Sie sich nur an den Hersteller des Produkts wenden und einen solchen Fix anfordern oder das Drittanbieterprodukt deinstallieren.