Memory leak detection
MFC로 프로그래밍을 하신 분이라면 아래와 같은 메모리릭 메세지를 자주 봤을 것이다.
Detected memory leaks!
Dumping objects ->
d:\documents\leak.cpp(28) : {95} normal block at 0x008B6EA0, 4 bytes long.
Data: < > 11 11 11 11
d:\documents\leak.cpp(27) : {94} normal block at 0x008B6E58, 7 bytes long.
Data: < > FF FF FF FF FF FF FF
Object dump complete.
그런데, MFC뿐만 아니라 일반적인 프로그램도 다 이런 메모리릭 기능을 켤 수 있다. 의외로 MFC가 아니면 안 된다고 생각하시는 분이 많다. 그렇지 않다. 또, new만 지원이 되고 malloc으로 한 것은 이 기능을 사용할 수 없다고 생각하시는 분도 있는데 역시 아니다. malloc, new 모두 가능하다. 그리고 생각보다 메모리릭을 디텍트 하는 기능은 만들기도 매우 간단하다. 알고보면 아무것도 아니다.

먼저, Visual Studio에서 사용하는 MS C Runtime 라이브러리에서 malloc, new는 모두 Win32 API HeapAlloc을 호출하는 것으로 구현이 되어있다. 아시다시피 new/delete는 C++ operator 형태로 되어있을 뿐, 실제 작동은 malloc과 다른 점이 하나도 없다. 리눅스의 libc는 malloc에 직접적인 구현이 다 들어가있지만 MSVC는 HeapAlloc으로 연결해놓았을 뿐이다. 물론, HeapAlloc/HeapFree의 동작방식도 malloc 동작 방식과 거의 차이가 없다. 다만 Windows XP SP2 이후 부터는 잦은 heap overflow attack을 막기 위해 약간의 보안 기능이 들어갔다 (그러나 워낙 적은 비트를 가지고 해야하기 때문에, 낮은 엔트로피로 brute-force로 공격하면 역시 뚫릴 수 밖에 없다).


그럼 차근차근 MSVC가 제공하는 메모리릭 검출 기능에 대해서 알아보자. 일단, MFC로 만든 프로그램에서 디버그 모드에서 아래 두 줄을 넣어보고 프로그램 종료 시 디버그 창의 메세지를 확인해보자.
memset(malloc(7), 0xFF, 7);
memset(new int, 0x11, 4);

Detected memory leaks!
Dumping objects ->
d:\documents\leak3.cpp(34) : {111} normal block at 0x0089B400, 4 bytes long.
Data: < > 11 11 11 11
{110} normal block at 0x0089B3B8, 7 bytes long.
Data: < > FF FF FF FF FF FF FF
Object dump complete.
먼저, malloc으로 7바이트를 할당하고 0xFF로 초기화를, new로 4바이트 만큼 잡고 0x11로 초기화를 하였다. 그러면 프로그램 종료시 메모리 릭을 탐지하는데, new로 할당한 4바이트는 어느 파일에서 어느 소스 라인에서 할당이 되었는지 친절히 알려준다. 그래서 이 라인을 더블 클릭하면 그 위치로 가준다. 반면, malloc으로 잡은 7바이트는 소스 정보 없이 그냥 메모리 릭만 보고 하고 있다.

MFC가 만들어주는 cpp 소스 앞 부분에는 아래와 같은 정의가 있다.
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
바로 이것으로 인해 new로 인한 메모리릭을 감지할 수 있다. 그렇다면 DEBUG_NEW는 어떻게 정의되어있을까. 아래 소스는 MFC 소스 중 일부인 afx.h에서 가져왔음.
// Memory tracking allocation
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#define DEBUG_NEW new(THIS_FILE, __LINE__)
#if _MSC_VER >= 1200
void AFX_CDECL operator delete(void* p, LPCSTR lpszFileName, int nLine);
#endif

