2008. 9. 2. 10:34

Detecting and Isolating Memory Leaks Using Microsoft Visual C++

번역 : codemuri (codemuri.tistory.com)

 

Edward Wright

Microsoft Corporation

 

May 1999

 

Summary

C/C++ 프로그래밍 언어로 개발된 애플리케이션에서 문제가 되는 행위를 일으키는 감지하기 어려운 버그인, 메모리 누수(memory leaks)의 위치를 알아내고, 격리시키기 위한 마이크로소프트 비쥬얼 C++ 디버거에 의해 제공되는 툴을 사용하는 방법을 설명한다.

 

Introduction. 1

Enabling Memory Leak Detection. 2

Using _CrtSetDbgFlag. 3

Setting a Breakpoint on a Memory Allocation Number. 4

Comparing Memory States. 5

Additional Information. 7

 

Introduction

 

메모리를 동적으로 할당하고 해제하는 능력은 C/C++ 프로그래밍 언어의 가장 강력한 기능중의 하나이다, 하지만 중국인 철학자 Sun Tzu 가 지적했듯이, 가장 위대한 힘은 가장 취약한 약점일 수 있다. 메모리-제어 실수가 가장 일반적인 버그의 원인이기에 C/C++ 애플리케이션에서 이것은 확실히 진리이다. 가장 미묘하고 감지하기 어려운 버그중의 하나는 메모리 누수이다 이전에 할당된 메모리를 철저히 해제해야 하는 것에 대한 불이행. 딱 한번만 일어나는 작은 메모리 누수는 발견되지 않을 지도 모른다, 하지만 대량의 메모리가 누수되거나, 또는 점진적으로 누수되는 프로그램은 메모리 고갈의 결과로서 실패(failure)로 끝나는 빈약한 ( 그리고 서서히 감소되는) 성능으로 이어지는 징후를 나타낼 것이다. 더욱 나쁘게는, 누수되는 프로그램은 너무 많은 메모리를 소진시켜서, 어느 프로그램이 실제로 문제를 일으키는지에 대한 어떠한 실마리도 없이 사용자를 내버려둔채, 다른 프로그램을 종료시킨다. 덧붙여서, 무해한 메모리 누수조차도 다른 문제들의 전조가 될 수 있다.

 

다행히, 비쥬얼 C++ 디버거와 CRT 라이브러리는 메모리 누수를 발견하고, 식별하기 위한 효과적인 도구들의 집합을 제공한다. 이 글은 효과적으로 그리고 체계적으로 메모리 누수를 격리시키기 위한 이러한 도구들에 대한 사용법을 설명한다.

 

Enabling Memory Leak Detection

 

메모리 누수를 감지하기 위한 근본적인 도구들은 디버거와 CRT 디버그 heap 함수들이다. 디버그 heap 함수를 활성화하기 위해서는, 아래의 문장을 프로그램내에 포함시켜야 한다.

 

#define _CRTDGB_MAP_ALLOC

#include <stdlib.h>

#include <crtdbg.h>

 

#include 문은 위에 보여지는 순서대로 정렬되어야만 한다. 만일 순서를 바꾸면, 당신이 사용할 함수들은 올바르게 동작하지 않을지도 모른다. crtdbg.h malloc free 함수를 그들의 디버그 버전, _malloc_dbg _free_dbg 로 맵핑하는 것을 포함하여, 메모리 할당과 해제를 추적한다. 이러한 맵핑은 디버그 빌드(, _DEBUG 가 정의되었을때에만)에서만 일어난다. 릴리즈 빌드는 보통의 malloc free 함수를 사용한다.

 

#define 문은 CRT heap 함수의 버전을 디버그 버전으로 맵핑한다. 이 문장은 꼭 필요한 것은 아니지만, 그것이 없을 경우, 메모리 누수 덤프(dump)는 덜 유용한 정보를 포함할 것이다.

 

위에 보여진 문장을 추가한 후에, 프로그램내에 아래에 나타난 문장을 포함시킴으로써 메모리 누수 정보를 덤프할 수 있다.

 

_CrtDumpMemoryLeaks();

 

디버거하에서 프로그램을 실행할 때, _CrtDumpMemoryLeaks 디버그 탭의 Output 윈도우에 메모리 누수 정보를 표시한다. 메모리 누수 정보를 아래와 같이 보여준다.

 

Detected memory leaks!

Dumping objects ->

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18}

   normal block at 0x00780E80, 64 bytes long.

 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.

 

#define _CRTDBG_MAP_ALLOC 문을 사용하지 않았다면, 메모리 누수 덤프는 아래와 같이 보여질 것이다.

 

Detected memory leaks!

Dumping objects ->

{18} normal block at 0x00780E80, 64 bytes long.

 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.

 

보는 바와 같이, _CrtDumpMemoryLeaks _CRTCBG_MAP_ALLOC 이 정의되어 있을 경우에는 더욱 유용한 정보를 제공한다. _CRTDBG_MAP_ALLOC 이 정의되어 있지 않은 경우, 디스플레이는 아래의 것들을 보여준다.

 

