지난 주에 프로그램 기능을 확장하면서 이상한 현상을 겪게 되었다. DLL을 확장한 별도의 모듈 타입을 만들어 쓰는데 그 녀석이 CRT만 연결하면 언로딩할 때 문제가 생기는 것이었다. 정확하게는 DLL을 FreeLibrary 할 때에는 문제가 없는데, 프로그램이 종료될 때 문제가 생겼다.
디버거를 돌려보니 프로그램이 종료되는데 이미 언로딩된 그 DLL의 메모리를 참조하는 것이 문제였다. 정확하게는 Fiber Callback을 호출하면서 문제가 생겼다. 왜 이럴까, 라는 생각을 하다가 여친님께서 납시셨다는 소식과 함께 디버깅 세션을 종료했다.
오늘 출근해서 디버깅을 하고 소스를 보고 하다가 엄청난 사실을 알게 되었다. 문제는 DllMain에 있는 lpReserved에 있었다. 우리가 만든 확장 DLL은 이 lpReserved를 사용해서 뭔가를 하도록 되어 있는데 VC++ CRT 소스에는 아래와 같은 엄청난 라인이 숨어 있었던 것이다. #ifdef _DEBUG가 아니라 #ifndef _DEBUG란 사실에 주의해야 한다. 즉, lpreserved를 비교하는 루틴은 릴리즈 버전에만 포함된다는 사실이다. 결국은 우리가 이 lpresreved로 NULL이 아닌 포인터를 공급했고, 그래서 CRT 정리 루틴이 타지 않았고, 프로세스 종료 시에 CRT가 싸놓은 Fiber Callback을 타면서 크래시가 난 것 이었다.
#ifndef _DEBUG
if ( lpreserved == NULL )
{
#endif /* _DEBUG */
/*
* The process is NOT terminating so we must clean up...
*/
/* Shut down lowio */
_ioterm();
_mtterm();
/* This should be the last thing the C run-time does */
_heap_term(); /* heap is now invalid! */
#ifndef _DEBUG
}
#endif /* _DEBUG */
나의 순진한 믿음, lpReserved는 말 그대로 예약된 것일 거라는 사실이 화근이 되었다. 크게는 남의 것을 함부로 건드린 죄라고 할 수 있겠다. 어쨌든 이제는 알게 되었지만 lpReserved는 아무 곳에도 사용되지 않는 값은 아니다. 디버깅 정보에 의하면 lpreserved는 보통 0이고, 특별하게 프로세스가 종료될 때 같이 DLL이 정리되는 상황에서는 1이 넘어 왔다.