void * __cdecl operator new[](size_t);
#if _MSC_VER >= 1210
void* __cdecl operator new[](size_t nSize, LPCSTR lpszFileName, int nLine);
void __cdecl operator delete[](void* p, LPCSTR lpszFileName, int nLine);
void __cdecl operator delete[](void *);
#endif
보다시피 new에 할당 크기인 nSize이외에 THIS_FILE, __LINE__이라는 두 매크로가 부가적으로 들어감을 알 수 있다. 이것때문에 특정 소스파일의 라인에서 메모리릭이 일어났음을 친절히 알려줄 수 있다.

여담으로 new[]와 new의 차이점에 대한 이야기가 종종 있는데, 적어도 MSVC에서는 new는 완벽하게 HeapAlloc으로 대체되기 때문에 아무런 차이가 없다. new[]로 하면 할당한 데이터 앞에 그 크기를 적는다는 소리가 들리는데 적어도 MSVC에서는 이건 거짓말이다. 그냥 malloc과 다를 바가 없다. 그래도 new[]/delete[]를 써야할 때는 그렇게 쓰는 것이 좋다. 아래 소스는 afxmem.cpp 일부분. 역시 new/new[]는 똑같다.

void* __cdecl operator new(size_t nSize, LPCSTR lpszFileName, int nLine)
{
return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);
}

#if _MSC_VER >= 1210
void* __cdecl operator new[](size_t nSize, LPCSTR lpszFileName, int nLine)
{
return ::operator new[](nSize, _NORMAL_BLOCK, lpszFileName, nLine);
}
#endif
iwongu님 지적 감사합니다. 이 부분 잘못되었습니다. 클래스 객체를 배열로 잡을 때, 그 수 만큼 생성자나 파괴자를 호출해야 합니다. 따라서 몇 개나 할당했느냐를 맨 앞에다가 저장을 하고 있습니다. 제가 혼동한 이유는 이 작업을 new[] 연산자 자체에서 해주는 것으로 생각했고, new와 new[] 차이가 없길래 그렇게 하지 않다고 생각한 것입니다. new[] 자체는 new와 다를 바가 없지만 컴파일러가 4바이트 만큼 공간을 더 할당하고 그 공간에 개수를 채워넣는 부분을 disassembly로 파악할 수 있었습니다.

Pseudo code로 대략 컴파일러가 만들어 놓은 어셈코드를 의역하면 다음과 같습니다.
;; Rect* pRect = new Rect[nCount];
void* p = malloc(sizeof(Rect) + 4);
(p + 4) 부분부터 개수만큼 Rect 생성자를 호출;
p 주소에 nCount를 기록;


이제 직접 디버거를 이용해서 new 연산을 계속 step-in을 해보자. 그러면 바로 위에 보여준 소스 코드를 지나 아래 소스 코드로 도달한다. (복잡한 각종 매크로 부분은 삭제했음)
void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)
{
#ifdef _AFX_NO_DEBUG_CRT
return ::operator new(nSize);
#else
void* pResult;
for (;;)
{
pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine);
if (pResult != NULL)
return pResult;
... 생략
}
보다시피 디버깅 모드가 아닐 때는 일반 new로 불러주고 그렇지 않을 때는 _malloc_dbg라는 디버깅 모드 malloc를 호출함을 알 수 있다. 여기서 또 다시 step-in을 계속하면 몇몇 함수를 거쳐 결국 MFC가 아닌 일반 C 런타임 함수인 _heap_alloc_dbg에 도달함을 알 수 있다. 따라서, 메모리 릭 감지 기능은 MFC 기능이 아닌 일반적인 MSVC의 기능이다. 아래는 dbgheap.c 소스 중 일부 (간략히 축약했음):
extern "C" void * __cdecl _heap_alloc_dbg(
size_t nSize,
int nBlockUse,
const char * szFileName,
int nLine
)
{
long lRequest;
size_t blockSize;
_CrtMemBlockHeader * pHead;

lRequest = _lRequestCurr;
blockSize = sizeof(_CrtMemBlockHeader) + nSize + nNoMansLandSize;
pHead = (_CrtMemBlockHeader*)_heap_alloc_base(blockSize);

/* commit allocation */
++_lRequestCurr;

/* keep track of total amount of memory allocated */
_lTotalAlloc += nSize;
_lCurAlloc += nSize;

if (_pFirstBlock)
_pFirstBlock->pBlockHeaderPrev = pHead;
else
_pLastBlock = pHead;

pHead->pBlockHeaderNext = _pFirstBlock;
pHead->pBlockHeaderPrev = NULL;
pHead->szFileName = (char *)szFileName;
pHead->nLine = nLine;
pHead->nDataSize = nSize;
pHead->nBlockUse = nBlockUse;
pHead->lRequest = lRequest;

/* link blocks together */
_pFirstBlock = pHead;

/* fill in gap before and after real block */
memset((void *)pHead->gap, _bNoMansLandFill, nNoMansLandSize);
memset((void *)(pbData(pHead) + nSize), _bNoMansLandFill, nNoMansLandSize);

/* fill data with silly value (but non-zero) */
memset((void *)pbData(pHead), _bCleanLandFill, nSize);

return (void *)pbData(pHead);
}
복잡해 보이지만 따라보면 무척 간단하다. 실제 필요한 데이터 크기에다 _CrtMemBlockHeader라는 크기와 NoMansLandSize를 더한 크기 만큼을 할당한다. 그런뒤 _heap_alloc_base를 호출하는데 이 함수는 정말 HeapAlloc만 호출한다. 그런 뒤에, 이제 메모리 릭을 찾기 위한 메모리 할당 정보를 리스트 형태로 쭉 연결한다. 보다시피 각 블락마다 할당 크기, 소스 파일 위치, 소스 라인 등을 기록한다. 실제 디버거로 살펴보면 아래 그림과 같다:

