本文可帮助你解决以下问题:如果 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,因此可以重新编译,可以尝试以下选项:
- 使用最新的 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,则唯一的选择可能是联系产品的供应商并请求此类修补程序,或卸载第三方产品。