Compartir a través de


Error irrecuperable en la salida del subproceso si no se libera la devolución de llamada de FLS

Este artículo le ayuda a resolver el problema por el que un archivo DLL de C++ vinculado estáticamente a la biblioteca en tiempo de ejecución (CRT) de C provoca un error irrecuperable al salir del subproceso si la secuencia de carga o descarga de DLL se interrumpe mediante una excepción no controlada.

Versión original del producto: Visual C++
Número de KB original: 2754614

Síntomas

Un archivo DLL de C++ vinculado estáticamente a la biblioteca en tiempo de ejecución de C (CRT) puede producir un error irrecuperable en la salida del subproceso si la secuencia de carga o descarga de DLL se interrumpe mediante una excepción no controlada.

Un proceso puede bloquearse al salir del subproceso con una excepción de infracción de acceso (0xC0000005, EXCEPTION_ACCESS_VIOLATION) si se ha cargado dinámicamente (por ejemplo, llamando a LoadLibraryA()) un archivo DLL nativo de C++ vinculado estáticamente a C Runtime y el archivo DLL generó una excepción no controlada durante su inicialización o apagado.

Durante el inicio o apagado de CRT (como durante DLL_PROCESS_ATTACH o DLL_PROCESS_DETACH en DllMain(), o en el constructor o destructor de un objeto C++ global o estático), si el archivo DLL genera un error irrecuperable que no se controla, la LoadLibrary llamada simplemente traga la excepción y devuelve con NULL. Cuando se produce un error en la carga o descarga de DLL, algunos códigos de error que puede observar incluyen:

  • 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)
  • Excepción de C++ (0xE06D7363, 0n3765269347)
  • ERROR_DLL_INIT_FAILED (0x8007045A)

Este error de inicio o apagado de la biblioteca normalmente no se observa hasta que el subproceso que realiza la llamada está a punto de salir, en forma de una excepción de infracción de acceso mortal con una pila de llamadas similar a la siguiente:

<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

Este comportamiento se puede reproducir con el siguiente fragmento de código en 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

Windows invoca una función de devolución de llamada de Fiber Local Storage (FLS) cuando se cierra el subproceso y la dirección de esa función ya no está en la memoria de proceso válida. La causa más común es el uso de CRT estático en un archivo DLL que se descarga prematuramente.

Cuando el entorno de ejecución de C se inicializa en tiempo de carga dll, registra una función de devolución de llamada fls denominada _freefls() a través de una llamada a FlsAlloc(); sin embargo, el entorno de ejecución de C no anula el registro de esta devolución de llamada fls si se produce una excepción no controlada en el archivo DLL mientras se carga o descarga.

Dado que el entorno de ejecución de C está vinculado estáticamente en el archivo DLL, su devolución de llamada FLS se implementa en ese propio archivo DLL. Si este archivo DLL no se puede cargar o descargar debido a una excepción no controlada, no solo se descargará automáticamente el archivo DLL, sino que la devolución de llamada FLS del entorno de ejecución de C permanecerá registrada en el sistema operativo incluso después de que se descargue el archivo DLL. Cuando el subproceso se cierra (por ejemplo, cuando se devuelve la función exe main() ), el sistema operativo intenta invocar la función de devolución de llamada FLS registrada (_freefls en este caso) que ahora apunta al espacio de proceso no asignado y, en última instancia, da como resultado una excepción de infracción de acceso.

Solución

Se ha realizado un cambio en VC++ 11.0 CRT (en VS 2012) para abordar mejor la limpieza de devolución de llamada de FLS en las excepciones no controladas durante el inicio del archivo DLL. Por lo tanto, para los archivos DLL cuyo código fuente es accesible y, por lo tanto, se pueden volver a compilar, se pueden probar las siguientes opciones:

  1. Compile el archivo DLL con la versión más reciente de VC11 CRT (por ejemplo, compile el archivo DLL con VS2012 RTM).
  2. Use la DLL de CRT, en lugar de la vinculación estática a C Runtime al compilar el archivo DLL; use /MD o /MDd en lugar de /MT o /MTd.
  3. Si es posible, corrija la causa de la excepción no controlada, quite el fragmento de código propenso a excepciones de DllMainy/o controle la excepción correctamente.
  4. Implemente una función de punto de entrada DLL personalizada, ajustando la inicialización y el código de CRT para anular el registro de la devolución de llamada FLS de CRT si se produce una excepción durante el inicio del archivo DLL. Este control de excepciones alrededor del punto de entrada puede provocar un problema cuando se usa /GS (comprobaciones de seguridad de búfer) en una compilación de depuración. Si elige esta opción, excluya el control de excepciones (mediante #if o #ifdef) de las compilaciones de depuración. En el caso de los archivos DLL que no se pueden volver a generar, actualmente no hay ninguna manera de corregir este comportamiento.

Más información

Este comportamiento se debe a un error al anular el registro de una devolución de llamada FLS en un módulo que se ha descargado, por lo que no solo se debe a una excepción no controlada durante el inicio o apagado de CRT de DLL, sino también al configurar una devolución de llamada FLS como se muestra a continuación y no anular el registro antes de que se descargue el archivo 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
}

Como el sistema operativo debe llamar a la función de devolución de llamada FLS para realizar la limpieza de FLS, el puntero de función no válido anterior dará lugar a una excepción de infracción de acceso. Por lo tanto, la solución ideal para este problema sería corregir el código en sí, lo que garantiza que la devolución de llamada fls no se registrará antes de que se descargue el archivo DLL.

Nota:

Puede haber productos de terceros registrados en el equipo en tiempo de ejecución que insertarán archivos DLL en tiempo de ejecución en la mayoría de los procesos. En tales casos, un archivo DLL afectado fuera del desarrollo del producto puede provocar este error durante la salida del subproceso. Si no está en condiciones de recompilar dichos archivos DLL de acuerdo con las instrucciones sugeridas anteriormente, su única opción puede ser ponerse en contacto con el proveedor del producto y solicitar dicha corrección, o desinstalar el producto de terceros.