이렇게 모든 메모리 할당에 대해 별도의 부가 정보를 기억한다. 그렇다면 free할 때 이 정보를 삭제할 것이고, 프로세스 종료시 남아있는 리스트의 내용을 보여주면 메모리 릭을 알려 줄 수 있을 것이다. 정말 간단하지 않은가? 실제 내용을 덤프하는 함수 역시 dbgheap.c에 구현이 되어있다. 이 함수는 CRT 내부에서 프로세스 종료시 호출이 된다. 보다시피 친숙한 "Dumping objects"를 출력하는 부분을 볼 수 있다.
static void __cdecl _CrtMemDumpAllObjectsSince_stat(
const _CrtMemState * state,
_locale_t plocinfo
)
{
... 생략
_RPT0(_CRT_WARN, "Dumping objects ->\n");
... 생략
}


지금까지 MFC가, 아니 MSVC가 어떻게 메모리 릭을 찾는지 알아보았다. 그러면 맨 처음 예에서 보았듯이 제대로 찾지 못하였던 malloc로 인한 메모리릭 소스 위치를 확인하려면 어떻게 해야할까?
#define DEBUG_MALLOC(size) _malloc_dbg(size, _NORMAL_BLOCK, __FILE__, __LINE__)
#define malloc DEBUG_MALLOC
이렇게 malloc을 _malloc_dbg로 대체하면 된다. 그리고 MFC가 아닌 일반 Win32 프로젝트로 작업할 때, new/malloc으로 인한 메모리 릭을 잡기 위한 방법을 직접 예제 코드를 통해 알아보자:
#include < tchar.h >
#include < memory.h >

//#define _CRTDBG_MAP_ALLOC
#include < cstdlib >
#include < crtdbg.h >

#ifdef _DEBUG
#define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__)
#define DEBUG_MALLOC(size) _malloc_dbg(size, _NORMAL_BLOCK, __FILE__, __LINE__)
#define new DEBUG_NEW
#define malloc DEBUG_MALLOC
#endif // _DEBUG

