Condividi tramite


Errore irreversibile all'uscita del thread se il callback FLS non viene liberato

Questo articolo consente di risolvere il problema in cui una DLL C++ collegata in modo statico alla libreria C Runtime Library (CRT) causa un errore irreversibile all'uscita del thread se il caricamento o lo scaricamento della DLL viene interrotto da un'eccezione non gestita.

Versione originale del prodotto: Visual C++
Numero KB originale: 2754614

Sintomi

Una DLL C++ collegata in modo statico alla libreria C Runtime (CRT) può causare un errore irreversibile all'uscita del thread se il caricamento o lo scaricamento della DLL viene interrotto da un'eccezione non gestita.

Un processo può arrestarsi in modo anomalo all'uscita del thread con un'eccezione di violazione di accesso (0xC0000005, EXCEPTION_ACCESS_VIOLATION) se fosse stata caricata dinamicamente (ad esempio chiamando LoadLibraryA()) una DLL C++ nativa collegata in modo statico al runtime C e la DLL ha generato un'eccezione non gestita durante l'inizializzazione o l'arresto.

Durante l'avvio o l'arresto CRT (ad esempio durante DLL_PROCESS_ATTACH o DLL_PROCESS_DETACH in o in DllMain()o nel costruttore o distruttore di un oggetto C++ globale/statico), se la DLL genera un errore irreversibile non gestito, la LoadLibrary chiamata inghiottirà l'eccezione e restituisce con NULL. Quando il caricamento o lo scaricamento della DLL ha esito negativo, alcuni codici di errore che è possibile osservare includono:

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

Questo errore di avvio o arresto della libreria in genere non viene osservato fino a quando il thread chiamante non sta per uscire, sotto forma di eccezione di violazione di accesso in modo inattivo con uno stack di chiamate simile al seguente:

<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

Questo comportamento può essere riprodotto con il frammento di codice seguente in 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
}

Causa

Una funzione di callback Fiber Local Storage (FLS) viene richiamata da Windows quando il thread viene chiuso e l'indirizzo di tale funzione non è più in memoria di processo valida. La causa più comune è l'uso di CRT statico in una DLL che viene scaricata prematuramente.

Quando il runtime C viene inizializzato in fase di caricamento della DLL, registra una funzione di callback FLS denominata _freefls() tramite una chiamata a FlsAlloc(); tuttavia, il runtime C non annulla la registrazione di questo callback FLS se si verifica un'eccezione non gestita nella DLL mentre viene caricata o scaricata.

Poiché il runtime C è collegato in modo statico nella DLL, il callback FLS viene implementato in tale DLL stessa. Se questa DLL non riesce a caricare o scaricare a causa di un'eccezione non gestita, non solo la DLL verrà scaricata automaticamente, ma il callback FLS del runtime C rimarrà registrato con il sistema operativo anche dopo che la DLL viene scaricata. Quando il thread viene chiuso (ad esempio, quando la funzione exe main() viene restituita), il sistema operativo tenta di richiamare la funzione di callback FLS registrata (_freefls in questo caso) che ora punta allo spazio di processo non mappato e infine genera un'eccezione di violazione di accesso.

Risoluzione

È stata apportata una modifica in VC++ 11.0 CRT (in VS 2012) per risolvere meglio la pulizia del callback FLS in eccezioni non gestite durante l'avvio della DLL. Pertanto, per le DLL il cui codice sorgente è accessibile e quindi può essere ricompilato, è possibile provare le opzioni seguenti:

  1. Compilare la DLL con la versione più recente di VC11 CRT (ad esempio, compilare la DLL con VS2012 RTM).
  2. Usare la DLL CRT, anziché il collegamento statico al runtime C durante la compilazione della DLL; usare /MD o /MDd anziché /MT o /MTd.
  3. Se possibile, correggere la causa dell'eccezione non gestita, rimuovere la parte di codice soggetta a eccezioni da DllMaine/o gestire correttamente l'eccezione.
  4. Implementare una funzione punto di ingresso DLL personalizzata, wrapping dell'inizializzazione e del codice di CRT per annullare la registrazione del callback FLS di CRT se si verifica un'eccezione durante l'avvio della DLL. Questa gestione delle eccezioni intorno al punto di ingresso può causare un problema quando /GS (controlli di sicurezza del buffer) viene usato in una compilazione di debug. Se si sceglie questa opzione, escludere la gestione delle eccezioni (usando #if o #ifdef) dalle compilazioni di debug. Per le DLL che non possono essere ricompilate, non esiste attualmente alcun modo per correggere questo comportamento.

Ulteriori informazioni

Questo comportamento è causato da un errore durante l'annullamento della registrazione di un callback FLS in un modulo scaricato, pertanto è causato non solo da un'eccezione non gestita durante l'avvio o l'arresto della DLL CRT, ma anche configurando un callback FLS come illustrato di seguito e non annullando la registrazione prima che la DLL venga scaricata:

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

Poiché la funzione di callback FLS dovrebbe essere chiamata dal sistema operativo per eseguire la pulizia FLS, il puntatore di funzione non valido precedente genererà un'eccezione di violazione di accesso. Pertanto, la soluzione ideale a questo problema consiste nel correggere il codice stesso, assicurandosi che il callback FLS venga annullata la registrazione prima che la DLL venga scaricata.

Note

Potrebbero essere presenti prodotti di terze parti registrati nel computer di runtime che inseriranno dll in fase di esecuzione nella maggior parte dei processi. In questi casi, una DLL interessata al di fuori dello sviluppo del prodotto può causare questo errore durante l'uscita del thread. Se non si è in grado di ricompilare tali DLL in base alle indicazioni suggerite in precedenza, l'unica opzione potrebbe essere contattare il fornitore del prodotto e richiedere tale correzione o disinstallare il prodotto di terze parti.