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

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

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

进程可能会在线程退出时崩溃, (0xC0000005出现访问冲突异常,EXCEPTION_ACCESS_VIOLATION) 如果它已动态加载 (,例如通过调用 LoadLibraryA () ) 与 C Runtime 静态链接的本机 C++ DLL,并且 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 中静态链接,因此其 FLS 回调在该 DLL 本身中实现。 如果由于未经处理的异常而无法加载或卸载此 DLL,不仅会自动卸载 DLL,而且 C 运行时的 FLS 回调也将在 OS 中保持注册状态,即使在卸载 DLL 之后也是如此。 例如,当线程退出 (时,当 EXE 的 main() 函数返回) 时,OS 会尝试调用注册的 FLS 回调函数, (在这种情况下 _freefls) 现在指向未映射的进程空间,最终导致访问冲突异常。

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

  • 使用最新的 VC11 CRT 编译 DLL (例如,使用 VS2012 RTM) 生成 DLL。
  • 在编译 DLL 时使用 CRT DLL,而不是静态链接到 C 运行时;使用 /MD/MDd 而不是 /MT/MTd
  • 如果可能,请更正未处理异常的原因,从 DllMain中删除容易出现异常的代码段,并/或正确处理异常。
  • 实现自定义 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,则唯一的选择可能是联系产品的供应商并请求此类修复,或卸载第三方产品。