int _tmain(int argc, _TCHAR* argv[])
{
int flags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
flags |= _CRTDBG_DELAY_FREE_MEM_DF;
flags |= _CRTDBG_LEAK_CHECK_DF;
_CrtSetDbgFlag(flags);

int *a, *b;
memset(a = (int*)malloc(7), 0xFF, 7);
memset(b = new int, 0x11, 4);
return 0;
}
대략 이런 방식으로 crtdbg.h를 추가하고 new 연산자를 다른 것으로 대체하면 된다. 약간의 혼동이 있을 수가 있는데, crtdbg를 include하기 전에 #define _CRTDBG_MAP_ALLOC를 해줄 경우 malloc을 별도로 DEBUG_MALLOC 등으로 대체할 필요는 없다. 컴파일러가 malloc이 중복 정의되어있다고 경고를 띄우니 잘 골라서 하면 된다. 그리고 이것만 가지고는 메모리 릭을 확인할 수 없고 반드시 _CrtSetDbgFlag를 이용해서 플래그를 고쳐야만 한다. 그러면 이제 Win32 프로그램에서도 메모리 릭을 확인할 수 있다.


마지막으로 heap overflow를 체크하기 위해 0xbaadf00d라는 재밌는 값을 심어놓는 경우도 있다. 이런 것을 통상 Canary value라고 하는데, canary는 새 이름으로 위험을 미리 감지하는 동물이란 뜻으로 사용되었다. 그래서 메모리를 free할 때, 이 부분의 값이 바뀌어져있으면 heap corruption 에러가 뜬다.
Heap overflow 보다 간단한 stack overflow도 직접 컴파일러가 만든 코드를 보면 스택 시작과 끝에 canary를 넣어서 체크함을 알 수 있다. MSVC 컴파일러에는 이것을 조절할 수 있는 옵션이 있다. 끝~
by object | 2007/08/18 15:27 | 컴퓨터 | 트랙백 | 핑백(4) | 덧글(14)
트랙백 주소 : http://minjang.egloos.com/tb/1414494
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at 미친병아리가 삐약삐약 : 20.. at 2007/08/20 09:26

... 들으려고 하는 사람들이 있을까? 전산과나 컴퓨터공학과 입학생도 줄고 C/C++은 쳐다도 안 본다는데, 전공필수나 되어야 억지로 듣지 별 관심 없을거다.. Memory leak detection 참고해볼만한 재미난 글.. ... more

Linked at 미친병아리가 삐약삐약 : 블로.. at 2007/08/31 15:10

... 차분하면서도 공감이 가는, 어쩔때는 아하~ 하는 깨우침을 주는 좋은 글들이 올라오는 블로그다.. 경험에서 오는 통찰력이 실린 글들은 배우는 즐거움을 느끼게한다.. art.oriented object님의 프로그래밍 관련 블로그.. 전산학에 관련된 내용과 시스템 프로그래밍에 관련된 흥미로운 글들이 자주 올라온다.. 외국 유학중인 이야기도 재미난 간접경험 ... more

Linked at Raptor 요놈!! 노력만이.. at 2007/09/21 11:28

... 디스플레이 하기http://www.codeproject.com/useritems/transparent__png.asp메모리 릭 탐지하기http://minjang.egloos.com/1414494DllMain에서 스레드 생성하면 데드락 걸릴까??http://blogs.msdn.com/oldnewthing/archive/2007/ ... more

Linked at NeverStop : Memo.. at 2007/11/28 17:03

... rtMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );자세한 내용은 _CrtSetReportMode를 참조하십시오.관련 자료 : http://minjang.egloos.com/1414494 ... more

