Поделиться через


Неустранимая ошибка при выходе потока, если обратный вызов FLS не освобожден

Эта статья поможет устранить проблему, из-за которой библиотека DLL C++ статически связана с библиотекой времени выполнения C (CRT) приводит к неустранимой ошибке при выходе потока, если последовательность загрузки или выгрузки библиотеки DLL прерывается необработанным исключением.

Исходная версия продукта: Visual C++
Исходный номер базы знаний: 2754614

Симптомы

Библиотека DLL C++ статически связана с библиотекой времени выполнения C (CRT) может привести к неустранимой ошибке при выходе потока, если загрузка или выгрузка библиотеки DLL прерывается необработанным исключением.

Процесс может завершиться сбоем при выходе потока с исключением нарушения доступа (0xC0000005, EXCEPTION_ACCESS_VIOLATION), если он динамически загружен (например, путем вызова loadLibraryA()) собственной библиотеки DLL C++, которая была статически связана с средой выполнения C, и библиотека DLL вызвала необработанное исключение во время инициализации или завершения работы.

Во время запуска или завершения работы CRT (например, во время DLL_PROCESS_ATTACH или DllMain()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
}

Причина

Функция обратного вызова Fibre Local Storage (FLS) вызывается Windows при выходе потока, а адрес этой функции больше не находится в допустимой памяти процесса. Наиболее распространенной причиной является использование статического CRT в библиотеке DLL, которая преждевременно выгружается.

При инициализации среды выполнения C во время загрузки библиотеки DLL она регистрирует функцию обратного вызова FLS с именем _freefls() с помощью вызова FlsAlloc(); однако среда выполнения C не отменяет регистрацию этого обратного вызова FLS, если необработанное исключение возникает в библиотеке DLL во время загрузки или выгрузки.

Так как среда выполнения C статически связана в библиотеке DLL, его обратный вызов FLS реализуется в самой библиотеке DLL. Если эта библиотека DLL не может загрузить или выгрузить из-за необработанного исключения, она не только будет выгружена автоматически, но обратный вызов FLS среды выполнения C останется зарегистрированным в ОС даже после выгрузки библиотеки DLL. Когда поток завершает работу (например, когда функция EXE main() возвращается), ОС пытается вызвать зарегистрированную функцию обратного вызова FLS (_freefls в данном случае), которая теперь указывает на незамеченное пространство процесса и в конечном итоге приводит к исключению нарушения доступа.

Решение

В VC++ 11.0 CRT (в VS 2012) было внесено изменение, чтобы улучшить очистку обратного вызова FLS при необработанных исключениях во время запуска библиотеки DLL. Таким образом, для библиотек DLL, исходный код которых доступен и поэтому может быть перекомпилирован, можно попробовать следующие параметры:

  1. Компилируйте библиотеку DLL с помощью последней версии CRT VC11 (например, создайте библиотеку DLL с помощью RTM VS2012).
  2. Используйте библиотеку DLL CRT, а не статическую компоновку со средой выполнения C при компиляции библиотеки DLL; используйте /MD или /MDd вместо /MT или /MTd.
  3. По возможности исправьте причину необработанного исключения, удалите фрагмент кода DllMain, подверженный исключению, и (или) правильно обработайте исключение.
  4. Реализуйте пользовательскую функцию точки входа DLL, упаковав инициализацию CRT и код, чтобы отменить регистрацию обратного вызова FLS CRT при возникновении исключения во время запуска библиотеки DLL. Эта обработка исключений вокруг точки входа может вызвать проблему при использовании /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, приведенный выше недопустимый указатель функции приведет к исключению нарушения доступа. Таким образом, идеальное решение этой проблемы заключается в том, чтобы исправить сам код, гарантируя, что обратный вызов FLS отменяется до выгрузки библиотеки DLL.

Примечание.

На компьютере среды выполнения могут быть зарегистрированы сторонние продукты, которые будут внедрять библиотеки DLL во время выполнения в большинство процессов. В таких случаях затронутая библиотека DLL за пределами разработки продукта может привести к этой ошибке во время выхода потока. Если вы не находитесь в состоянии перестроить такие библиотеки DLL в соответствии с приведенными выше рекомендациями, вы можете обратиться к поставщику продукта и запросить такое исправление или удалить сторонний продукт.