如果 FLS 回调未释放,则线程退出时出现严重错误

本文可帮助你解决以下问题:如果 DLL 加载或卸载序列因未经处理的异常中断,C++ DLL 静态链接到 C 运行时库 (CRT)会导致线程退出时出现严重错误。

原始产品版本: Visual C++
原始 KB 数: 2754614

现象

如果 DLL 加载或卸载序列被未经处理的异常中断,则静态链接到 C 运行时库 (CRT) C++ DLL 可能会导致线程退出时出现严重错误。

如果进程已动态加载(例如,通过调用 LoadLibraryA()与 C 运行时静态链接的本机 C++ DLL,则进程可能会在线程退出时崩溃(0xC0000005,EXCEPTION_ACCESS_VIOLATION),DLL 在初始化或关闭期间生成了未经处理的异常。

在 CRT 启动或关闭期间(例如,在DLL_PROCESS_ATTACH DLL_PROCESS_DETACH DllMain()全局/静态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
}

原因

当线程退出时,Windows 将调用光纤本地存储(FLS)回调函数,并且该函数的地址不再处于有效的进程内存中。 最常见的原因是在过早卸载的 DLL 中使用静态 CRT。

当 C 运行时在 DLL 加载时初始化时,它会通过对 FlsAlloc() 的调用注册名为 _freefls()FLS 回调函数;但是,如果在加载或卸载 DLL 时发生未经处理的异常,C 运行时不会注销此 FLS 回调。

由于 C 运行时在 DLL 中静态链接,因此它在 DLL 本身中实现其 FLS 回调。 如果此 DLL 由于未经处理的异常而无法加载或卸载,不仅会自动卸载 DLL,而且即使卸载 DLL,C 运行时的 FLS 回调仍将注册到 OS。 当线程退出(例如,EXE 的函数返回时),OS 会尝试调用已注册的 main() FLS 回调函数(在本例中_freefls ),该函数现在指向未映射的进程空间,并最终导致访问冲突异常。

解决方法

VC++ 11.0 CRT(在 VS 2012 中)进行了更改,以便更好地解决 DLL 启动时未处理的异常的 FLS 回调清理问题。 因此,对于可访问其源代码的 DLL,因此可以重新编译,可以尝试以下选项:

  1. 使用最新的 VC11 CRT 编译 DLL(例如,使用 VS2012 RTM 生成 DLL)。
  2. 在编译 DLL 时使用 CRT DLL,而不是静态链接到 C 运行时;使用 /MD/MDd 而不是 /MT/MTd
  3. 如果可能,请更正未经处理的异常的原因,从中删除容易发生异常的代码 DllMain片段,并/或正确处理异常。
  4. 实现自定义 DLL 入口点函数,包装 CRT 的初始化和代码,以在 DLL 启动时发生异常时取消注册 CRT 的 FLS 回调。 在调试版本中使用 /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
}

由于 OS 应调用 FLS 回调函数来执行 FLS 清理,因此上述无效的函数指针将导致访问冲突异常。 因此,解决此问题的理想解决方法是更正代码本身,确保在卸载 DLL 之前取消注册 FLS 回调。

注意

运行时计算机上可能注册了第三方产品,这些产品将在运行时将 DLL 注入到大多数进程中。 在这种情况下,在产品开发之外受影响的 DLL 可能会导致线程退出期间出现此错误。 如果无法根据上述指南重新生成此类 DLL,则唯一的选择可能是联系产品的供应商并请求此类修补程序,或卸载第三方产品。