Commented by 최재훈 at 2007/08/18 17:31
게임 분야에서 많이 쓰는 기법이군요. 항상 관리되는 환경 위주로 개발하다가 게임 업계로 오고 나서야 알았습니다만...
Commented by object at 2007/08/19 01:55
저는 managed 환경을 그다지 좋아하지 않는데, 메모리를 직접 관리하는데 들어가는 비용만큼 얻는 성능상의 이득을 생각하면 항상 손해라고 보기에는 무리가 있을 거 같아요. 그리고 일반적인 프로그램들은 (또 웬만큼 복잡한 녀석이라고 해도) 메모리 릭 잡는 것이 그렇게 힘들지도 않구요.
Commented by 구라마왕 at 2007/08/19 10:33
예전에 무수한 메모리 릭 프로그램을 짜놓고 좌절했던 기억이 나네요.
근데 남의 소스라 메모리 릭을 어떤 식으로 찾아내야 하는지 몰랐더랬죠..
결국 메모리 누출을 막지 못해서 프로그램을 완성하지 못했는데..
메모리 누출되는 곳을 아는 방법도 좀 알려주세요. ^^;
메시지에 보이는 숫자는 라인번호는 아닌 것 같던데, 어셈라인인가요?
Commented by object at 2007/08/19 15:19
메모리릭을 검출하는 방법은 위와 같은 방법도 있고 BoundsChecker라는 프로그램을 쓸 수도 있지요. 그리고 위 예제가 메모리 누출되는 곳을 알려주고 있답니다 ㅎㅎ 메세지에 보이는 것이 해당 소스 파일과 라인 번호입니다. 물론, 이 정보만 가지고 릭을 찾기가 항상 쉬운 것이 아닙니다. 그런 힘든 경우도 많이 존재합니다.
Commented by eslife at 2007/08/19 21:13
메모리릭 된 곳을 찾으려면 _CrtSetBreakAlloc() 함수에 Detected memory leaks! 경고 메시지에 나온 95 값을 입력하면 해당 라인에서 자동으로 디버거가 멈춥니다. ^^;
저희도 한참동안을 어느 곳에서 leak 이 발생하는지 몰랐다가 이 함수로 겨우 detect 한 적이 있어서요..
Commented by iwongu at 2007/08/20 11:29
좋은 내용 잘 읽었습니다. ^^ 그런데 new[]와 new는 실제 메모리를 alloc/free 하는 과정은 똑같겠지만 할당한 메모리를 사용하는 과정에서 달라지지 않나요? new[]로 할당한 메모리는 바로 free 만 하면 되는 것이 아니라 객체 개수만큼 소멸자를 불러주어야 하니까요. 이를 위해 어딘가에 객체의 크기나 개수같은 정보를 저장해야 하고 이 정보를 저장할 제일 좋은 위치는 바로 할당한 메모리의 제일 앞 부분이 아닐까 싶네요. ^^
Commented by object at 2007/08/20 15:28
좋은 지적 감사합니다. 말씀대로 new[]로 할 경우 클래스 객체인 경우 생성자를 여러번 불러야하는데요. Disassembly로 부 실제로 이 개수만큼 생성자/소멸자를 호출하는 코드가 생성됨을 알 수 있습니다. 그러니까 컴파일러 차원에서 이건 부가적으로 코드를 생성할 뿐, 실제 메모리 공간에 별도로 개수를 기록하는 것은 없는 것 같습니다.

다시말하면 메모리 할당은 malloc과 다른 것이 없고 할당 이후 생성자를 불러주는 코드가 덧 붙여집니다. vector constructor iterator라는 이름의 함수로 호출이 됨을 disassembly에서 확인할 수 있습니다. 리눅스에서의 구현은 저도 잘 모르겠습니다.
Commented by iwongu at 2007/08/20 16:43
네, new[]는 그런 식으로 컴파일러가 코드를 생성해야 할 것 같습니다. 하지만 다음과 같은 delete[] 코드를 컴파일하기 위해서는 메모리 어딘가 개수 정보가 들어가야 할 것 같은데... 혹시 delete[]의 disassembly도 보셨나요?

void delete_array(Foo* p) { delete [] p; }

그리고 객체의 크기는 저장할 필요가 없겠네요. (컴파일러가 delete[]가 어떤 클래스 타입에 대한 delete[]인지 알고 있어야 하니까)

혹시 객체의 크기를 컴파일러가 알고 있으므로 alloc 함수가 (어딘가) 기록해 놓은 해당 포인터에 할당된 크기를 객체의 크기로 나누어 개수를 알 수 있는 방법도 있겠지만 이렇게 되면 내부에서 사용되는 alloc 함수가 new[]와 밀접하게 구현이 되어 있어야 하겠죠.