l       메모리 할당 숫자({} ) – the memory allocation number

l       블락 타입(normal, client, or CRT) – the type of block

l       16진수 형태의 메모리 위치 – the memory location in hexadecimal form

l       바이트 단위의 블락 사이즈 (the size of the block in bytes)

l       첫번째 16 바이트의 내용(역시 16진수 형태) – the contents of the first 16 btyes

 

_CRTDBG_MAP_ALLOC 이 정의되어 있는 경우, 디스플레이는 또한 누수된 메모리가 할당되었던 파일을 보여준다. 파일 이름뒤에 괄호안에 있는 수(아래 예에서는 20)는 그 파일안의 라인 넘버이다. 라인 넘버와 파일 이름을 포함하는 출력 라인을 더블클릭하면,

 

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18}

   normal block at 0x00780E80, 64 bytes long.

 

커서는 소스파일 내의 메모리가 할당된 라인으로 점프할 것이다 (위 예의 경우, leaktest.cpp의 라인넘버 20). 출력된 라인을 선택하는 것과 F4를 누르는 것은 같은 효과를 가질 것이다.

 

Using _CrtSetDbgFlag

 

_CrtDumpMemoryLeaks 를 호출하는 것은 프로그램이 항상 같은 곳에서 종료한다면 무난할 것이다. 그러나, 프로그램이 다수의 위치에서 종료될 수 있다면? 각각의 가능성 있는 종료에다가 _CrtDumpMemoryLeaks의 호출을 놓는 대신에, 당신의 프로그램의 시작 부분에 아래의 호출을 포함시킬 수 있다.

 

_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

 

다른 위치로 출력을 보내기 위한 _CrtSetReportMode를 사용하는 것에 대한 정보는, Visual C++ 문서의 _CrtSetReportMode를 보라 (Visual C++ Programmer’s Guide, Run-Time Library Reference, Debug Function Reference).

 

Setting a Breakpoint on a Memory Allocation Number

 

메모리 누수 리포트내의 파일 이름과 라인 넘버는 누수된 메모리가 할당된 위치를 가르쳐 준다, 하지만 메모리가 어디에서 할당되었는 지를 아는 것은 문제를 식별하기 위해서 항상 충분한 것은 아니다. 종종 할당은 프로그램이 실행되는 동안에 여러 번 호출될 것이다, 그러나 특정 호출에 대해서만 메모리가 누수될 지도 모른다. 문제를 식별하기 위해서, 누수된 메모리가 어디에서 할당되었는 지뿐만 아니라, 누수가 일어나는 조건(conditions)도 역시 알고 있어야만 한다. 이것을 가능하게 하는 정보는 메모리 할당 넘버이다. 이것은 결과가 표시될 때 파일이름과 라인넘버의 뒤에서, { } 괄호안에 나타나는 수이다. 예를 들면, 아래 출력에서, “18”은 메모리 할당 넘버이다. 누수된 메모리는 프로그램에서 할당된 메모리의 18번째 블락(the 18th block of memory)이라는 것을 의미한다.

 

Detected memory leaks!

Dumping objects ->

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18}

   normal block at 0x00780E80, 64 bytes long.

 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.

 

CRT 라이브러리는 CRT 라이브러리에 의해 할당되는 메모리 또는 MFC와 같은 다른 라이브러리에 의해서 할당되는 메모리를 포함하여, 프로그램이 실행되는 동안에 할당되는 모든 메모리 블락들을 카운트 한다. 그리하여, 할당 넘버 n인 객체(object)는 프로그램에서 할당된 n번째 객체일 것이다 그러나 당신의 코드에 의해 할당된 n번째 코드는 아닐 수 있다. (대부분의 경우에, 그렇지 않을 것이다.)

 

메모리가 할당된 위치에 중단점을 설정하기 위해서 할당 넘버를 사용할 수 있다. 이렇게 하기 위해서는, 프로그램의 시작 근처에 중단점을 설정하라. 그 지점에서 프로그램이 멈추었을 때, QuickWatch 대화 상자나 Watch 윈도우를 통해서 그러한 메모리 할당 중단점을 설정할 수 있다. 예를 들면, Watch 윈도우에서 이름 칼럼(Name column)안에 아래의 문장을 입력하라.

 

_crtBreakAlloc

 

만일 multithreaded DLL 버전의 CRT 라이브러리 (/MD 옵션)를 사용하고 있다면, 아래 보여지는 바와 같은 컨텍스트 연산자(context operator)를 반드시 포함해야 한다.

 

{,,msvcrtd.dll}_crtBreakAlloc

{,,msvcr71d.dll}_crtBreakAlloc=89 (.Net 2003)

{,,msvcr80d.dll}_crtBreakAlloc=1993 (.NET 2005)


위에 dll 파일은 모듈창에서 확인가능 함.

 

이제, RETRUN을 눌러라. 디버거는 그 호출을 평가(evaluate)하고 Value 칼럼안에 그 결과를 둔다. 이 값은 메모리 할당에 대한 어떠한 중단점도 설정하지 않았다면 -1일 것이다. 중단점을 설정하고 싶은 메모리 할당의 할당 넘버를 Value 칼럼의 값에다가 입력하라 예를 들면, 앞의 출력에서 보여준 할당에 중단점을 설정하기 위해서 18을 입력)

 

