2010. 10. 20. 13:57

Zombie Thread 오류로 인한 unknown/unloaded 오류 발견 & 대처법

이름을 정하는데 무척 어려웠습니다.
개발하면서, 어려가지 오류를 발견하게 되는데, 본 포스트에서 다루는 버그는 원인을 매우 찾기가 힘든 버그중 하나입니다.

일단, DLL에서 생성되어 동작중인 Thread가 있는데, Terminate가 되지 못하여 Zombie로 동작하고 있을때, 그 DLL이 FreeLibrary가 되면 그 Thread에 의해 Crash가 발생하게 됩니다. Terminate가 되지 못하는 Thread를 Zombie Thread라고 했는데, 물론, Terminate가 될 수 있는 잘-동작중인 Thread도, 버그에 의해 Terminate되지 못한 상황에서 FreeLibrary가 되어도 문제가 재현됩니다.

Crash가 발생하면, 덤프 수집에 의한 분석(!analyze -v)이 가능하리라 보는데, 위 상황, 즉, Thread가 남아 있는 상황에서 DLL이 FreeLibrary가 될때는, 어떠한 dump를 남기더라도 그 내용이 깨져서 정확한 분석이 어렵습니다. 그래서 일반적인 대응법이 아닌, 특수하게 처리하여 버그를 찾는 방법을 알려드리고자 합니다.

해결법은 간단합니다.

  • 어떤 Thread가 문제인지를 찾아라.
  • 그 Thread를 생성시키는 DLL API 최고 윗간계의 Call stack을 찾아라.
  • 해당 DLL API가 Create 개념이였다면, Destroy API를 호출하였는지 찾아라.
  • Destroy API를 호출하였다면, Destroy API에서 생성된 Thread를 종료시킬수 있게 만들어라.

와 같습니다.


오류 발견 방법

1) XP에서 아래와 같이 오류가 발생하였는데,

를 눌렀더니,

와 같이 ModName이 unknown으로 뜬 경우입니다.

2) 이벤트로그에서 다음과 같이 unknown이나 xxx.dll_unloaded이 포함된 경우입니다.

3) windbg로 포스트모텀 디버거로 등록된 상황에서, 아래와 같은 오류가 발생한 경우입니다. (unloaded_xx)


4) Vista 이상에서 다음과 같은 오류가 발생한 경우입니다.



오류 해결 방법

여러가지 방법이 있겠지만, procmon.exe를 통한 해결법을 소개하고자 합니다.
제가 샘플을 첨부하였는데, zombieapp.exe / zombie.dll를 통해 재현이 됩니다.
(소스 코드도 포함되었으니, 확인해 보시기 바랍니다.)

즉,
zombieapp.exe는 zombie.dll::fnZombie(VOID)를 호출합니다.
해당 함수는 그냥 무한 루프의 Thread 하나를 생성합니다.
ZOMBIE_API INT __cdecl fnZombie(VOID)
{
	// 호출시마다 좀비 Thread 생성
	HANDLE hThread = NULL;
	UINT nThreadId = 0;

	hThread = (HANDLE)::_beginthreadex(NULL, 0, &ThreadFn, NULL, 0, &nThreadId);
	if (NULL == hThread)
	{
		::MessageBox(NULL, TEXT("Error"), TEXT("Error"), MB_OK);
	}

	return 0;
}

//////////////////////////////////////////////////////////////////////////
// Thread
static UINT __stdcall ThreadFn(LPVOID pContext)
{
	INT i = 0;

	for (;;)
	{
		::Sleep(1000);
	}
}
이와 같아 DLL에서 생성되어 동작중인 Thread가 아직 있을때, FreeLibrary가 되면 Crash가 발생합니다.
아래의 zombieapp.exe를 통해 재현이 됩니다.

즉, DLL Load -> Zombie Make 했다가 DLL Free 하면 Crash가 위 "오류 발견 방법"과 같이 발생하게 됩니다.

일단, procmon.exe를 실행시키고, 아래와 같이 Filter합니다

그러면, 일단 비어있는 창이 뜹니다.
그리고, procmon.exe 메인창의 리스트 컨트롤의 Header에 오른쪽 클릭하여, 아래와 같이 Thread ID를 추가합니다.


이제 그러면, zombieapp.exe를 실행하고 crash를 발생시킵니다. 즉, 위 버튼 3개를 순서대로 누릅니다.
물론, Crash 창이 발생하면 닫아둬도 좋습니다.
이 상태에서 procmon.exe의 마지막을 주목합니다.

이를 통해 Thread ID가 1268인 놈이 문제를 잃으켰다고 볼 수 있습니다.
그럼, 1268 TID가 언제 생성되었을까요?

와 같이 "thread create"로 Find하면 zombieapp.exe가 생성한 Thread를 볼 수 있으며,
상세보기를 통해 1268이 생성된 Thread를 확인할 수 있습니다. !!!


그리고, 바로옆의 Stack을 눌러, 그때의 callstack을 아래와 같이 확인할 수 있습니다.


그런데, Zombie.dll+0x1031로 떠서 정확한 원인 지점 확인이 안됩니다.

그래서 아래와 같이 심볼을 맞춥니다.

dbghelp.dll은 windbg의 설치 경로로 하여야 동작이 됩니다. 그리고, Symbol path는 문제가 된 관련된 모듈들의 pdb 파일이 있는 경로로 설정하면 됩니다. 물론, Release이더라도 프로젝트 세팅에 Debug Info가 있어야만 pdb가 생성되겠죠.

그러면 위와 같이 Zombie.cpp의 28번 line에서 생성된 Thread가 Zombie Thread가 되어 문제를 잃으켰다는것을 확인할 수 있습니다. 물론, 해당 Thread가 종료된 다음 FreeLibrary되도록 본인들이 알아서 수정해야 합니다.
procmon.exe 특징상 너무 많은 이벤트가 발생하는데, "thread create"로 또한번더 Filter해주면 좀더 깔끔하게 확인할 수 있습니다. pdb 파일은 직접 빌드해서 연결시키세요~