그런데 이렇게까진 되어 있진 않을 것 같은데요... -_-;;
Commented by object at 2007/08/20 18:26
죄송합니다. 제가 틀렸네요. 보니까 new로 반환된 메모리 앞에다가 개수를 저장하네요. 저는 메모리 할당만 신경을 썼네요. 실제로 컴파일러가 만드는 코드를 보니 앞에다가 개수를 저장하는 부분이 있습니다. 죄송합니다. 헛소리를 해서요 -_-;

예를 들어, int하나 있는 Rect 클래스를 만들고
Rect* p = new Rect[count]; // count = 7;
를 컴파일 해보면 실제로 4바이트 더 더해서 new[]를 호출을 하구요. 그러니까 28+4=32바이트 만큼 메모리가 할당이 되고, 첫번째 4바이트에는 개수를 저장하는 코드가 만들어지네요.

004114DF call operator new[] (4110C3h)
004114E4 add esp,4
004114E7 mov dword ptr [ebp-110h],eax
004114ED mov dword ptr [ebp-4],0
004114F4 cmp dword ptr [ebp-110h],0
004114FB je wmain+10Eh (41153Eh)
004114FD mov ecx,dword ptr [ebp-110h]
00411503 mov edx,dword ptr [ebp-128h]
00411509 mov dword ptr [ecx],edx
0041150B push offset Rect::~Rect (41117Ch)
00411510 push offset Rect::Rect (411145h)
00411515 mov eax,dword ptr [ebp-128h]

eax에는 전체 할당받은 메모리가 저장되어있구요. 이것이 결국 다시 ecx로 올라오고, edx에는 count 변수값이 올라와서 저장이 되네요. 마찬가지로 delete[] 코드도 살펴보니 이 값을 읽어와서 그 만큼 루프를 돌립니다.

죄송합니다... 제대로 확인해보지도 않고 글을 써서 ㅎㅎ.. 저는 new[] 연산자 자체에서 이 부분을 처리하는 줄 알고 그런 부분이 없어서 아닌가보다 했는데 new[]는 그대로 malloc과 다를바가 없지만 그 앞뒤로 컴파일러가 많은 코드를 만들어서 붙여놓고 있네요.
Commented by object at 2007/08/20 18:36
그리고 _CrtSetBreakAlloc는 정말 좋은 팁이네요.. MFC 같은 경우 DYNCREATE 기능으로 만들어진 클래스 객체인 경우 메모리 릭으로 보고되는 위치가 실제 사용자가 만들어라고 명령한 위치와 다릅니다. 그래서 찾기가 꽤나 힘든 경우가 있는데요. 이런 경우 이걸 쓰면 참 좋겠네요. 감사합니다.
Commented by se at 2009/07/08 12:11
좋은 정보 잘 읽고 갑니다~
Commented by kuaaan at 2009/07/30 18:27
링크해갑니다~ 감사합니다. ^^
Commented by 나모 at 2010/08/15 12:08
canary(카나리아)는 메탄이나 일산화탄소에 매우 민감한 특징때문에 옛날 탄광에서 광부들이 안전을 위해 사용을 했던 노란색 새죠.
Commented by y2k at 2011/11/23 20:21
항상 좋은 글 잘 보고 있습니다 ^^

:         :

:

비공개 덧글

<< 이전 페이지 다음 페이지 >>





by 김민장 2008 이글루스 TOP 100
최근 등록된 덧글
개발자 입장에서의 수많은 ..
by Jiyoon at 02/04
저도 아들 돌잔치때 돌잡이 ..
by 박상욱 at 01/18
미국 대학원 원서 작성중에 p..
by 태클사이야 at 01/13
TO: 박PD 로그인 하지 않아..
by 박응용 at 01/10
http://gigglehd.com/zbx..
by dhunter at 12/28
우와.. 좋네요. 태반이 ..
by 윤광배 at 12/17
항상 좋은 글 잘 보고 있습니..
by y2k at 11/23
글이 좋아서 제 블로그에 담..
by 쏭섭 at 11/23
최근 등록된 트랙백
조엘 스폴스키의 강연 (Sta..
by 인덕원칸타타
[Redis] sds.c를 분..
by 조급하지말고 천천히
메뉴릿
이글루 파인더

website counter

Add to Google

rss

skin by 이글루스