이 문서에서는 C++ DLL이 정적으로 C CRT(런타임 라이브러리)에 연결되어 DLL 로드 또는 언로드 시퀀스가 처리되지 않은 예외에 의해 중단될 경우 스레드 종료 시 심각한 오류가 발생하는 문제를 해결하는 데 도움이 됩니다.
원래 제품 버전: Visual C++
원래 KB 번호: 2754614
증상
C++ DLL이 C CRT(런타임 라이브러리)에 정적으로 연결되면 처리되지 않은 예외로 인해 DLL 로드 또는 언로드 시퀀스가 중단될 경우 스레드 종료 시 심각한 오류가 발생할 수 있습니다.
C 런타임과 정적으로 연결된 네이티브 C++ DLL을 동적으로 로드한 경우(예: LoadLibraryA()를 호출하여) 초기화 또는 종료 중에 DLL이 처리되지 않은 예외를 생성한 경우 스레드 종료 시 프로세스에 액세스 위반 예외(0xC0000005, EXCEPTION_ACCESS_VIOLATION)가 발생할 수 있습니다.
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
}
원인
FLS(파이버 로컬 스토리지) 콜백 함수는 스레드가 종료될 때 Windows에서 호출되며 해당 함수의 주소가 더 이상 유효한 프로세스 메모리에 없습니다. 가장 일반적인 원인은 조기에 언로드되는 DLL에서 정적 CRT를 사용하는 것입니다.
C 런타임은 DLL 로드 시 초기화될 때 FlsAlloc()에 대한 호출을 통해 _freefls()라는 FLS 콜백 함수를 등록하지만, C 런타임은 로드되거나 언로드되는 동안 처리되지 않은 예외가 DLL에서 발생하는 경우 이 FLS 콜백의 등록을 취소하지 않습니다.
C 런타임은 DLL에 정적으로 연결되므로 해당 FLS 콜백은 해당 DLL 자체에서 구현됩니다. 처리되지 않은 예외로 인해 이 DLL을 로드하거나 언로드하지 못하는 경우 DLL이 자동으로 언로드될 뿐만 아니라 DLL이 언로드된 후에도 C 런타임의 FLS 콜백이 OS에 등록된 상태로 유지됩니다. 스레드가 종료되면(예: EXE의 main() 함수가 반환될 때) OS는 등록된 FLS 콜백 함수(이 경우 _freefls )를 호출하려고 합니다. 이 함수는 이제 매핑되지 않은 프로세스 공간을 가리키고 궁극적으로 액세스 위반 예외가 발생합니다.
해결
VC++ 11.0 CRT(VS 2012)에서 DLL 시작 중에 처리되지 않은 예외에 대한 FLS 콜백 정리를 보다 효율적으로 처리하도록 변경되었습니다. 따라서 소스 코드에 액세스할 수 있으므로 다시 컴파일할 수 있는 DLL의 경우 다음 옵션을 시도할 수 있습니다.
- 최신 VC11 CRT를 사용하여 DLL을 컴파일합니다(예: VS2012 RTM을 사용하여 DLL 빌드).
- DLL을 컴파일하는 동안 C 런타임에 정적 연결 대신 CRT DLL을 사용합니다. /MT 또는 /MTd 대신 /MD 또는 /MDd를 사용합니다.
- 가능하면 처리되지 않은 예외의 원인을 수정하고, 예외가 발생하기 쉬운 코드
DllMain부분을 제거한 다음/또는 예외를 제대로 처리합니다. - 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 정리를 수행하기 위해 OS에서 호출되어야 하므로 위의 잘못된 함수 포인터로 인해 액세스 위반 예외가 발생합니다. 따라서 이 문제에 대한 이상적인 해결 방법은 코드 자체를 수정하여 DLL이 언로드되기 전에 FLS 콜백이 등록 취소되도록 하는 것입니다.
참고 항목
런타임에 DLL을 대부분의 프로세스에 주입하는 타사 제품이 런타임 머신에 등록되어 있을 수 있습니다. 이러한 경우 제품 개발 외부에서 영향을 받는 DLL이 스레드 종료 중에 이 오류가 발생할 수 있습니다. 위에서 제안한 지침에 따라 이러한 DLL을 다시 빌드할 수 없는 경우 유일한 옵션은 제품 공급업체에 문의하여 수정을 요청하거나 타사 제품을 제거하는 것입니다.