관심이 있는 메모리 할당에 대한 중단점을 설정한 후에, 디버깅을 계속할 수 있다. 할당 순서가 바뀌지 않도록 이전 실행과 똑 같은 상태하에서 프로그램을 실행하는 것에 주의하라. 프로그램이 지정된 메모리 할당에서 중단되었을 때, 그 메모리가 할당된 상태를 결정하는 Call Stack 윈도우와 다른 디버그 정보를 볼 수 있다. 필요하다면, 그 객체에 일어나는 것을 보기 위해 그 지점으로부터 프로그램의 실행을 계속하거나 어쩌면 왜 그것이 올바르게 해제 되지 않았는 지를 측정할 수 있을 것이다 (그 객체에 대한 데이터 중단점(Data breakpoint)을 설정하는 것은 유용할 것이다.)

 

디버거에서 메모리 할당 중단점을 설정하는 것이 보통 더 쉬울지라도, 기호에 따라 그것들을 당신의 코드에서 설정할 수 있다. 코드 안에 메모리 할당 중단점을 설정하기 위해서는, 아래와 같은 라인을 추가하라 ( 18번째 메모리 할당의 경우)

 

_crtBreakAlloc = 18;

 

다른 방법으로는, 같은 효과를 가진 _CrtSetBreakAlloc 함수를 사용할 수 있다.

 

_CrtSetBreakAlloc(18);

 

Comparing Memory States

 

메모리 릭의 소재를 파악하기 위한 또 다른 기법은 키 포인트에다가 애플리케이션의 메모리 상태에 대한 스냅샷(snapshots)을 포함시키는 것이다. CRT 라이브러리는 메모리 상태의 스탭샷을 저장하기 위해 사용할 수 있는 구조체형, _CrtMemState 를 제공한다.

 

_CrtMemState s1, s2, s3;

 

주어진 지접에서 메모리 상태의 스냅샷을 찍기 위해서는, _CrtMemCheckpoint 함수에다가_CrtMemState 구조체를 전달하라. 이 함수는 구조체 안에다가 현재 메모리 상태의 스냅샷을 채운다.

 

_CrtMemCheckpoint( &s1 );

 

_CrtMemDumpStatistics 함수에다가 그 구조체를 전달함으로써 어떤 지점에서든 _CrtMemState 구조체의 내용을 덤프할 수 있다.

 

_CrtMemDumpStatistics( &s3 );( &s1 );

 

이 함수는 아래와 같이 보이는 메모리 할당 정보의 덤프를 출력한다.

 

0 bytes in 0 Free Blocks.

0 bytes in 0 Normal Blocks.

3071 bytes in 16 CRT Blocks.

0 bytes in 0 Ignore Blocks.

0 bytes in 0 Client Blocks.

Largest number used: 3071 bytes.

Total allocations: 3764 bytes.

 

메모리 누수가 코드의 어떤 코드 섹션에서 일어났는지를 측정하기 위해서는, 그 섹션의 앞과 뒤에다가 메모리 상태의 스냅샷을 찍고, 그러고나서 두개의 상태를 비교하기 위해서 _CrtMemDifference 를 사용할 수 있다.

 

_CrtMemCheckpoint( &s1 );

// memory allocations take place here

_CrtMemCheckpoint( &s2 );

 

if ( _CrtMemDifference( &s3, &s1, &s2) )

   _CrtMemDumpStatistics( &s3 );

 

이름이 암시하듯이, _CrtMemDifference 는 두개의 메모리 상태 (처음 두개의 파라미터들)를 비교하고 두개의 상태에 대한 차이점의 결과를 (세번째 파라미터) 만들어 낸다. 프로그램의 시작과 끝에다가 _CrtMemCheckpoint 호출을 두는 것과 그 결과를 비교하기 위해서 _CrtMemDifference 를 사용하는 것은 메모리 누수를 체크하기 위한 또 다른 수단을 제공한다. 누수가 발견되면, 프로그램을 분할하기 위해 _CrtMemCheckpoint 호출을 사용할 수 있다. 그리고 이진 검색 기법(binary search technique)를 사용하여 그 누수의 소재를 알아낼 수 있다.

 

Additional Information

 

아래의 글은 MFC 애플리케이션에서 특수한 강조 기법(special emphasis)을 가진 메모리 누수 디버깅에 대해서 논의한다.

 

l       Blasczak, Mike. Stunt Debugging: Diagnosing memory Leaks, Visual C++ Developer. Pinnacle Publishing, 1997.

 

아래 책은 메모리 누수 디버깅에 대한 어떠한 정보도 담고 있지 않다, 그러나 만일 당신이 백지(white paper)를 작성하고 있고 고대의 중국 철학자로부터 인용 어구(quote)를 원한다면 유용할 지도 모른다.

 

l       Tzu, Sun. The Art of War. Various publishers.