본문으로 바로가기

프로세서 핸들 조회

category 개발언어/c++ 2016. 7. 18. 05:31

 

일반적으로 가장 잡기 힘든 버그의 하나로서 메모리 누수, 메모리 Overwrite등을 꼽을 수 있다. 이런 문제점을 해결하기 위해 CRT(C Runtime library)에서는 여러가지 다양한 메모리 관련 디버그 함수들을 제공한다. 그러나 이것들이 디폴트로 사용하기 힘들게 꺼져 있기 때문에 대부분의 프로그래머들은 이 사실을 알지 못하는 경우가 많다. 그래서 이 글에서는 CRT의 디버그 관련 함수들에 대해 알아보고 어떻게 사용하는 것이 좋은지에 대해 논해 보려고 한다.

John Robbins(필자가 가장 좋아하는 프로그래머 중의 한명)가 지은 Debugging Applications 이라는 책에도 좋은 내용들이 있으니 참고하기 바란다. 그러나 여기 나온 팁은 그 책에는 나와 있지 않은 것이다. Numega Bounds Checker나 Rational Purify등의 툴이 비싸서 엄두도 못내는 분들께는 좋은 내용이 되리라 믿는다

메모리 누수

개요

CRT가 출력해 주는 메모리 누수 리포트 결과를 간혹 본적이 있는 사람도 있을 것이다. 만약 MFC를 주로 사용한다면 이와 같은 메시지를 자주 볼 수 있다. 왜냐하면 MFC에서는 기본적으로 CRT 디버그 기능을 켜기 때문이다. 메시지 내용은 다음과 같다

Dumping objects ->

 

{65} normal block at 0x003748B0, 48 bytes long.

 

Data: <@H7 @H7 @H7 > 40 48 37 00 40 48 37 00 40 48 37 00 CD CD CD CD

 

{64} normal block at 0x00374840, 48 bytes long.

 

Data: < H7 H7 H7 > B0 48 37 00 B0 48 37 00 B0 48 37 00 CD CD CD CD

 

Object dump complete.

이 메시지의 내용은 두개의 블록이 할당된다음 해제되지 않았다는 것을 의미한다. 그렇다면 우선 이 메시지들에 대해 대략 알아보도록 하자

우선 {64}, {65}와 같은 것은 메모리에 할당된 순서로서, 각각 64번째, 65번째 할당된 메모리가 해제되지 않았음을 나타낸다. Normal block이라 함은 사용자에 의해 일반적인 new, delete, malloc등으로 할당된 메모리라는 뜻이다. 그리고 그 메모리 포인터 주소를 알려주고 있으며, 몇 바이트짜리 메모리 블록인지도 나와 있다

Data:… 라인은 그 메모리의 선두번지로부터 실제 메모리의 내용이 어떤 값을 포함하고 있는지를 나타내는 것이다

그러나 위의 메시지를 보았듯이 실제로는 아무 도움이 되지 않는다는 것을 금방 알수 있을것이다. 왜냐하면 메시지의 내용이 너무 암호와같이 복잡하다는데 문제가 있다. 몇줄짜리 프로그램이라면 또 모르겠으나, 거대한 프로그램인 경우 실제로 64,65번째 할당된 메모리를 순서대로 추적하는 것은 너무나도 어려우며, 어떤 포인터에 어느 번지의 메모리가 할당되었는지를 확인하는 것도 불가능에 가깝다고 할 수 있다. 또 자신이 만든 클래스나 구조체가 몇 바이트짜리인지를 일일이 확인한다는 것도 불가능하다.

그렇다면, 이 문제를 어떻게 해결해야 할 것인가? 실제로 CRT에는 여러가지 도우미 함수들을 포함하고 있어서 이 문제를 좀 더 쉽게 해결할 수 있도록 해 준다. 그러나 제대로 알고 사용하지 않는다면 결국에는 또다른 암호 코드만을 추가로 더 얻게 될 뿐이라는 것도 알아야 한다

샘플코드

#include "stdafx.h"

 

int main()

{

int *p = new int;

 

return 0;

 

}

위의 코드는 int *를 할당하고 해제하지 않았으므로 명백한 메모리 누수이다. 그러나 이 프로그램을 빌드하고 디버그해봐도 프로그램 종료시 아무런 메시지를 남기지 않는다. 즉 메모리가 샌것인지, 혹은 Overwrite가 일어났는지 등을 확인할 길이 전혀 없다. 그러면 프로그램 일부를 수정해 보도록 한다

#include "stdafx.h"

#include <crtdbg.h>

 

int main()

{

 

int *p = new int;

 

_CrtMemDumpAllObjectsSince(0);

 

return 0;

}

_CrtMemBumpAllObjectsSince를 사용하기 위해 crtdbg.h를 인클루드 하고, 프로그램 종료 직전에 함수를 호출했다 디버그를 시작하고 프로그램을 종료하면 출력결과는 다음과 같다

Dumping objects ->

 

{64} normal block at 0x00374838, 4 bytes long.

 

Data: < > CD CD CD CD

 

Object dump complete.

The thread 0x65C has exited with code 0 (0x0).

The program 'test.exe' has exited with code 0 (0x0).

위에서 설명한것과 비슷한 종류의 메시지가 포함되어 있는 것을 알 수 있을 것이다. 이 메모리는 64번째 할당된 일반 메모리이며 0x00374838번지에 4바이트 할당되었음을 알 수 있다. 또 데이터 내용은 16진수로 CD CD CD CD이다 이 정보만으로도 많은 것을 알 수 있다. 예를들어 데이터가 CD CD CD CD라는 것은 할당만 해놓고 전혀 초기화를 하지 않았다는 의미이다. 단순한 위의 프로그램 만으로도 사용자가 처음 할당한 메모리가 64번째만에 할당되었다. 이유가 무엇일까? 이유는 간단하다. main함수가 호출되기 이전에 이미 많은 메모리 할당 요청이 있었고, 그것은 프로그램을 실행시키기 위해 운영체제나, CRT가 이미 사용했기 때문이다. 위의 프로그램은 단순하기 때문에 어디서 메모리가 샜는지 한눈에 척 알 수 있다. 그러나 메모리를 수십~수백번씩 할당했다 해제하는 일반 애플리케이션에서는 어떻게 정확히 64번째 할당된 메모리를 찾아낼 수 있을까? CRT에서 내준 정보는 CRT를 이용해 분석 가능하다 main함수가 처음 시작되기 전에 다음의 함수를 사용하도록 하자

_CrtSetBreakAlloc(64);

 

int *p = new int;

이 함수는 64번째 메모리 할당이 발생할 경우 프로그램 실행을 중지하고 디버거로 제어를 넘기라는 의미이다. 실제로 프로그램을 실행시켜 보면, "crtdbg.exe의 0x00411cb7에 처리되지 않은 예외가 있습니다. 사용자 중단점"과 같은 메시지를 출력하면서 프로그램 실행을 중지하게 된다. 브레이크 포인트가 가리키는 위치는 CRT의 메모리 할당 함수 내부이며, Call Stack을 따라가 보면 어느곳에서 할당이 일어났는지 바로 알수 있게된다.

전역 변수와 CRT 디버깅

다음과 같은 프로그램을 보도록 하자

#include "stdafx.h"

#include "crtdbg.h"

#include <crtdbg.h>

 

class A

{

public:

A() { p = new int; }

 

private:

int *p;

};

 

A a;

 

int APIENTRY _tWinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPTSTR lpCmdLine,

int nCmdShow)

{

_CrtSetBreakAlloc(64);

 

_CrtMemDumpAllObjectsSince(0);

 

return 0;

}

이 프로그램을 디버그 해 보면 똑 같은 메모리 누수를 보고하긴 하지만, 그러나 64번째 메모리 할당에서 정지하지 않는다. 이유가 무엇일까?

이유는 간단하다. A라는 클래스의 인스턴스인 a가 전역변수로 선언되었기 때문에 main함수가 호출되기 이전에 생성되기 때문이라는 것이다. a가 이미 생성되고 난 다음에 브레이크 포인트를 설정하니, 브레이크 포인트가 먹힐리가 없다. 그럼 방법이 없는 것일까?

답은 간단하다. 모든 할당이 일어나기 직전에 프로그램을 정지시켜놓고, 64번째 메모리 할당이 일어날 때 브레이크 하라는 명령을 주면 된다. 그럼 어떻게 하면 될까? 다음과 같이 하도록 한다.

  1. CRT소스에서 WinMainCRTStartup()함수를 찾아낸다. 이 함수는 실질적인 main함수이며, 프로그램이 로드되기 전에 가장먼저 실행된다. 이 함수 내부에서 여러분이 정의한 main 또는 WinMain함수를 호출하게 된다. 이 함수는 파일의 찾기 기능을 이용하거나, 또는 crt0.c파일을 바로 열어서 찾아도 된다. 그러나 더 간단한 방법은 main함수에 BP를 찍어놓고, 한번 실행시킨다음 call stack을 거슬러 올라가는 방법이다.
  2. WinMinCRTStartup함수의 시작부분에 BP를 찍어놓고 다시 디버거를 시작시킨다
  3. Watch창을 열어 _crtBreakAlloc 변수를 확인해 본다. 아마 -1일 것이다.
  4. 이 변수값을 원하는 메모리 할당 번지(위의 경우64)로 바꾼다.
  5. 다시 실행시키면 64번째 메모리 할당을 하기 전에 정지한다.

이 기술은 코드를 재 컴파일 하지 않아도 디버거 상에서 바로 브레이크 포인트를 수정할 수 있다는 장점이 있다. 현재 이 방법 보다 간단하게 할 수 있는 방법은 현재 연구중에 있다. 좋은 결과가 나오는대로 다시 여러분들에게 알려드리도록 하겠다.

이상 몇가지 메모리 누수를 찾아내는 방법을 살펴보았다. 그러나 주의할 것은 반드시 crtdbg.h를 같이 인클루드 해야 한다는 것이며 _DEBUG매크로가 정의되어 있을때에만 제대로 동작한다는 것이다.

CRT Debug 기능 사용법 2

요즘 CRT의 디버그 기능을 연구하기 시작하면서, 그동안 정말 좋은 기능들을 여럿 묵혀놓았다는 느낌을 지울수가 없습니다. 어렵게 메모리 관련 디버깅 루틴을 만들지 않아도, 너무나도 정확히 메모리 관련 에러를 잡아주니 STL을 처음 쓸 때 만큼이나 편리하게 느껴지더군요. 그럼 그동안 제가 연구한 것에 대해 보고드리도록 하겠습니다

지난 내용의 메모리 누수가 아닌 것 까지 모두 보고하는 문제

지난번 예제에서 약간만 더 수정해 보자

#include "stdafx.h"

#include <crtdbg.h>

#include <list>

using std::list;

 

typedef list<int> IntList;

typedef IntList::iterator IntListIter;

 

IntList myList;

 

int main()

{

_CrtMemDumpAllObjectsSince(0);

 

return 0;

}

 

위의 프로그램은 메모리 누수를 한 개 보고한다. 위치는 myList의 생성자이다. 그러나 정말 그것이 샌것일까? 그렇다면 STL은 항상 메모리 누수가 있다는 말인가.. 이것저것 고민하던 중에, 진짜 Main함수는 {{{WinMainCRTStartup()}}}라는 사실이 생각났고, 디버그 상에서 {{{WinMainCRTStartup()}}} 메소드의 끝까지 따라가 보았다. 그랬더니 다음과 같은 루틴을 찾을 수 있었다. (처음에는 숨겨진 함수를 찾았다고 생각했으나, 알고봤더니 MSDN에 이미 문서화 되어있는 함수였다. 역시 소스보다는 문서를 찾아보는 것이 우선이다 -.-)

{{{

/* Dump all memory leaks */

if (!fExit && _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) & _CRTDBG_LEAK_CHECK_DF)

{

fExit = 1;

_CrtDumpMemoryLeaks();

}

_CrtSetDbgFlag이라는 함수는 CRT디버거의 전역 플랙을 셋팅하는 함수이다. 위에서 보다 시피 이미 CRT의 메인함수가 종료할 때 메모리 누수를 검사하고 있었던 것이다. 다만 디폴트로 꺼져 있었으며 열쇠는 _CrtSetDbgFlag이라는 함수가 쥐고 있다. MSDN에서 찾아본 결과 다음과 같다.

_CrtSetDbgFlag함수는 다섯개의 Flag이 있다.

_CRTDBG_ALLOC_MEM_DF : 디폴트로 켜져 있으며, 디버그 버전에서 모든 메모리 할당이 일어날 때마다 추적 가능하도록 특별한 기능을 추가해 둔다. 이 플랙이 켜져 있어야 메모리 누수를 안전하게 검사 할 수 있다.

_CRTDBG_DELAY_FREE_MEM_DF : delete, free등으로 삭제되어도 바로 삭제되지 않고, CRT의 메모리 관리소에 남아 있다가 프로그램 종료시에 완전히 삭제된다.

_CRTDBG_CHECK_ALWAYS_DF : 모든 메모리관련 연산에서 _CrtCheckMemory를 호출한다. 이 메소드는 이후에 다시 살펴볼 것임

_CRTDBG_CHECK_CRT_DF : CRT가 내부적으로 할당한 블록도 메모리를 체크할 때 포함한다. 일반적으로는 CRT가 할당한 블록은 메모리 체크에서 제외된다. 일반적으로 사용하지 않는다

_CRTDBG_LEAK_CHECK_DF : 프로그램이 완전히 종료되기 직전에 아직 해제되지 않은 메모리가 있는지 검사한다. 프로그램의 종료 포인트가 여러군데 있는 경우에 사용하면 일일이 _CrtDumpMemoryLeaks 메소드를 호출하지 않아도 자동적으로 메모리 누수를 검사할 수 있게된다.

일반적으로 가장 잡기 힘든 버그의 하나로서 메모리 누수, 메모리 Overwrite등을 꼽을 수 있다. 이런 문제점을 해결하기 위해 CRT(C Runtime library)에서는 여러가지 다양한 메모리 관련 디버그 함수들을 제공한다. 그러나 이것들이 디폴트로 사용하기 힘들게 꺼져 있기 때문에 대부분의 프로그래머들은 이 사실을 알지 못하는 경우가 많다. 그래서 이 글에서는 CRT의 디버그 관련 함수들에 대해 알아보고 어떻게 사용하는 것이 좋은지에 대해 논해 보려고 한다.

John Robbins(필자가 가장 좋아하는 프로그래머 중의 한명)가 지은 Debugging Applications 이라는 책에도 좋은 내용들이 있으니 참고하기 바란다. 그러나 여기 나온 팁은 그 책에는 나와 있지 않은 것이다. Numega Bounds Checker나 Rational Purify등의 툴이 비싸서 엄두도 못내는 분들께는 좋은 내용이 되리라 믿는다

메모리 누수

개요

CRT가 출력해 주는 메모리 누수 리포트 결과를 간혹 본적이 있는 사람도 있을 것이다. 만약 MFC를 주로 사용한다면 이와 같은 메시지를 자주 볼 수 있다. 왜냐하면 MFC에서는 기본적으로 CRT 디버그 기능을 켜기 때문이다. 메시지 내용은 다음과 같다

Dumping objects -> 

 

{65} normal block at 0x003748B0, 48 bytes long. 

 

 Data: <@H7 @H7 @H7     > 40 48 37 00 40 48 37 00 40 48 37 00 CD CD CD CD  

 

{64} normal block at 0x00374840, 48 bytes long. 

 

 Data: < H7  H7  H7     > B0 48 37 00 B0 48 37 00 B0 48 37 00 CD CD CD CD  

 

Object dump complete. 

이 메시지의 내용은 두개의 블록이 할당된다음 해제되지 않았다는 것을 의미한다. 그렇다면 우선 이 메시지들에 대해 대략 알아보도록 하자

우선 {64}, {65}와 같은 것은 메모리에 할당된 순서로서, 각각 64번째, 65번째 할당된 메모리가 해제되지 않았음을 나타낸다. Normal block이라 함은 사용자에 의해 일반적인 new, delete, malloc등으로 할당된 메모리라는 뜻이다. 그리고 그 메모리 포인터 주소를 알려주고 있으며, 몇 바이트짜리 메모리 블록인지도 나와 있다

Data:… 라인은 그 메모리의 선두번지로부터 실제 메모리의 내용이 어떤 값을 포함하고 있는지를 나타내는 것이다

그러나 위의 메시지를 보았듯이 실제로는 아무 도움이 되지 않는다는 것을 금방 알수 있을것이다. 왜냐하면 메시지의 내용이 너무 암호와같이 복잡하다는데 문제가 있다. 몇줄짜리 프로그램이라면 또 모르겠으나, 거대한 프로그램인 경우 실제로 64,65번째 할당된 메모리를 순서대로 추적하는 것은 너무나도 어려우며, 어떤 포인터에 어느 번지의 메모리가 할당되었는지를 확인하는 것도 불가능에 가깝다고 할 수 있다. 또 자신이 만든 클래스나 구조체가 몇 바이트짜리인지를 일일이 확인한다는 것도 불가능하다.

그렇다면, 이 문제를 어떻게 해결해야 할 것인가? 실제로 CRT에는 여러가지 도우미 함수들을 포함하고 있어서 이 문제를 좀 더 쉽게 해결할 수 있도록 해 준다. 그러나 제대로 알고 사용하지 않는다면 결국에는 또다른 암호 코드만을 추가로 더 얻게 될 뿐이라는 것도 알아야 한다

샘플코드

#include "stdafx.h" 

 

int main() 

{ 

       int *p = new int; 

 

       return 0; 

 

} 

위의 코드는 int *를 할당하고 해제하지 않았으므로 명백한 메모리 누수이다. 그러나 이 프로그램을 빌드하고 디버그해봐도 프로그램 종료시 아무런 메시지를 남기지 않는다. 즉 메모리가 샌것인지, 혹은 Overwrite가 일어났는지 등을 확인할 길이 전혀 없다. 그러면 프로그램 일부를 수정해 보도록 한다

#include "stdafx.h" 

#include <crtdbg.h> 

 

int main() 

{ 

 

       int *p = new int; 

 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

_CrtMemBumpAllObjectsSince를 사용하기 위해 crtdbg.h를 인클루드 하고, 프로그램 종료 직전에 함수를 호출했다 디버그를 시작하고 프로그램을 종료하면 출력결과는 다음과 같다

Dumping objects -> 

 

{64} normal block at 0x00374838, 4 bytes long. 

 

 Data: <    > CD CD CD CD  

 

Object dump complete. 

The thread 0x65C has exited with code 0 (0x0). 

The program 'test.exe' has exited with code 0 (0x0). 

위에서 설명한것과 비슷한 종류의 메시지가 포함되어 있는 것을 알 수 있을 것이다. 이 메모리는 64번째 할당된 일반 메모리이며 0x00374838번지에 4바이트 할당되었음을 알 수 있다. 또 데이터 내용은 16진수로 CD CD CD CD이다 이 정보만으로도 많은 것을 알 수 있다. 예를들어 데이터가 CD CD CD CD라는 것은 할당만 해놓고 전혀 초기화를 하지 않았다는 의미이다. 단순한 위의 프로그램 만으로도 사용자가 처음 할당한 메모리가 64번째만에 할당되었다. 이유가 무엇일까? 이유는 간단하다. main함수가 호출되기 이전에 이미 많은 메모리 할당 요청이 있었고, 그것은 프로그램을 실행시키기 위해 운영체제나, CRT가 이미 사용했기 때문이다. 위의 프로그램은 단순하기 때문에 어디서 메모리가 샜는지 한눈에 척 알 수 있다. 그러나 메모리를 수십~수백번씩 할당했다 해제하는 일반 애플리케이션에서는 어떻게 정확히 64번째 할당된 메모리를 찾아낼 수 있을까? CRT에서 내준 정보는 CRT를 이용해 분석 가능하다 main함수가 처음 시작되기 전에 다음의 함수를 사용하도록 하자

       _CrtSetBreakAlloc(64); 

 

       int *p = new int; 

이 함수는 64번째 메모리 할당이 발생할 경우 프로그램 실행을 중지하고 디버거로 제어를 넘기라는 의미이다. 실제로 프로그램을 실행시켜 보면, "crtdbg.exe의 0x00411cb7에 처리되지 않은 예외가 있습니다. 사용자 중단점"과 같은 메시지를 출력하면서 프로그램 실행을 중지하게 된다. 브레이크 포인트가 가리키는 위치는 CRT의 메모리 할당 함수 내부이며, Call Stack을 따라가 보면 어느곳에서 할당이 일어났는지 바로 알수 있게된다.

전역 변수와 CRT 디버깅

다음과 같은 프로그램을 보도록 하자

#include "stdafx.h" 

#include "crtdbg.h" 

#include <crtdbg.h> 

 

class A 

{ 

public: 

       A() { p = new int; } 

 

private: 

       int *p; 

}; 

 

A a; 

 

int APIENTRY _tWinMain(HINSTANCE hInstance, 

                     HINSTANCE hPrevInstance, 

                     LPTSTR    lpCmdLine, 

                     int       nCmdShow) 

{ 

       _CrtSetBreakAlloc(64); 

 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

이 프로그램을 디버그 해 보면 똑 같은 메모리 누수를 보고하긴 하지만, 그러나 64번째 메모리 할당에서 정지하지 않는다. 이유가 무엇일까?

이유는 간단하다. A라는 클래스의 인스턴스인 a가 전역변수로 선언되었기 때문에 main함수가 호출되기 이전에 생성되기 때문이라는 것이다. a가 이미 생성되고 난 다음에 브레이크 포인트를 설정하니, 브레이크 포인트가 먹힐리가 없다. 그럼 방법이 없는 것일까?

답은 간단하다. 모든 할당이 일어나기 직전에 프로그램을 정지시켜놓고, 64번째 메모리 할당이 일어날 때 브레이크 하라는 명령을 주면 된다. 그럼 어떻게 하면 될까? 다음과 같이 하도록 한다.

  1. CRT소스에서 WinMainCRTStartup()함수를 찾아낸다. 이 함수는 실질적인 main함수이며, 프로그램이 로드되기 전에 가장먼저 실행된다. 이 함수 내부에서 여러분이 정의한 main 또는 WinMain함수를 호출하게 된다. 이 함수는 파일의 찾기 기능을 이용하거나, 또는 crt0.c파일을 바로 열어서 찾아도 된다. 그러나 더 간단한 방법은 main함수에 BP를 찍어놓고, 한번 실행시킨다음 call stack을 거슬러 올라가는 방법이다.
  2. WinMinCRTStartup함수의 시작부분에 BP를 찍어놓고 다시 디버거를 시작시킨다
  3. Watch창을 열어 _crtBreakAlloc 변수를 확인해 본다. 아마 -1일 것이다.
  4. 이 변수값을 원하는 메모리 할당 번지(위의 경우64)로 바꾼다.
  5. 다시 실행시키면 64번째 메모리 할당을 하기 전에 정지한다.

이 기술은 코드를 재 컴파일 하지 않아도 디버거 상에서 바로 브레이크 포인트를 수정할 수 있다는 장점이 있다. 현재 이 방법 보다 간단하게 할 수 있는 방법은 현재 연구중에 있다. 좋은 결과가 나오는대로 다시 여러분들에게 알려드리도록 하겠다.

이상 몇가지 메모리 누수를 찾아내는 방법을 살펴보았다. 그러나 주의할 것은 반드시 crtdbg.h를 같이 인클루드 해야 한다는 것이며 _DEBUG매크로가 정의되어 있을때에만 제대로 동작한다는 것이다.

CRT Debug 기능 사용법 2

요즘 CRT의 디버그 기능을 연구하기 시작하면서, 그동안 정말 좋은 기능들을 여럿 묵혀놓았다는 느낌을 지울수가 없습니다. 어렵게 메모리 관련 디버깅 루틴을 만들지 않아도, 너무나도 정확히 메모리 관련 에러를 잡아주니 STL을 처음 쓸 때 만큼이나 편리하게 느껴지더군요. 그럼 그동안 제가 연구한 것에 대해 보고드리도록 하겠습니다

지난 내용의 메모리 누수가 아닌 것 까지 모두 보고하는 문제

지난번 예제에서 약간만 더 수정해 보자

#include "stdafx.h" 

#include <crtdbg.h> 

#include <list> 

using std::list; 

 

typedef list<int> IntList; 

typedef IntList::iterator IntListIter; 

 

IntList myList; 

 

int main() 

{ 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

 

위의 프로그램은 메모리 누수를 한 개 보고한다. 위치는 myList의 생성자이다. 그러나 정말 그것이 샌것일까? 그렇다면 STL은 항상 메모리 누수가 있다는 말인가.. 이것저것 고민하던 중에, 진짜 Main함수는 {{{WinMainCRTStartup()}}}라는 사실이 생각났고, 디버그 상에서 {{{WinMainCRTStartup()}}} 메소드의 끝까지 따라가 보았다. 그랬더니 다음과 같은 루틴을 찾을 수 있었다. (처음에는 숨겨진 함수를 찾았다고 생각했으나, 알고봤더니 MSDN에 이미 문서화 되어있는 함수였다. 역시 소스보다는 문서를 찾아보는 것이 우선이다 -.-) 

{{{

/* Dump all memory leaks */ 

if (!fExit && _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) & _CRTDBG_LEAK_CHECK_DF) 

{ 

    fExit = 1; 

    _CrtDumpMemoryLeaks(); 

} 

_CrtSetDbgFlag이라는 함수는 CRT디버거의 전역 플랙을 셋팅하는 함수이다. 위에서 보다 시피 이미 CRT의 메인함수가 종료할 때 메모리 누수를 검사하고 있었던 것이다. 다만 디폴트로 꺼져 있었으며 열쇠는 _CrtSetDbgFlag이라는 함수가 쥐고 있다. MSDN에서 찾아본 결과 다음과 같다.

_CrtSetDbgFlag함수는 다섯개의 Flag이 있다.

_CRTDBG_ALLOC_MEM_DF : 디폴트로 켜져 있으며, 디버그 버전에서 모든 메모리 할당이 일어날 때마다 추적 가능하도록 특별한 기능을 추가해 둔다. 이 플랙이 켜져 있어야 메모리 누수를 안전하게 검사 할 수 있다.

_CRTDBG_DELAY_FREE_MEM_DF : delete, free등으로 삭제되어도 바로 삭제되지 않고, CRT의 메모리 관리소에 남아 있다가 프로그램 종료시에 완전히 삭제된다.

_CRTDBG_CHECK_ALWAYS_DF : 모든 메모리관련 연산에서 _CrtCheckMemory를 호출한다. 이 메소드는 이후에 다시 살펴볼 것임

_CRTDBG_CHECK_CRT_DF : CRT가 내부적으로 할당한 블록도 메모리를 체크할 때 포함한다. 일반적으로는 CRT가 할당한 블록은 메모리 체크에서 제외된다. 일반적으로 사용하지 않는다

_CRTDBG_LEAK_CHECK_DF : 프로그램이 완전히 종료되기 직전에 아직 해제되지 않은 메모리가 있는지 검사한다. 프로그램의 종료 포인트가 여러군데 있는 경우에 사용하면 일일이 _CrtDumpMemoryLeaks 메소드를 호출하지 않아도 자동적으로 메모리 누수를 검사할 수 있게된다.

일반적으로 가장 잡기 힘든 버그의 하나로서 메모리 누수, 메모리 Overwrite등을 꼽을 수 있다. 이런 문제점을 해결하기 위해 CRT(C Runtime library)에서는 여러가지 다양한 메모리 관련 디버그 함수들을 제공한다. 그러나 이것들이 디폴트로 사용하기 힘들게 꺼져 있기 때문에 대부분의 프로그래머들은 이 사실을 알지 못하는 경우가 많다. 그래서 이 글에서는 CRT의 디버그 관련 함수들에 대해 알아보고 어떻게 사용하는 것이 좋은지에 대해 논해 보려고 한다.

John Robbins(필자가 가장 좋아하는 프로그래머 중의 한명)가 지은 Debugging Applications 이라는 책에도 좋은 내용들이 있으니 참고하기 바란다. 그러나 여기 나온 팁은 그 책에는 나와 있지 않은 것이다. Numega Bounds Checker나 Rational Purify등의 툴이 비싸서 엄두도 못내는 분들께는 좋은 내용이 되리라 믿는다

메모리 누수

개요

CRT가 출력해 주는 메모리 누수 리포트 결과를 간혹 본적이 있는 사람도 있을 것이다. 만약 MFC를 주로 사용한다면 이와 같은 메시지를 자주 볼 수 있다. 왜냐하면 MFC에서는 기본적으로 CRT 디버그 기능을 켜기 때문이다. 메시지 내용은 다음과 같다

Dumping objects -> 

 

{65} normal block at 0x003748B0, 48 bytes long. 

 

 Data: <@H7 @H7 @H7     > 40 48 37 00 40 48 37 00 40 48 37 00 CD CD CD CD  

 

{64} normal block at 0x00374840, 48 bytes long. 

 

 Data: < H7  H7  H7     > B0 48 37 00 B0 48 37 00 B0 48 37 00 CD CD CD CD  

 

Object dump complete. 

이 메시지의 내용은 두개의 블록이 할당된다음 해제되지 않았다는 것을 의미한다. 그렇다면 우선 이 메시지들에 대해 대략 알아보도록 하자

우선 {64}, {65}와 같은 것은 메모리에 할당된 순서로서, 각각 64번째, 65번째 할당된 메모리가 해제되지 않았음을 나타낸다. Normal block이라 함은 사용자에 의해 일반적인 new, delete, malloc등으로 할당된 메모리라는 뜻이다. 그리고 그 메모리 포인터 주소를 알려주고 있으며, 몇 바이트짜리 메모리 블록인지도 나와 있다

Data:… 라인은 그 메모리의 선두번지로부터 실제 메모리의 내용이 어떤 값을 포함하고 있는지를 나타내는 것이다

그러나 위의 메시지를 보았듯이 실제로는 아무 도움이 되지 않는다는 것을 금방 알수 있을것이다. 왜냐하면 메시지의 내용이 너무 암호와같이 복잡하다는데 문제가 있다. 몇줄짜리 프로그램이라면 또 모르겠으나, 거대한 프로그램인 경우 실제로 64,65번째 할당된 메모리를 순서대로 추적하는 것은 너무나도 어려우며, 어떤 포인터에 어느 번지의 메모리가 할당되었는지를 확인하는 것도 불가능에 가깝다고 할 수 있다. 또 자신이 만든 클래스나 구조체가 몇 바이트짜리인지를 일일이 확인한다는 것도 불가능하다.

그렇다면, 이 문제를 어떻게 해결해야 할 것인가? 실제로 CRT에는 여러가지 도우미 함수들을 포함하고 있어서 이 문제를 좀 더 쉽게 해결할 수 있도록 해 준다. 그러나 제대로 알고 사용하지 않는다면 결국에는 또다른 암호 코드만을 추가로 더 얻게 될 뿐이라는 것도 알아야 한다

샘플코드

#include "stdafx.h" 

 

int main() 

{ 

       int *p = new int; 

 

       return 0; 

 

} 

위의 코드는 int *를 할당하고 해제하지 않았으므로 명백한 메모리 누수이다. 그러나 이 프로그램을 빌드하고 디버그해봐도 프로그램 종료시 아무런 메시지를 남기지 않는다. 즉 메모리가 샌것인지, 혹은 Overwrite가 일어났는지 등을 확인할 길이 전혀 없다. 그러면 프로그램 일부를 수정해 보도록 한다

#include "stdafx.h" 

#include <crtdbg.h> 

 

int main() 

{ 

 

       int *p = new int; 

 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

_CrtMemBumpAllObjectsSince를 사용하기 위해 crtdbg.h를 인클루드 하고, 프로그램 종료 직전에 함수를 호출했다 디버그를 시작하고 프로그램을 종료하면 출력결과는 다음과 같다

Dumping objects -> 

 

{64} normal block at 0x00374838, 4 bytes long. 

 

 Data: <    > CD CD CD CD  

 

Object dump complete. 

The thread 0x65C has exited with code 0 (0x0). 

The program 'test.exe' has exited with code 0 (0x0). 

위에서 설명한것과 비슷한 종류의 메시지가 포함되어 있는 것을 알 수 있을 것이다. 이 메모리는 64번째 할당된 일반 메모리이며 0x00374838번지에 4바이트 할당되었음을 알 수 있다. 또 데이터 내용은 16진수로 CD CD CD CD이다 이 정보만으로도 많은 것을 알 수 있다. 예를들어 데이터가 CD CD CD CD라는 것은 할당만 해놓고 전혀 초기화를 하지 않았다는 의미이다. 단순한 위의 프로그램 만으로도 사용자가 처음 할당한 메모리가 64번째만에 할당되었다. 이유가 무엇일까? 이유는 간단하다. main함수가 호출되기 이전에 이미 많은 메모리 할당 요청이 있었고, 그것은 프로그램을 실행시키기 위해 운영체제나, CRT가 이미 사용했기 때문이다. 위의 프로그램은 단순하기 때문에 어디서 메모리가 샜는지 한눈에 척 알 수 있다. 그러나 메모리를 수십~수백번씩 할당했다 해제하는 일반 애플리케이션에서는 어떻게 정확히 64번째 할당된 메모리를 찾아낼 수 있을까? CRT에서 내준 정보는 CRT를 이용해 분석 가능하다 main함수가 처음 시작되기 전에 다음의 함수를 사용하도록 하자

       _CrtSetBreakAlloc(64); 

 

       int *p = new int; 

이 함수는 64번째 메모리 할당이 발생할 경우 프로그램 실행을 중지하고 디버거로 제어를 넘기라는 의미이다. 실제로 프로그램을 실행시켜 보면, "crtdbg.exe의 0x00411cb7에 처리되지 않은 예외가 있습니다. 사용자 중단점"과 같은 메시지를 출력하면서 프로그램 실행을 중지하게 된다. 브레이크 포인트가 가리키는 위치는 CRT의 메모리 할당 함수 내부이며, Call Stack을 따라가 보면 어느곳에서 할당이 일어났는지 바로 알수 있게된다.

전역 변수와 CRT 디버깅

다음과 같은 프로그램을 보도록 하자

#include "stdafx.h" 

#include "crtdbg.h" 

#include <crtdbg.h> 

 

class A 

{ 

public: 

       A() { p = new int; } 

 

private: 

       int *p; 

}; 

 

A a; 

 

int APIENTRY _tWinMain(HINSTANCE hInstance, 

                     HINSTANCE hPrevInstance, 

                     LPTSTR    lpCmdLine, 

                     int       nCmdShow) 

{ 

       _CrtSetBreakAlloc(64); 

 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

이 프로그램을 디버그 해 보면 똑 같은 메모리 누수를 보고하긴 하지만, 그러나 64번째 메모리 할당에서 정지하지 않는다. 이유가 무엇일까?

이유는 간단하다. A라는 클래스의 인스턴스인 a가 전역변수로 선언되었기 때문에 main함수가 호출되기 이전에 생성되기 때문이라는 것이다. a가 이미 생성되고 난 다음에 브레이크 포인트를 설정하니, 브레이크 포인트가 먹힐리가 없다. 그럼 방법이 없는 것일까?

답은 간단하다. 모든 할당이 일어나기 직전에 프로그램을 정지시켜놓고, 64번째 메모리 할당이 일어날 때 브레이크 하라는 명령을 주면 된다. 그럼 어떻게 하면 될까? 다음과 같이 하도록 한다.

  1. CRT소스에서 WinMainCRTStartup()함수를 찾아낸다. 이 함수는 실질적인 main함수이며, 프로그램이 로드되기 전에 가장먼저 실행된다. 이 함수 내부에서 여러분이 정의한 main 또는 WinMain함수를 호출하게 된다. 이 함수는 파일의 찾기 기능을 이용하거나, 또는 crt0.c파일을 바로 열어서 찾아도 된다. 그러나 더 간단한 방법은 main함수에 BP를 찍어놓고, 한번 실행시킨다음 call stack을 거슬러 올라가는 방법이다.
  2. WinMinCRTStartup함수의 시작부분에 BP를 찍어놓고 다시 디버거를 시작시킨다
  3. Watch창을 열어 _crtBreakAlloc 변수를 확인해 본다. 아마 -1일 것이다.
  4. 이 변수값을 원하는 메모리 할당 번지(위의 경우64)로 바꾼다.
  5. 다시 실행시키면 64번째 메모리 할당을 하기 전에 정지한다.

이 기술은 코드를 재 컴파일 하지 않아도 디버거 상에서 바로 브레이크 포인트를 수정할 수 있다는 장점이 있다. 현재 이 방법 보다 간단하게 할 수 있는 방법은 현재 연구중에 있다. 좋은 결과가 나오는대로 다시 여러분들에게 알려드리도록 하겠다.

이상 몇가지 메모리 누수를 찾아내는 방법을 살펴보았다. 그러나 주의할 것은 반드시 crtdbg.h를 같이 인클루드 해야 한다는 것이며 _DEBUG매크로가 정의되어 있을때에만 제대로 동작한다는 것이다.

CRT Debug 기능 사용법 2

요즘 CRT의 디버그 기능을 연구하기 시작하면서, 그동안 정말 좋은 기능들을 여럿 묵혀놓았다는 느낌을 지울수가 없습니다. 어렵게 메모리 관련 디버깅 루틴을 만들지 않아도, 너무나도 정확히 메모리 관련 에러를 잡아주니 STL을 처음 쓸 때 만큼이나 편리하게 느껴지더군요. 그럼 그동안 제가 연구한 것에 대해 보고드리도록 하겠습니다

지난 내용의 메모리 누수가 아닌 것 까지 모두 보고하는 문제

지난번 예제에서 약간만 더 수정해 보자

#include "stdafx.h" 

#include <crtdbg.h> 

#include <list> 

using std::list; 

 

typedef list<int> IntList; 

typedef IntList::iterator IntListIter; 

 

IntList myList; 

 

int main() 

{ 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

 

위의 프로그램은 메모리 누수를 한 개 보고한다. 위치는 myList의 생성자이다. 그러나 정말 그것이 샌것일까? 그렇다면 STL은 항상 메모리 누수가 있다는 말인가.. 이것저것 고민하던 중에, 진짜 Main함수는 {{{WinMainCRTStartup()}}}라는 사실이 생각났고, 디버그 상에서 {{{WinMainCRTStartup()}}} 메소드의 끝까지 따라가 보았다. 그랬더니 다음과 같은 루틴을 찾을 수 있었다. (처음에는 숨겨진 함수를 찾았다고 생각했으나, 알고봤더니 MSDN에 이미 문서화 되어있는 함수였다. 역시 소스보다는 문서를 찾아보는 것이 우선이다 -.-) 

{{{

/* Dump all memory leaks */ 

if (!fExit && _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) & _CRTDBG_LEAK_CHECK_DF) 

{ 

    fExit = 1; 

    _CrtDumpMemoryLeaks(); 

} 

_CrtSetDbgFlag이라는 함수는 CRT디버거의 전역 플랙을 셋팅하는 함수이다. 위에서 보다 시피 이미 CRT의 메인함수가 종료할 때 메모리 누수를 검사하고 있었던 것이다. 다만 디폴트로 꺼져 있었으며 열쇠는 _CrtSetDbgFlag이라는 함수가 쥐고 있다. MSDN에서 찾아본 결과 다음과 같다.

_CrtSetDbgFlag함수는 다섯개의 Flag이 있다.

_CRTDBG_ALLOC_MEM_DF : 디폴트로 켜져 있으며, 디버그 버전에서 모든 메모리 할당이 일어날 때마다 추적 가능하도록 특별한 기능을 추가해 둔다. 이 플랙이 켜져 있어야 메모리 누수를 안전하게 검사 할 수 있다.

_CRTDBG_DELAY_FREE_MEM_DF : delete, free등으로 삭제되어도 바로 삭제되지 않고, CRT의 메모리 관리소에 남아 있다가 프로그램 종료시에 완전히 삭제된다.

_CRTDBG_CHECK_ALWAYS_DF : 모든 메모리관련 연산에서 _CrtCheckMemory를 호출한다. 이 메소드는 이후에 다시 살펴볼 것임

_CRTDBG_CHECK_CRT_DF : CRT가 내부적으로 할당한 블록도 메모리를 체크할 때 포함한다. 일반적으로는 CRT가 할당한 블록은 메모리 체크에서 제외된다. 일반적으로 사용하지 않는다

_CRTDBG_LEAK_CHECK_DF : 프로그램이 완전히 종료되기 직전에 아직 해제되지 않은 메모리가 있는지 검사한다. 프로그램의 종료 포인트가 여러군데 있는 경우에 사용하면 일일이 _CrtDumpMemoryLeaks 메소드를 호출하지 않아도 자동적으로 메모리 누수를 검사할 수 있게된다.

일반적으로 가장 잡기 힘든 버그의 하나로서 메모리 누수, 메모리 Overwrite등을 꼽을 수 있다. 이런 문제점을 해결하기 위해 CRT(C Runtime library)에서는 여러가지 다양한 메모리 관련 디버그 함수들을 제공한다. 그러나 이것들이 디폴트로 사용하기 힘들게 꺼져 있기 때문에 대부분의 프로그래머들은 이 사실을 알지 못하는 경우가 많다. 그래서 이 글에서는 CRT의 디버그 관련 함수들에 대해 알아보고 어떻게 사용하는 것이 좋은지에 대해 논해 보려고 한다.

John Robbins(필자가 가장 좋아하는 프로그래머 중의 한명)가 지은 Debugging Applications 이라는 책에도 좋은 내용들이 있으니 참고하기 바란다. 그러나 여기 나온 팁은 그 책에는 나와 있지 않은 것이다. Numega Bounds Checker나 Rational Purify등의 툴이 비싸서 엄두도 못내는 분들께는 좋은 내용이 되리라 믿는다

메모리 누수

개요

CRT가 출력해 주는 메모리 누수 리포트 결과를 간혹 본적이 있는 사람도 있을 것이다. 만약 MFC를 주로 사용한다면 이와 같은 메시지를 자주 볼 수 있다. 왜냐하면 MFC에서는 기본적으로 CRT 디버그 기능을 켜기 때문이다. 메시지 내용은 다음과 같다

Dumping objects -> 

 

{65} normal block at 0x003748B0, 48 bytes long. 

 

 Data: <@H7 @H7 @H7     > 40 48 37 00 40 48 37 00 40 48 37 00 CD CD CD CD  

 

{64} normal block at 0x00374840, 48 bytes long. 

 

 Data: < H7  H7  H7     > B0 48 37 00 B0 48 37 00 B0 48 37 00 CD CD CD CD  

 

Object dump complete. 

이 메시지의 내용은 두개의 블록이 할당된다음 해제되지 않았다는 것을 의미한다. 그렇다면 우선 이 메시지들에 대해 대략 알아보도록 하자

우선 {64}, {65}와 같은 것은 메모리에 할당된 순서로서, 각각 64번째, 65번째 할당된 메모리가 해제되지 않았음을 나타낸다. Normal block이라 함은 사용자에 의해 일반적인 new, delete, malloc등으로 할당된 메모리라는 뜻이다. 그리고 그 메모리 포인터 주소를 알려주고 있으며, 몇 바이트짜리 메모리 블록인지도 나와 있다

Data:… 라인은 그 메모리의 선두번지로부터 실제 메모리의 내용이 어떤 값을 포함하고 있는지를 나타내는 것이다

그러나 위의 메시지를 보았듯이 실제로는 아무 도움이 되지 않는다는 것을 금방 알수 있을것이다. 왜냐하면 메시지의 내용이 너무 암호와같이 복잡하다는데 문제가 있다. 몇줄짜리 프로그램이라면 또 모르겠으나, 거대한 프로그램인 경우 실제로 64,65번째 할당된 메모리를 순서대로 추적하는 것은 너무나도 어려우며, 어떤 포인터에 어느 번지의 메모리가 할당되었는지를 확인하는 것도 불가능에 가깝다고 할 수 있다. 또 자신이 만든 클래스나 구조체가 몇 바이트짜리인지를 일일이 확인한다는 것도 불가능하다.

그렇다면, 이 문제를 어떻게 해결해야 할 것인가? 실제로 CRT에는 여러가지 도우미 함수들을 포함하고 있어서 이 문제를 좀 더 쉽게 해결할 수 있도록 해 준다. 그러나 제대로 알고 사용하지 않는다면 결국에는 또다른 암호 코드만을 추가로 더 얻게 될 뿐이라는 것도 알아야 한다

샘플코드

#include "stdafx.h" 

 

int main() 

{ 

       int *p = new int; 

 

       return 0; 

 

} 

위의 코드는 int *를 할당하고 해제하지 않았으므로 명백한 메모리 누수이다. 그러나 이 프로그램을 빌드하고 디버그해봐도 프로그램 종료시 아무런 메시지를 남기지 않는다. 즉 메모리가 샌것인지, 혹은 Overwrite가 일어났는지 등을 확인할 길이 전혀 없다. 그러면 프로그램 일부를 수정해 보도록 한다

#include "stdafx.h" 

#include <crtdbg.h> 

 

int main() 

{ 

 

       int *p = new int; 

 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

_CrtMemBumpAllObjectsSince를 사용하기 위해 crtdbg.h를 인클루드 하고, 프로그램 종료 직전에 함수를 호출했다 디버그를 시작하고 프로그램을 종료하면 출력결과는 다음과 같다

Dumping objects -> 

 

{64} normal block at 0x00374838, 4 bytes long. 

 

 Data: <    > CD CD CD CD  

 

Object dump complete. 

The thread 0x65C has exited with code 0 (0x0). 

The program 'test.exe' has exited with code 0 (0x0). 

위에서 설명한것과 비슷한 종류의 메시지가 포함되어 있는 것을 알 수 있을 것이다. 이 메모리는 64번째 할당된 일반 메모리이며 0x00374838번지에 4바이트 할당되었음을 알 수 있다. 또 데이터 내용은 16진수로 CD CD CD CD이다 이 정보만으로도 많은 것을 알 수 있다. 예를들어 데이터가 CD CD CD CD라는 것은 할당만 해놓고 전혀 초기화를 하지 않았다는 의미이다. 단순한 위의 프로그램 만으로도 사용자가 처음 할당한 메모리가 64번째만에 할당되었다. 이유가 무엇일까? 이유는 간단하다. main함수가 호출되기 이전에 이미 많은 메모리 할당 요청이 있었고, 그것은 프로그램을 실행시키기 위해 운영체제나, CRT가 이미 사용했기 때문이다. 위의 프로그램은 단순하기 때문에 어디서 메모리가 샜는지 한눈에 척 알 수 있다. 그러나 메모리를 수십~수백번씩 할당했다 해제하는 일반 애플리케이션에서는 어떻게 정확히 64번째 할당된 메모리를 찾아낼 수 있을까? CRT에서 내준 정보는 CRT를 이용해 분석 가능하다 main함수가 처음 시작되기 전에 다음의 함수를 사용하도록 하자

       _CrtSetBreakAlloc(64); 

 

       int *p = new int; 

이 함수는 64번째 메모리 할당이 발생할 경우 프로그램 실행을 중지하고 디버거로 제어를 넘기라는 의미이다. 실제로 프로그램을 실행시켜 보면, "crtdbg.exe의 0x00411cb7에 처리되지 않은 예외가 있습니다. 사용자 중단점"과 같은 메시지를 출력하면서 프로그램 실행을 중지하게 된다. 브레이크 포인트가 가리키는 위치는 CRT의 메모리 할당 함수 내부이며, Call Stack을 따라가 보면 어느곳에서 할당이 일어났는지 바로 알수 있게된다.

전역 변수와 CRT 디버깅

다음과 같은 프로그램을 보도록 하자

#include "stdafx.h" 

#include "crtdbg.h" 

#include <crtdbg.h> 

 

class A 

{ 

public: 

       A() { p = new int; } 

 

private: 

       int *p; 

}; 

 

A a; 

 

int APIENTRY _tWinMain(HINSTANCE hInstance, 

                     HINSTANCE hPrevInstance, 

                     LPTSTR    lpCmdLine, 

                     int       nCmdShow) 

{ 

       _CrtSetBreakAlloc(64); 

 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

이 프로그램을 디버그 해 보면 똑 같은 메모리 누수를 보고하긴 하지만, 그러나 64번째 메모리 할당에서 정지하지 않는다. 이유가 무엇일까?

이유는 간단하다. A라는 클래스의 인스턴스인 a가 전역변수로 선언되었기 때문에 main함수가 호출되기 이전에 생성되기 때문이라는 것이다. a가 이미 생성되고 난 다음에 브레이크 포인트를 설정하니, 브레이크 포인트가 먹힐리가 없다. 그럼 방법이 없는 것일까?

답은 간단하다. 모든 할당이 일어나기 직전에 프로그램을 정지시켜놓고, 64번째 메모리 할당이 일어날 때 브레이크 하라는 명령을 주면 된다. 그럼 어떻게 하면 될까? 다음과 같이 하도록 한다.

  1. CRT소스에서 WinMainCRTStartup()함수를 찾아낸다. 이 함수는 실질적인 main함수이며, 프로그램이 로드되기 전에 가장먼저 실행된다. 이 함수 내부에서 여러분이 정의한 main 또는 WinMain함수를 호출하게 된다. 이 함수는 파일의 찾기 기능을 이용하거나, 또는 crt0.c파일을 바로 열어서 찾아도 된다. 그러나 더 간단한 방법은 main함수에 BP를 찍어놓고, 한번 실행시킨다음 call stack을 거슬러 올라가는 방법이다.
  2. WinMinCRTStartup함수의 시작부분에 BP를 찍어놓고 다시 디버거를 시작시킨다
  3. Watch창을 열어 _crtBreakAlloc 변수를 확인해 본다. 아마 -1일 것이다.
  4. 이 변수값을 원하는 메모리 할당 번지(위의 경우64)로 바꾼다.
  5. 다시 실행시키면 64번째 메모리 할당을 하기 전에 정지한다.

이 기술은 코드를 재 컴파일 하지 않아도 디버거 상에서 바로 브레이크 포인트를 수정할 수 있다는 장점이 있다. 현재 이 방법 보다 간단하게 할 수 있는 방법은 현재 연구중에 있다. 좋은 결과가 나오는대로 다시 여러분들에게 알려드리도록 하겠다.

이상 몇가지 메모리 누수를 찾아내는 방법을 살펴보았다. 그러나 주의할 것은 반드시 crtdbg.h를 같이 인클루드 해야 한다는 것이며 _DEBUG매크로가 정의되어 있을때에만 제대로 동작한다는 것이다.

CRT Debug 기능 사용법 2

요즘 CRT의 디버그 기능을 연구하기 시작하면서, 그동안 정말 좋은 기능들을 여럿 묵혀놓았다는 느낌을 지울수가 없습니다. 어렵게 메모리 관련 디버깅 루틴을 만들지 않아도, 너무나도 정확히 메모리 관련 에러를 잡아주니 STL을 처음 쓸 때 만큼이나 편리하게 느껴지더군요. 그럼 그동안 제가 연구한 것에 대해 보고드리도록 하겠습니다

지난 내용의 메모리 누수가 아닌 것 까지 모두 보고하는 문제

지난번 예제에서 약간만 더 수정해 보자

#include "stdafx.h" 

#include <crtdbg.h> 

#include <list> 

using std::list; 

 

typedef list<int> IntList; 

typedef IntList::iterator IntListIter; 

 

IntList myList; 

 

int main() 

{ 

       _CrtMemDumpAllObjectsSince(0); 

 

       return 0; 

} 

 

위의 프로그램은 메모리 누수를 한 개 보고한다. 위치는 myList의 생성자이다. 그러나 정말 그것이 샌것일까? 그렇다면 STL은 항상 메모리 누수가 있다는 말인가.. 이것저것 고민하던 중에, 진짜 Main함수는 {{{WinMainCRTStartup()}}}라는 사실이 생각났고, 디버그 상에서 {{{WinMainCRTStartup()}}} 메소드의 끝까지 따라가 보았다. 그랬더니 다음과 같은 루틴을 찾을 수 있었다. (처음에는 숨겨진 함수를 찾았다고 생각했으나, 알고봤더니 MSDN에 이미 문서화 되어있는 함수였다. 역시 소스보다는 문서를 찾아보는 것이 우선이다 -.-) 

{{{

/* Dump all memory leaks */ 

if (!fExit && _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) & _CRTDBG_LEAK_CHECK_DF) 

{ 

    fExit = 1; 

    _CrtDumpMemoryLeaks(); 

} 

_CrtSetDbgFlag이라는 함수는 CRT디버거의 전역 플랙을 셋팅하는 함수이다. 위에서 보다 시피 이미 CRT의 메인함수가 종료할 때 메모리 누수를 검사하고 있었던 것이다. 다만 디폴트로 꺼져 있었으며 열쇠는 _CrtSetDbgFlag이라는 함수가 쥐고 있다. MSDN에서 찾아본 결과 다음과 같다.

_CrtSetDbgFlag함수는 다섯개의 Flag이 있다.

_CRTDBG_ALLOC_MEM_DF : 디폴트로 켜져 있으며, 디버그 버전에서 모든 메모리 할당이 일어날 때마다 추적 가능하도록 특별한 기능을 추가해 둔다. 이 플랙이 켜져 있어야 메모리 누수를 안전하게 검사 할 수 있다.

_CRTDBG_DELAY_FREE_MEM_DF : delete, free등으로 삭제되어도 바로 삭제되지 않고, CRT의 메모리 관리소에 남아 있다가 프로그램 종료시에 완전히 삭제된다.

_CRTDBG_CHECK_ALWAYS_DF : 모든 메모리관련 연산에서 _CrtCheckMemory를 호출한다. 이 메소드는 이후에 다시 살펴볼 것임

_CRTDBG_CHECK_CRT_DF : CRT가 내부적으로 할당한 블록도 메모리를 체크할 때 포함한다. 일반적으로는 CRT가 할당한 블록은 메모리 체크에서 제외된다. 일반적으로 사용하지 않는다

_CRTDBG_LEAK_CHECK_DF : 프로그램이 완전히 종료되기 직전에 아직 해제되지 않은 메모리가 있는지 검사한다. 프로그램의 종료 포인트가 여러군데 있는 경우에 사용하면 일일이 _CrtDumpMemoryLeaks 메소드를 호출하지 않아도 자동적으로 메모리 누수를 검사할 수 있게된다.

6. 동적인 메모리 디버깅하기

 

메모리를 동적으로 할당하는 것은 잠재적인 버그의 가능성을 안고 있는 것이며, 아마도 이 상황에서 가장 흔한 버그는 메모리 유출일 것이다. 여러분에게 기억을 일깨우기 위해 메모리 유출이라는 것은 여러분이 new 연산자를 사용하여 메모리를 할당할 때 발생한다. 그러나 여러분이 그 메모리를 다 사용한 후에 그것을 다시 해제할 때 delete를 사용해서는 안된다. 여러분이 할당했던 메모리를 삭제하는 것을 잊어버리는 것은 별 문제로 하고, 여러분은 클래스 계층 구조 안에서 가상이 아닌 디스트럭터들도 그러한 문제를 발생시킬 수 있다는 사실을 특별히 명심해야 한다(왜냐하면, 우리가 보았듯이 그것은 하나의 객체가 파괴될 때 잘못된 디스트럭터가 호출되도록 할 수 있기 때문이다). 물론, 여러분의 프로그램이 종료될 때 모든 메모리는 해제된다. 그러나 프로그램이 실행되고 있을 때 그것은 여러분의 프로그램에 할당된 채로 남아 있게 된다. 메모리 유출은 대부분의 경우, 어떠한 명백한 증상도 나타내지 않는다. 어떤 경우에는 전혀 증상을 나타내지 않는다. 그러나 그것은 여러분 컴퓨터의 성능에 해로운 영향을 끼친다. 왜냐하면, 메모리가 어떤 좋은 목적으로 점유되지 않기 때문이다. 때로, 모든 사용 가능한 메모리가 할당되었을 때 그것은 큰 혼란을 야기시킬 수 있다.

여러분의 프로그램이 free store를 사용하는 것을 감시하기 위해서 비주얼 C++는 일련의 진단 루틴들을 제공한다(그것들은 free store의 특별한 디버그 버전을 사용한다). 그것들은 crtdbg.h라는 헤더 안에서 선언된다. 그러한 루틴들에 대한 모든 호출들은 여러분 프로그램의 릴리즈 버전으로부터 자동적으로 삭제된다. 그러므로 여러분은 그것들에 대해서 어떠한 전처리기 제어도 추가할 필요가 없다.

6.1 free store를 검사하는 함수들

 

free store를 검사하는 작업에서 어떠한 것들이 포함되는가에 대해서 전체적으로 살펴본다. 그런 다음, 어떻게 메모리 유출이 검출되는가에 대해서 살펴본다. crtdbg.h 안에서 선언되는 함수들은 _CrtMemState 형식을 갖는 구조 안에 저장되어 있는 free store의 상태에 대한 기록을 사용하여 free store를 검사한다. 이 구조는 비교적 간단한데, 다음과 같이 정의된다.

typedef struct _CrtMemState

{

    struct _CrtMemBlockHeader* pBlockHeader; // 최근에 할당된 블록에 대한 포인터들

    unsigned long lCounts[_MAX_BLOCKS]; //블록이 각 형식들에 대한 카운터

    unsigned long ISizes][_MAX_BLOCKS]; //각 블록 형식에 할당되어 있는 전체 바이트

    unsigned long lHighWaterCount; //현재까지 한번에 가장 많이 할당된 바이트

    unsigned long lTotalCount; //현재 할당된 전체 바이트

} _CrtMemState;

 

우리는 free store의 상태에 관한 세부적인 사항에 대해서는 직접적으로 살펴보지 않을 것이다. 왜냐하면, 우리는 정보를 좀더 읽기 쉬운 형식으로 나타내는 함수들을 사용할 것이기 때문이다. free store의 작업을 추적하는 매우 많은 함수들이 있다. 그러나 우리는 가장 살펴볼 만한 5개에 대해서만 알아볼 것이다.

그것들은 여러분에게 다음과 같은 기능들을 제공한다.

◇ 어떠한 지점에 있는 free store의 상태도 기록하는 기능

◇ free store의 두 가지 상태의 차이점을 알아내는 기능

◇ 상태 정보 출력하기

◇ free store 안에 있는 객체들에 관한 정보를 출력하는 기능

◇ 메모리 유출을 검출하는 기능

다음은 그러한 함수들의 선언들과 함께 그것들이 어떤 것을 하는가에 대한 간단한 설명 이다.

void _CrtMemCheckpoint (_CrtMemState* state);

 

이것은 free store의 현재의 상태를 _CrtMemState 구조 안에 저장한다. 여러분이 함수에 전달하는 인수는 상태가 기록될 _CrtMemState 구조에 대한 포인터이다.

int _CrtMemDifference (_CrtMemState* stateDiff,

const _CrtMemState* oldstate,

const _CrtMemState* newState);

 

이 함수는 세 번째 인수에 의해서 지정된 상태를 두 번째 인수 안에서 여러분이 지정한 이전의 상태와 비교한다. 그 차이점은 여러분이 첫 번째 인수 안에서 지정한 _CrtMemState 구조 안에 저장된다. 만약 상태들이 다르면 함수는 0이 아닌 값(true)을 리턴하고, 그렇지 않으면 0(false)을 리턴한다.

void _CrtMemDumStatistics(const _CrtMemState* state);

이것은 인수에 의해서 지정된 free store 상태에 관한 정보를 출력 스트림으로 전달한다. 인수에 의해서 가리켜지고 있는 상태 구조는 여러분이 _CrtMemCheckPoint()를 사용하여 기록했던 상태이거나, 또는 _CrtMemDifference()에 의해서 생성된 두 개의 상태의 차이점일 수도 있다.

void _CrtMemDumpAllObjectsSince(const _CrtMemState* state);

이 함수는 free store의 상태가 인수에 의해서 지정된 이후로 free store 안에서 할당된 객체들에 대한 정보를 나타낸다(이것은 여러분의 프로그램에서 _CrtMemCheckPoint()에 대한 이전의 호출에 의해서 기록될 것이다). 만약 여러분이 null을 함수에 전달하면 그것은 여러분의 프로그램이 실행된 이후에 할당된 모든 객체들에 대한 정보를 나타낸다.

int _CrtDumpMemoryLeaks();

이것은 우리의 샘플에 대해서 필요한 함수이다 왜냐하면, 이것은 메모리 유출을 검사하고, 검출된 모든 유출에 대한 정보를 나타내기 때문이다. 여러분은 이 함수를 어느 때고 호출할 수 있다. 그러나 매우 유용한 메커니즘을 사용하여 여러분의 프로그램이 종료될 때 그 함수가 자동적으로 호출되도록 할 수 있다. 만약 여러분이 그러한 메커니즘을 인에이블시켰다면 프로그램 실행 동안에 발생하는 어떠한 메모리 유출도 자동적으로 검출하게 될 것이다. 그러므로 우리가 어떻게 그렇게 하는가에 대해서 살펴본다.

6.2 free store 디버그 작업 제어하기

비트 필드

설명

_CRTDBG_ALLOC_MEM_DF

이 비트가 on되면 이것은 디버그 할당을 on시킨다. 그러므로 free store 상태를 추적할 수 있다.

_CRTDBG_DELAY_FREE_MEM_DF

이 비트가 on되면 이것은 메모리가 delete에 의해서 해제되지 않도록 한다. 그러므로 여러분은 메모리가 부족한 상황에서 어떤 것이 발생하는가를 알아 볼 수 있다.

_CRTDBG_CHECK_ALWAYS_DF

이 비트가 on되면 이것은 _CrtCheckMemory() 함수가 모든 new와 delete에 대해서 자동적으로 호출되도록 한다. 이 함수는 free store가 잘 보존되어 있는가를 검사한다. 예를 들어, 배열의 범위를 넘어서는 값을 저장함으로써 블록들이 덮어씌여지지 않았는가를 검사한다. 만약 어떤 결합이 발견되면 보고서가 출력된다. 이것은 실행을 느리게 한다. 그러나 에러를 재빨리 잡아낸다.

_CRTDBG_CHECK_CRT_DF

이 비트가 on되면 런 타임 라이브러리에 의해서 내부적으로 사용되는 메모리가 디버그 작업 등안에 추적된다.

_CRTDBG_LEAK_CHECK_DF

프로그램이 종료될 때 _CrtDumpMemoryLeaks()를 자동적으로 호출함으로써 유출 검사 작업이 실행되도록 한다. 만약 여러분의 프로그램이 할당된 모든 메모리를 해제하는 데 실패한 경우에만 출력을 얻게 된다.

여러분은 int 형식을 갖는 _crtDbgFlag 플래그를 설정함으로써 free store 디버그 작업을 제어한다. 이 플래그는 5개의 개별적인 제어 비트를 포함하는데, 그 중 하나는 자동 메모리 유출 검사를 인에이블하는 데 사용된다. 여러분은 그러한 제어 비트들을 다음의 식별자들을 사용하여 지정한다.

디폴트로, _CRTDBG_ALL0C_MEM_DF 비트가 on된다. 그리고 다른 모든 것들은 off된다. 이러한 비트들의 조합을 설정하고 해제하는 데 여러분은 반드시 bitwise 연산자를 사용해야 한다. _crtDbgFlag 플래그를 설정하기 위해서 여러분은 int 형식의 플래그를 여러분이 필요로 하는 지시자들의 조합을 구현하는 _CrtDbgFlag() 함수에 전달해야 한다. 여러분이 원하는 지시자들을 설정하는 한가지 방법은, 먼저 _crtDbgFlag 플래그의 현재 상태를 얻는 것이다. 여러분은 그것을 다음과 같이 _CrtSetDbgFlag() 함수를 _CRTDBG_REPORT_FLAG 인수와 함께 호출함으로써 할 수 있다.

int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); //현재 플래그를 얻는다.

그런 다음, 여러분은 bitwise 연산자를 사용하여 이 플래그를 갖는 개별적인 지시자들에 대한 식별자들을 결합시킴으로써 그러한 지시자들을 설정 또는 해제할 수 있다. 지시자를 on시키려면 여러분은 지시자 식별자를 플래그와 함께 OR 연산시켜야 한다. 예를 들어, 자동 메모리 유출 검사 지시자를 on시키려면 플래그 안에서 여러분은 다음과 같이 작성할 수 있다.

flag |= _CRTDBG_LEAK_CHECK_DF;

지시자를 off시키려면 식별자의 부정을 플래그와 함께 AND 연산시켜야 한다. 예를 들어, 라이브러리에 의해서 내부적으로 사용되는 메모리를 추적하는 것을 off시키려면 다음과 같이 작성할 수 있다.

flag &= ~_CRTDBG_CHECK_CRT_DF;

새로운 플래그가 효과를 발휘하게 하려면 플래그를 인수로 하여 _CrtSetDbgFlag()를 호출하면 된다.

_CrtSetDbgFlag(flag);

그 외에, 여러분이 원하는 지시자들에 대한 모든 식별자들을 OR 연산시켜서 그 결과를 _CrtSetDbgflag()에 대한 인수로서 전달할 수 있다. 만약 프로그램이 종료될 때 메모리 유출에 대해서만 검사하고자 한다면 다음과 같이 작성한다.

_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF |_CRTDBG_ALL0C_MEM_DF);

여러분의 프로그램에 있는 다양한 지점에서 비트들을 설정/해제하기보다는 지시자들의 특정한 조합을 원한다면 그렇게 하는 가장 쉬운 방법이다. 거의 우리는 동적 메모리 디버깅 기능을 샘플에 적용할 때에 도달했다. free store 디버깅 출력을 어디로 보낼 깃인가를 우리가 어떻게 결정하는가에 대해서 살펴보아야 한다.

6.3 free store 디버깅 출력

free store 디버깅 함수들로부터의 출력의 목적지는 디폴트로 표준 출력 스트림이 아니다. 그것들은 디버그 메시지 윈도우로 간다. 만약 그것들을 stdout에서 보기를 원한다면 우리가 반드시 그렇게 설정해야 한다. 이것에는 두 가지의 함수가 있다. _CrtSetReportMode()는 출력에 대한 일반적인 목적지를 설정하며, _CrtSetReportFile()은 특별히 스트림 목적지를 지정한다. _CrtSetReportMode()는 다음과 같이 선언된다.

int _CrtSetReportMode(int reportType, int reportMode);

리포트 형식

설명

_CRT_WARN

다양한 종류의 경고 메시지를 나타낸다. 메모리 유출이 발생했을 때의 출력이 하나의 경고이다.

_CRT_ERROR

복구할 수 없는 문제를 발생시키는 치명적인 에러

_CRT_ASSERT

assertion으로부터의 출력(우리가 이전에 설명했던 assert()는 함수로부터의 출력이 아닌)

free store 디버깅 함수들에 의해서 세 종류의 출력이 생성된다. _CrtSetReportMode() 함수에 대한 각각의 호출은 두 번째 인수에 의해서 지정된 목적지를 첫 번째 인수에 의해서 지정된 출력 형식으로 설정한다. 여러분은 리포트 형식을 다음에 있는 식별자들 중의 하나로 지정하게 된다.

crtdbg.h 헤더는 ASSERT와 ASSERTE, 두 개의 매크로를 정의하는데, 이것들은 표준 라이브러리에 있는 assert() 함수와 매우 동일한 방식으로 작동한다. 이 두 매크로 사이의 차이점은, ASSERTE는 실패가 발생할 때 assertion 표현을 보고하는 반면, ASSERT는 그렇지 않는다는 것이다.

여러분은 다음의 식별자들의 조합을 사용하여 리포트 모드를 지정한다.

리포트 모드

_CrtDbgReport

_CRTDBG_M0DE_DEBUG

이것은 디폴트 모드로서 디버거의 제어 아래에서 실행할 때 디버그 윈도우 안에서 여러분이 보게 될 디버그 문자열 안에 출력을 전달한다.

_CRTDBG_MODE_FILE

출력은 출력 스트림으로 전달된다.

_CRTDBG_MODE_WNDW

출력은 메시지 박스 안에 나타난다.

_CRTDBG_REPORT_MODE

만약 여러분이 이것을 지정하면 _CrtSetReportMode() 함수는 단지 현재의 리포트 모드를 리턴한다.

한 개 이상의 목적지를 지정하려면 여러분은 | 연산자를 사용하여 식별자들을 OR 연산시키면 된다. 여러분은 _CrtSetReportMode()에 대한 개별적인 호출을 통해서 각각의 출력 형식에 대한 목적지를 설정할 수 있다. 메모리 유출이 검출되었을 때 출력을 파일 스트림으로 돌리기 위해 우리는 다음의 문장을 통해서 리포트 모드를 설정할 수 있다.

CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

이것은 목적지를 일반적으로 파일 스트림으로 설정한다. 목적지를 특별하게 지정하려면 여전히 _CrtSetReportFile() 함수를 호출해야 한다.

_CrtSetReportFile() 함수는 다음과 같이 선언된다.

_HFILE _CrtSetReportFile(int reportType, _HFILE reportFile);

여기서의 두 번째 인수는 파일 스트림에 대한 _HFILE 형식의 포인터이거나(이것에 대해서는 살펴보지 않을 것이다), 아니면 다음의 식별자들 중 하나이다.

리포트 파일

_CrtDbgReport 행동

_CRTDBG_FILE_STDERR

출력은 표준 에러 스트림인 stderr로 전달된다.

_CRTDBG_FILE_STDOUT

출력은 표준 출력 스트림인 stdout로 전달된다.

_CRTDBG_REPORT_FILE

만약 여러분이 이인수를 지정하면 _CrtSerReportFile() 함수는 현재의 목적지를 리턴할 것이다.

6. 동적인 메모리 디버깅하기

 

메모리를 동적으로 할당하는 것은 잠재적인 버그의 가능성을 안고 있는 것이며, 아마도 이 상황에서 가장 흔한 버그는 메모리 유출일 것이다. 여러분에게 기억을 일깨우기 위해 메모리 유출이라는 것은 여러분이 new 연산자를 사용하여 메모리를 할당할 때 발생한다. 그러나 여러분이 그 메모리를 다 사용한 후에 그것을 다시 해제할 때 delete를 사용해서는 안된다. 여러분이 할당했던 메모리를 삭제하는 것을 잊어버리는 것은 별 문제로 하고, 여러분은 클래스 계층 구조 안에서 가상이 아닌 디스트럭터들도 그러한 문제를 발생시킬 수 있다는 사실을 특별히 명심해야 한다(왜냐하면, 우리가 보았듯이 그것은 하나의 객체가 파괴될 때 잘못된 디스트럭터가 호출되도록 할 수 있기 때문이다). 물론, 여러분의 프로그램이 종료될 때 모든 메모리는 해제된다. 그러나 프로그램이 실행되고 있을 때 그것은 여러분의 프로그램에 할당된 채로 남아 있게 된다. 메모리 유출은 대부분의 경우, 어떠한 명백한 증상도 나타내지 않는다. 어떤 경우에는 전혀 증상을 나타내지 않는다. 그러나 그것은 여러분 컴퓨터의 성능에 해로운 영향을 끼친다. 왜냐하면, 메모리가 어떤 좋은 목적으로 점유되지 않기 때문이다. 때로, 모든 사용 가능한 메모리가 할당되었을 때 그것은 큰 혼란을 야기시킬 수 있다.

여러분의 프로그램이 free store를 사용하는 것을 감시하기 위해서 비주얼 C++는 일련의 진단 루틴들을 제공한다(그것들은 free store의 특별한 디버그 버전을 사용한다). 그것들은 crtdbg.h라는 헤더 안에서 선언된다. 그러한 루틴들에 대한 모든 호출들은 여러분 프로그램의 릴리즈 버전으로부터 자동적으로 삭제된다. 그러므로 여러분은 그것들에 대해서 어떠한 전처리기 제어도 추가할 필요가 없다.

6.1 free store를 검사하는 함수들

 

free store를 검사하는 작업에서 어떠한 것들이 포함되는가에 대해서 전체적으로 살펴본다. 그런 다음, 어떻게 메모리 유출이 검출되는가에 대해서 살펴본다. crtdbg.h 안에서 선언되는 함수들은 _CrtMemState 형식을 갖는 구조 안에 저장되어 있는 free store의 상태에 대한 기록을 사용하여 free store를 검사한다. 이 구조는 비교적 간단한데, 다음과 같이 정의된다.

typedef struct _CrtMemState

{

    struct _CrtMemBlockHeader* pBlockHeader; // 최근에 할당된 블록에 대한 포인터들

    unsigned long lCounts[_MAX_BLOCKS]; //블록이 각 형식들에 대한 카운터

    unsigned long ISizes][_MAX_BLOCKS]; //각 블록 형식에 할당되어 있는 전체 바이트

    unsigned long lHighWaterCount; //현재까지 한번에 가장 많이 할당된 바이트

    unsigned long lTotalCount; //현재 할당된 전체 바이트

} _CrtMemState;

 

우리는 free store의 상태에 관한 세부적인 사항에 대해서는 직접적으로 살펴보지 않을 것이다. 왜냐하면, 우리는 정보를 좀더 읽기 쉬운 형식으로 나타내는 함수들을 사용할 것이기 때문이다. free store의 작업을 추적하는 매우 많은 함수들이 있다. 그러나 우리는 가장 살펴볼 만한 5개에 대해서만 알아볼 것이다.

그것들은 여러분에게 다음과 같은 기능들을 제공한다.

◇ 어떠한 지점에 있는 free store의 상태도 기록하는 기능

◇ free store의 두 가지 상태의 차이점을 알아내는 기능

◇ 상태 정보 출력하기

◇ free store 안에 있는 객체들에 관한 정보를 출력하는 기능

◇ 메모리 유출을 검출하는 기능

다음은 그러한 함수들의 선언들과 함께 그것들이 어떤 것을 하는가에 대한 간단한 설명 이다.

void _CrtMemCheckpoint (_CrtMemState* state);

 

이것은 free store의 현재의 상태를 _CrtMemState 구조 안에 저장한다. 여러분이 함수에 전달하는 인수는 상태가 기록될 _CrtMemState 구조에 대한 포인터이다.

int _CrtMemDifference (_CrtMemState* stateDiff,

const _CrtMemState* oldstate,

const _CrtMemState* newState);

 

이 함수는 세 번째 인수에 의해서 지정된 상태를 두 번째 인수 안에서 여러분이 지정한 이전의 상태와 비교한다. 그 차이점은 여러분이 첫 번째 인수 안에서 지정한 _CrtMemState 구조 안에 저장된다. 만약 상태들이 다르면 함수는 0이 아닌 값(true)을 리턴하고, 그렇지 않으면 0(false)을 리턴한다.

void _CrtMemDumStatistics(const _CrtMemState* state);

이것은 인수에 의해서 지정된 free store 상태에 관한 정보를 출력 스트림으로 전달한다. 인수에 의해서 가리켜지고 있는 상태 구조는 여러분이 _CrtMemCheckPoint()를 사용하여 기록했던 상태이거나, 또는 _CrtMemDifference()에 의해서 생성된 두 개의 상태의 차이점일 수도 있다.

void _CrtMemDumpAllObjectsSince(const _CrtMemState* state);

이 함수는 free store의 상태가 인수에 의해서 지정된 이후로 free store 안에서 할당된 객체들에 대한 정보를 나타낸다(이것은 여러분의 프로그램에서 _CrtMemCheckPoint()에 대한 이전의 호출에 의해서 기록될 것이다). 만약 여러분이 null을 함수에 전달하면 그것은 여러분의 프로그램이 실행된 이후에 할당된 모든 객체들에 대한 정보를 나타낸다.

int _CrtDumpMemoryLeaks();

이것은 우리의 샘플에 대해서 필요한 함수이다 왜냐하면, 이것은 메모리 유출을 검사하고, 검출된 모든 유출에 대한 정보를 나타내기 때문이다. 여러분은 이 함수를 어느 때고 호출할 수 있다. 그러나 매우 유용한 메커니즘을 사용하여 여러분의 프로그램이 종료될 때 그 함수가 자동적으로 호출되도록 할 수 있다. 만약 여러분이 그러한 메커니즘을 인에이블시켰다면 프로그램 실행 동안에 발생하는 어떠한 메모리 유출도 자동적으로 검출하게 될 것이다. 그러므로 우리가 어떻게 그렇게 하는가에 대해서 살펴본다.

6.2 free store 디버그 작업 제어하기

비트 필드

설명

_CRTDBG_ALLOC_MEM_DF

이 비트가 on되면 이것은 디버그 할당을 on시킨다. 그러므로 free store 상태를 추적할 수 있다.

_CRTDBG_DELAY_FREE_MEM_DF

이 비트가 on되면 이것은 메모리가 delete에 의해서 해제되지 않도록 한다. 그러므로 여러분은 메모리가 부족한 상황에서 어떤 것이 발생하는가를 알아 볼 수 있다.

_CRTDBG_CHECK_ALWAYS_DF

이 비트가 on되면 이것은 _CrtCheckMemory() 함수가 모든 new와 delete에 대해서 자동적으로 호출되도록 한다. 이 함수는 free store가 잘 보존되어 있는가를 검사한다. 예를 들어, 배열의 범위를 넘어서는 값을 저장함으로써 블록들이 덮어씌여지지 않았는가를 검사한다. 만약 어떤 결합이 발견되면 보고서가 출력된다. 이것은 실행을 느리게 한다. 그러나 에러를 재빨리 잡아낸다.

_CRTDBG_CHECK_CRT_DF

이 비트가 on되면 런 타임 라이브러리에 의해서 내부적으로 사용되는 메모리가 디버그 작업 등안에 추적된다.

_CRTDBG_LEAK_CHECK_DF

프로그램이 종료될 때 _CrtDumpMemoryLeaks()를 자동적으로 호출함으로써 유출 검사 작업이 실행되도록 한다. 만약 여러분의 프로그램이 할당된 모든 메모리를 해제하는 데 실패한 경우에만 출력을 얻게 된다.

여러분은 int 형식을 갖는 _crtDbgFlag 플래그를 설정함으로써 free store 디버그 작업을 제어한다. 이 플래그는 5개의 개별적인 제어 비트를 포함하는데, 그 중 하나는 자동 메모리 유출 검사를 인에이블하는 데 사용된다. 여러분은 그러한 제어 비트들을 다음의 식별자들을 사용하여 지정한다.

디폴트로, _CRTDBG_ALL0C_MEM_DF 비트가 on된다. 그리고 다른 모든 것들은 off된다. 이러한 비트들의 조합을 설정하고 해제하는 데 여러분은 반드시 bitwise 연산자를 사용해야 한다. _crtDbgFlag 플래그를 설정하기 위해서 여러분은 int 형식의 플래그를 여러분이 필요로 하는 지시자들의 조합을 구현하는 _CrtDbgFlag() 함수에 전달해야 한다. 여러분이 원하는 지시자들을 설정하는 한가지 방법은, 먼저 _crtDbgFlag 플래그의 현재 상태를 얻는 것이다. 여러분은 그것을 다음과 같이 _CrtSetDbgFlag() 함수를 _CRTDBG_REPORT_FLAG 인수와 함께 호출함으로써 할 수 있다.

int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); //현재 플래그를 얻는다.

그런 다음, 여러분은 bitwise 연산자를 사용하여 이 플래그를 갖는 개별적인 지시자들에 대한 식별자들을 결합시킴으로써 그러한 지시자들을 설정 또는 해제할 수 있다. 지시자를 on시키려면 여러분은 지시자 식별자를 플래그와 함께 OR 연산시켜야 한다. 예를 들어, 자동 메모리 유출 검사 지시자를 on시키려면 플래그 안에서 여러분은 다음과 같이 작성할 수 있다.

flag |= _CRTDBG_LEAK_CHECK_DF;

지시자를 off시키려면 식별자의 부정을 플래그와 함께 AND 연산시켜야 한다. 예를 들어, 라이브러리에 의해서 내부적으로 사용되는 메모리를 추적하는 것을 off시키려면 다음과 같이 작성할 수 있다.

flag &= ~_CRTDBG_CHECK_CRT_DF;

새로운 플래그가 효과를 발휘하게 하려면 플래그를 인수로 하여 _CrtSetDbgFlag()를 호출하면 된다.

_CrtSetDbgFlag(flag);

그 외에, 여러분이 원하는 지시자들에 대한 모든 식별자들을 OR 연산시켜서 그 결과를 _CrtSetDbgflag()에 대한 인수로서 전달할 수 있다. 만약 프로그램이 종료될 때 메모리 유출에 대해서만 검사하고자 한다면 다음과 같이 작성한다.

_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF |_CRTDBG_ALL0C_MEM_DF);

여러분의 프로그램에 있는 다양한 지점에서 비트들을 설정/해제하기보다는 지시자들의 특정한 조합을 원한다면 그렇게 하는 가장 쉬운 방법이다. 거의 우리는 동적 메모리 디버깅 기능을 샘플에 적용할 때에 도달했다. free store 디버깅 출력을 어디로 보낼 깃인가를 우리가 어떻게 결정하는가에 대해서 살펴보아야 한다.

6.3 free store 디버깅 출력

free store 디버깅 함수들로부터의 출력의 목적지는 디폴트로 표준 출력 스트림이 아니다. 그것들은 디버그 메시지 윈도우로 간다. 만약 그것들을 stdout에서 보기를 원한다면 우리가 반드시 그렇게 설정해야 한다. 이것에는 두 가지의 함수가 있다. _CrtSetReportMode()는 출력에 대한 일반적인 목적지를 설정하며, _CrtSetReportFile()은 특별히 스트림 목적지를 지정한다. _CrtSetReportMode()는 다음과 같이 선언된다.

int _CrtSetReportMode(int reportType, int reportMode);

리포트 형식

설명

_CRT_WARN

다양한 종류의 경고 메시지를 나타낸다. 메모리 유출이 발생했을 때의 출력이 하나의 경고이다.

_CRT_ERROR

복구할 수 없는 문제를 발생시키는 치명적인 에러

_CRT_ASSERT

assertion으로부터의 출력(우리가 이전에 설명했던 assert()는 함수로부터의 출력이 아닌)

free store 디버깅 함수들에 의해서 세 종류의 출력이 생성된다. _CrtSetReportMode() 함수에 대한 각각의 호출은 두 번째 인수에 의해서 지정된 목적지를 첫 번째 인수에 의해서 지정된 출력 형식으로 설정한다. 여러분은 리포트 형식을 다음에 있는 식별자들 중의 하나로 지정하게 된다.

crtdbg.h 헤더는 ASSERT와 ASSERTE, 두 개의 매크로를 정의하는데, 이것들은 표준 라이브러리에 있는 assert() 함수와 매우 동일한 방식으로 작동한다. 이 두 매크로 사이의 차이점은, ASSERTE는 실패가 발생할 때 assertion 표현을 보고하는 반면, ASSERT는 그렇지 않는다는 것이다.

여러분은 다음의 식별자들의 조합을 사용하여 리포트 모드를 지정한다.

리포트 모드

_CrtDbgReport

_CRTDBG_M0DE_DEBUG

이것은 디폴트 모드로서 디버거의 제어 아래에서 실행할 때 디버그 윈도우 안에서 여러분이 보게 될 디버그 문자열 안에 출력을 전달한다.

_CRTDBG_MODE_FILE

출력은 출력 스트림으로 전달된다.

_CRTDBG_MODE_WNDW

출력은 메시지 박스 안에 나타난다.

_CRTDBG_REPORT_MODE

만약 여러분이 이것을 지정하면 _CrtSetReportMode() 함수는 단지 현재의 리포트 모드를 리턴한다.

한 개 이상의 목적지를 지정하려면 여러분은 | 연산자를 사용하여 식별자들을 OR 연산시키면 된다. 여러분은 _CrtSetReportMode()에 대한 개별적인 호출을 통해서 각각의 출력 형식에 대한 목적지를 설정할 수 있다. 메모리 유출이 검출되었을 때 출력을 파일 스트림으로 돌리기 위해 우리는 다음의 문장을 통해서 리포트 모드를 설정할 수 있다.

CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

이것은 목적지를 일반적으로 파일 스트림으로 설정한다. 목적지를 특별하게 지정하려면 여전히 _CrtSetReportFile() 함수를 호출해야 한다.

_CrtSetReportFile() 함수는 다음과 같이 선언된다.

_HFILE _CrtSetReportFile(int reportType, _HFILE reportFile);

여기서의 두 번째 인수는 파일 스트림에 대한 _HFILE 형식의 포인터이거나(이것에 대해서는 살펴보지 않을 것이다), 아니면 다음의 식별자들 중 하나이다.

리포트 파일

_CrtDbgReport 행동

_CRTDBG_FILE_STDERR

출력은 표준 에러 스트림인 stderr로 전달된다.

_CRTDBG_FILE_STDOUT

출력은 표준 출력 스트림인 stdout로 전달된다.

_CRTDBG_REPORT_FILE

만약 여러분이 이인수를 지정하면 _CrtSerReportFile() 함수는 현재의 목적지를 리턴할 것이다.

6. 동적인 메모리 디버깅하기

 

메모리를 동적으로 할당하는 것은 잠재적인 버그의 가능성을 안고 있는 것이며, 아마도 이 상황에서 가장 흔한 버그는 메모리 유출일 것이다. 여러분에게 기억을 일깨우기 위해 메모리 유출이라는 것은 여러분이 new 연산자를 사용하여 메모리를 할당할 때 발생한다. 그러나 여러분이 그 메모리를 다 사용한 후에 그것을 다시 해제할 때 delete를 사용해서는 안된다. 여러분이 할당했던 메모리를 삭제하는 것을 잊어버리는 것은 별 문제로 하고, 여러분은 클래스 계층 구조 안에서 가상이 아닌 디스트럭터들도 그러한 문제를 발생시킬 수 있다는 사실을 특별히 명심해야 한다(왜냐하면, 우리가 보았듯이 그것은 하나의 객체가 파괴될 때 잘못된 디스트럭터가 호출되도록 할 수 있기 때문이다). 물론, 여러분의 프로그램이 종료될 때 모든 메모리는 해제된다. 그러나 프로그램이 실행되고 있을 때 그것은 여러분의 프로그램에 할당된 채로 남아 있게 된다. 메모리 유출은 대부분의 경우, 어떠한 명백한 증상도 나타내지 않는다. 어떤 경우에는 전혀 증상을 나타내지 않는다. 그러나 그것은 여러분 컴퓨터의 성능에 해로운 영향을 끼친다. 왜냐하면, 메모리가 어떤 좋은 목적으로 점유되지 않기 때문이다. 때로, 모든 사용 가능한 메모리가 할당되었을 때 그것은 큰 혼란을 야기시킬 수 있다.

여러분의 프로그램이 free store를 사용하는 것을 감시하기 위해서 비주얼 C++는 일련의 진단 루틴들을 제공한다(그것들은 free store의 특별한 디버그 버전을 사용한다). 그것들은 crtdbg.h라는 헤더 안에서 선언된다. 그러한 루틴들에 대한 모든 호출들은 여러분 프로그램의 릴리즈 버전으로부터 자동적으로 삭제된다. 그러므로 여러분은 그것들에 대해서 어떠한 전처리기 제어도 추가할 필요가 없다.

6.1 free store를 검사하는 함수들

 

free store를 검사하는 작업에서 어떠한 것들이 포함되는가에 대해서 전체적으로 살펴본다. 그런 다음, 어떻게 메모리 유출이 검출되는가에 대해서 살펴본다. crtdbg.h 안에서 선언되는 함수들은 _CrtMemState 형식을 갖는 구조 안에 저장되어 있는 free store의 상태에 대한 기록을 사용하여 free store를 검사한다. 이 구조는 비교적 간단한데, 다음과 같이 정의된다.

typedef struct _CrtMemState

{

    struct _CrtMemBlockHeader* pBlockHeader; // 최근에 할당된 블록에 대한 포인터들

    unsigned long lCounts[_MAX_BLOCKS]; //블록이 각 형식들에 대한 카운터

    unsigned long ISizes][_MAX_BLOCKS]; //각 블록 형식에 할당되어 있는 전체 바이트

    unsigned long lHighWaterCount; //현재까지 한번에 가장 많이 할당된 바이트

    unsigned long lTotalCount; //현재 할당된 전체 바이트

} _CrtMemState;

 

우리는 free store의 상태에 관한 세부적인 사항에 대해서는 직접적으로 살펴보지 않을 것이다. 왜냐하면, 우리는 정보를 좀더 읽기 쉬운 형식으로 나타내는 함수들을 사용할 것이기 때문이다. free store의 작업을 추적하는 매우 많은 함수들이 있다. 그러나 우리는 가장 살펴볼 만한 5개에 대해서만 알아볼 것이다.

그것들은 여러분에게 다음과 같은 기능들을 제공한다.

◇ 어떠한 지점에 있는 free store의 상태도 기록하는 기능

◇ free store의 두 가지 상태의 차이점을 알아내는 기능

◇ 상태 정보 출력하기

◇ free store 안에 있는 객체들에 관한 정보를 출력하는 기능

◇ 메모리 유출을 검출하는 기능

다음은 그러한 함수들의 선언들과 함께 그것들이 어떤 것을 하는가에 대한 간단한 설명 이다.

void _CrtMemCheckpoint (_CrtMemState* state);

 

이것은 free store의 현재의 상태를 _CrtMemState 구조 안에 저장한다. 여러분이 함수에 전달하는 인수는 상태가 기록될 _CrtMemState 구조에 대한 포인터이다.

int _CrtMemDifference (_CrtMemState* stateDiff,

const _CrtMemState* oldstate,

const _CrtMemState* newState);

 

이 함수는 세 번째 인수에 의해서 지정된 상태를 두 번째 인수 안에서 여러분이 지정한 이전의 상태와 비교한다. 그 차이점은 여러분이 첫 번째 인수 안에서 지정한 _CrtMemState 구조 안에 저장된다. 만약 상태들이 다르면 함수는 0이 아닌 값(true)을 리턴하고, 그렇지 않으면 0(false)을 리턴한다.

void _CrtMemDumStatistics(const _CrtMemState* state);

이것은 인수에 의해서 지정된 free store 상태에 관한 정보를 출력 스트림으로 전달한다. 인수에 의해서 가리켜지고 있는 상태 구조는 여러분이 _CrtMemCheckPoint()를 사용하여 기록했던 상태이거나, 또는 _CrtMemDifference()에 의해서 생성된 두 개의 상태의 차이점일 수도 있다.

void _CrtMemDumpAllObjectsSince(const _CrtMemState* state);

이 함수는 free store의 상태가 인수에 의해서 지정된 이후로 free store 안에서 할당된 객체들에 대한 정보를 나타낸다(이것은 여러분의 프로그램에서 _CrtMemCheckPoint()에 대한 이전의 호출에 의해서 기록될 것이다). 만약 여러분이 null을 함수에 전달하면 그것은 여러분의 프로그램이 실행된 이후에 할당된 모든 객체들에 대한 정보를 나타낸다.

int _CrtDumpMemoryLeaks();

이것은 우리의 샘플에 대해서 필요한 함수이다 왜냐하면, 이것은 메모리 유출을 검사하고, 검출된 모든 유출에 대한 정보를 나타내기 때문이다. 여러분은 이 함수를 어느 때고 호출할 수 있다. 그러나 매우 유용한 메커니즘을 사용하여 여러분의 프로그램이 종료될 때 그 함수가 자동적으로 호출되도록 할 수 있다. 만약 여러분이 그러한 메커니즘을 인에이블시켰다면 프로그램 실행 동안에 발생하는 어떠한 메모리 유출도 자동적으로 검출하게 될 것이다. 그러므로 우리가 어떻게 그렇게 하는가에 대해서 살펴본다.

6.2 free store 디버그 작업 제어하기

비트 필드

설명

_CRTDBG_ALLOC_MEM_DF

이 비트가 on되면 이것은 디버그 할당을 on시킨다. 그러므로 free store 상태를 추적할 수 있다.

_CRTDBG_DELAY_FREE_MEM_DF

이 비트가 on되면 이것은 메모리가 delete에 의해서 해제되지 않도록 한다. 그러므로 여러분은 메모리가 부족한 상황에서 어떤 것이 발생하는가를 알아 볼 수 있다.

_CRTDBG_CHECK_ALWAYS_DF

이 비트가 on되면 이것은 _CrtCheckMemory() 함수가 모든 new와 delete에 대해서 자동적으로 호출되도록 한다. 이 함수는 free store가 잘 보존되어 있는가를 검사한다. 예를 들어, 배열의 범위를 넘어서는 값을 저장함으로써 블록들이 덮어씌여지지 않았는가를 검사한다. 만약 어떤 결합이 발견되면 보고서가 출력된다. 이것은 실행을 느리게 한다. 그러나 에러를 재빨리 잡아낸다.

_CRTDBG_CHECK_CRT_DF

이 비트가 on되면 런 타임 라이브러리에 의해서 내부적으로 사용되는 메모리가 디버그 작업 등안에 추적된다.

_CRTDBG_LEAK_CHECK_DF

프로그램이 종료될 때 _CrtDumpMemoryLeaks()를 자동적으로 호출함으로써 유출 검사 작업이 실행되도록 한다. 만약 여러분의 프로그램이 할당된 모든 메모리를 해제하는 데 실패한 경우에만 출력을 얻게 된다.

여러분은 int 형식을 갖는 _crtDbgFlag 플래그를 설정함으로써 free store 디버그 작업을 제어한다. 이 플래그는 5개의 개별적인 제어 비트를 포함하는데, 그 중 하나는 자동 메모리 유출 검사를 인에이블하는 데 사용된다. 여러분은 그러한 제어 비트들을 다음의 식별자들을 사용하여 지정한다.

디폴트로, _CRTDBG_ALL0C_MEM_DF 비트가 on된다. 그리고 다른 모든 것들은 off된다. 이러한 비트들의 조합을 설정하고 해제하는 데 여러분은 반드시 bitwise 연산자를 사용해야 한다. _crtDbgFlag 플래그를 설정하기 위해서 여러분은 int 형식의 플래그를 여러분이 필요로 하는 지시자들의 조합을 구현하는 _CrtDbgFlag() 함수에 전달해야 한다. 여러분이 원하는 지시자들을 설정하는 한가지 방법은, 먼저 _crtDbgFlag 플래그의 현재 상태를 얻는 것이다. 여러분은 그것을 다음과 같이 _CrtSetDbgFlag() 함수를 _CRTDBG_REPORT_FLAG 인수와 함께 호출함으로써 할 수 있다.

int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); //현재 플래그를 얻는다.

그런 다음, 여러분은 bitwise 연산자를 사용하여 이 플래그를 갖는 개별적인 지시자들에 대한 식별자들을 결합시킴으로써 그러한 지시자들을 설정 또는 해제할 수 있다. 지시자를 on시키려면 여러분은 지시자 식별자를 플래그와 함께 OR 연산시켜야 한다. 예를 들어, 자동 메모리 유출 검사 지시자를 on시키려면 플래그 안에서 여러분은 다음과 같이 작성할 수 있다.

flag |= _CRTDBG_LEAK_CHECK_DF;

지시자를 off시키려면 식별자의 부정을 플래그와 함께 AND 연산시켜야 한다. 예를 들어, 라이브러리에 의해서 내부적으로 사용되는 메모리를 추적하는 것을 off시키려면 다음과 같이 작성할 수 있다.

flag &= ~_CRTDBG_CHECK_CRT_DF;

새로운 플래그가 효과를 발휘하게 하려면 플래그를 인수로 하여 _CrtSetDbgFlag()를 호출하면 된다.

_CrtSetDbgFlag(flag);

그 외에, 여러분이 원하는 지시자들에 대한 모든 식별자들을 OR 연산시켜서 그 결과를 _CrtSetDbgflag()에 대한 인수로서 전달할 수 있다. 만약 프로그램이 종료될 때 메모리 유출에 대해서만 검사하고자 한다면 다음과 같이 작성한다.

_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF |_CRTDBG_ALL0C_MEM_DF);

여러분의 프로그램에 있는 다양한 지점에서 비트들을 설정/해제하기보다는 지시자들의 특정한 조합을 원한다면 그렇게 하는 가장 쉬운 방법이다. 거의 우리는 동적 메모리 디버깅 기능을 샘플에 적용할 때에 도달했다. free store 디버깅 출력을 어디로 보낼 깃인가를 우리가 어떻게 결정하는가에 대해서 살펴보아야 한다.

6.3 free store 디버깅 출력

free store 디버깅 함수들로부터의 출력의 목적지는 디폴트로 표준 출력 스트림이 아니다. 그것들은 디버그 메시지 윈도우로 간다. 만약 그것들을 stdout에서 보기를 원한다면 우리가 반드시 그렇게 설정해야 한다. 이것에는 두 가지의 함수가 있다. _CrtSetReportMode()는 출력에 대한 일반적인 목적지를 설정하며, _CrtSetReportFile()은 특별히 스트림 목적지를 지정한다. _CrtSetReportMode()는 다음과 같이 선언된다.

int _CrtSetReportMode(int reportType, int reportMode);

리포트 형식

설명

_CRT_WARN

다양한 종류의 경고 메시지를 나타낸다. 메모리 유출이 발생했을 때의 출력이 하나의 경고이다.

_CRT_ERROR

복구할 수 없는 문제를 발생시키는 치명적인 에러

_CRT_ASSERT

assertion으로부터의 출력(우리가 이전에 설명했던 assert()는 함수로부터의 출력이 아닌)

free store 디버깅 함수들에 의해서 세 종류의 출력이 생성된다. _CrtSetReportMode() 함수에 대한 각각의 호출은 두 번째 인수에 의해서 지정된 목적지를 첫 번째 인수에 의해서 지정된 출력 형식으로 설정한다. 여러분은 리포트 형식을 다음에 있는 식별자들 중의 하나로 지정하게 된다.

crtdbg.h 헤더는 ASSERT와 ASSERTE, 두 개의 매크로를 정의하는데, 이것들은 표준 라이브러리에 있는 assert() 함수와 매우 동일한 방식으로 작동한다. 이 두 매크로 사이의 차이점은, ASSERTE는 실패가 발생할 때 assertion 표현을 보고하는 반면, ASSERT는 그렇지 않는다는 것이다.

여러분은 다음의 식별자들의 조합을 사용하여 리포트 모드를 지정한다.

리포트 모드

_CrtDbgReport

_CRTDBG_M0DE_DEBUG

이것은 디폴트 모드로서 디버거의 제어 아래에서 실행할 때 디버그 윈도우 안에서 여러분이 보게 될 디버그 문자열 안에 출력을 전달한다.

_CRTDBG_MODE_FILE

출력은 출력 스트림으로 전달된다.

_CRTDBG_MODE_WNDW

출력은 메시지 박스 안에 나타난다.

_CRTDBG_REPORT_MODE

만약 여러분이 이것을 지정하면 _CrtSetReportMode() 함수는 단지 현재의 리포트 모드를 리턴한다.

한 개 이상의 목적지를 지정하려면 여러분은 | 연산자를 사용하여 식별자들을 OR 연산시키면 된다. 여러분은 _CrtSetReportMode()에 대한 개별적인 호출을 통해서 각각의 출력 형식에 대한 목적지를 설정할 수 있다. 메모리 유출이 검출되었을 때 출력을 파일 스트림으로 돌리기 위해 우리는 다음의 문장을 통해서 리포트 모드를 설정할 수 있다.

CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

이것은 목적지를 일반적으로 파일 스트림으로 설정한다. 목적지를 특별하게 지정하려면 여전히 _CrtSetReportFile() 함수를 호출해야 한다.

_CrtSetReportFile() 함수는 다음과 같이 선언된다.

_HFILE _CrtSetReportFile(int reportType, _HFILE reportFile);

여기서의 두 번째 인수는 파일 스트림에 대한 _HFILE 형식의 포인터이거나(이것에 대해서는 살펴보지 않을 것이다), 아니면 다음의 식별자들 중 하나이다.

리포트 파일

_CrtDbgReport 행동

_CRTDBG_FILE_STDERR

출력은 표준 에러 스트림인 stderr로 전달된다.

_CRTDBG_FILE_STDOUT

출력은 표준 출력 스트림인 stdout로 전달된다.

_CRTDBG_REPORT_FILE

만약 여러분이 이인수를 지정하면 _CrtSerReportFile() 함수는 현재의 목적지를 리턴할 것이다.

"특정 프로세스에서 사용하고 있는 핸들 수를 조회하는 방법"

 

#pragma warning(disable: 4996)

#include <stdio.h>

#include <windows.h>

#include <crtdbg.h>

#define malloc(size) _malloc_dbg(size, _NORMAL_BLOCK, __FILE__, __LINE__)

int GetHandleCount(DWORD dwPID);

int main(int argc, char *argv[])

{

int n1, n2, n3;

FILE *fp = NULL;

n1 = GetHandleCount(GetCurrentProcessId());

fp = fopen("./a.txt", "w");

n2 = GetHandleCount(GetCurrentProcessId());

fclose(fp);

n3 = GetHandleCount(GetCurrentProcessId());

printf("n1 [%d], n2 [%d], n3 [%d]\n", n1, n2, n3);

_CrtMemDumpAllObjectsSince(NULL);

return 0;

}

 

int GetHandleCount(DWORD dwPID)

{

typedef LONG NTSTATUS;

#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)

#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004L

typedef struct _UNICODE_STRING

{

USHORT Length;

USHORT MaximumLength;

PWSTR Buffer;

} UNICODE_STRING;

 

typedef struct _CLIENT_ID

{

HANDLE UniqueProcess;

HANDLE UniqueThread;

} CLIENT_ID;

 

typedef struct _THREAD_BASIC_INFORMATION

{ // Information Class 0

LONG ExitStatus;

PNT_TIB TebBaseAddress;

CLIENT_ID ClientId;

ULONG AffinityMask;

ULONG Priority;

ULONG BasePriority;

} THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;

 

typedef struct _VM_COUNTERS

{

ULONG PeakVirtualSize;

ULONG VirtualSize;

ULONG PageFaultCount;

ULONG PeakWorkingSetSize;

ULONG WorkingSetSize;

ULONG QuotaPeakPagedPoolUsage;

ULONG QuotaPagedPoolUsage;

ULONG QuotaPeakNonPagedPoolUsage;

ULONG QuotaNonPagedPoolUsage;

ULONG PagefileUsage;

ULONG PeakPagefileUsage;

} VM_COUNTERS;

 

typedef struct _SYSTEM_THREADS

{

LARGE_INTEGER KernelTime;

LARGE_INTEGER UserTime;

LARGE_INTEGER CreateTime;

ULONG WaitTime;

PVOID StartAddress;

CLIENT_ID ClientId;

LONG Priority;

LONG BasePriority;

ULONG ContextSwitchCount;

LONG State;

LONG WaitReason;

} SYSTEM_THREADS, * PSYSTEM_THREADS;

 

typedef struct _SYSTEM_PROCESSES

{

ULONG NextEntryDelta;

ULONG ThreadCount;

ULONG Reserved1[6];

LARGE_INTEGER CreateTime;

LARGE_INTEGER UserTime;

LARGE_INTEGER KernelTime;

UNICODE_STRING ProcessName;

ULONG BasePriority;

ULONG ProcessId;

ULONG InheritedFromProcessId;

ULONG HandleCount;

ULONG Reserved2[2];

VM_COUNTERS VmCounters;

IO_COUNTERS IoCounters;

SYSTEM_THREADS Threads[1];

} SYSTEM_PROCESSES, * PSYSTEM_PROCESSES;

 

typedef LONG (WINAPI *pZwQuerySystemInformation)(UINT,PVOID,ULONG,PULONG*);

pZwQuerySystemInformation ZwQuerySystemInformation;

 

PSYSTEM_PROCESSES pProcess = NULL;

BOOL bDone = FALSE;

ULONG nBuffSize = 0x100;

LPVOID pBuff = NULL;

HMODULE hMod;

int nHandleCount = 0;

 

if ( (hMod = GetModuleHandle("ntdll.dll")) == NULL )

return -1;

 

ZwQuerySystemInformation = (pZwQuerySystemInformation)GetProcAddress(hMod, "ZwQuerySystemInformation");

 

if( !ZwQuerySystemInformation )

return -1;

 

pBuff = malloc(sizeof(ULONG) * nBuffSize);

 

while (ZwQuerySystemInformation( 5, pBuff, nBuffSize, NULL) == STATUS_INFO_LENGTH_MISMATCH)

{

nBuffSize *= 2;

pBuff = realloc(pBuff, sizeof(ULONG) * nBuffSize);

}

 

for (pProcess = (PSYSTEM_PROCESSES)pBuff;

bDone == FALSE;

pProcess = (PSYSTEM_PROCESSES)(((LPBYTE)pProcess)+ pProcess->NextEntryDelta))

{

if ( pProcess->ProcessId == dwPID )

{

nHandleCount = pProcess->HandleCount;

break;

}

 

bDone = (pProcess->NextEntryDelta == 0);

}

 

free(pBuff);

 

return nHandleCount;

}

 

메모리 유출을 검출하는 출력을 표준 출력 스트림으로 전달하려면 다음과 같이 작성 한다.

_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STD0UT);

이제, 우리는 샘플에서 메모리 유출 검사 작업을 해보는 데 충분한 free store 디버그 루틴에 대한 지식을 갖게 되었다.

연습 - 메모리 유출 검사

심지어 우리가 프로젝트 설정 사항을 통하여 표준 출력 스트림을 하나의 파일로 가도록 했다고 해도 그것은 출력의 양을 감소시킬 것이다. 그러므로 우리는 이름들에 대한 배열의 크기를 5개 요소로 줄일 것이다. 다음은 일반적인 free store 디버그 기능과 특별한 메모리 유출 검출 기능을 사용하는 main()의 새로운 버전이다.

int main(int argc, char* argv[])

{

    // free store 디버깅 비트와 유출 검사 비트를 on시킨다.

    _CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF |_CRTDBG_ALLOC_MEM_DF);

 

    // stdout에 대한 직접적인 경고

    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

    _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);

 

    Name myName("Ivor", "Horton");    // 한 개의 객체를 실험해 본다.

 

    // 로컬 char 배열에 있는 이름들을 검색하며 저장한다.

    char theName[12];

    cout << "\nThe name is " << myName.getName(theName);

 

    // free store 안에 있는 배열에 이름을 저장한다.

    char* pName = new char[myName.getNameLength()+1];

    cout << "\n The name is " << myName.getName(pName);

 

    const int arraysize = 5;

    Name names[arraysize];    // 한 개의 배열에 대한 작업

 

    // 이름들을 초기화한다.

    init(names, arraysize);

 

    // 비교 작업을 한다.

    char* phrase = 0;    //비교 문구를 저장한다.

    char* iName = 0;    //전체 이름을 저장한다.

    char* jName = 0;    //전체 이름을 저장한다.

 

    for(int i = 0; i < arraysize ; i++)    // 각 요소를 비교한다.

    {

        iName = new char[names[i].getNameLength()+1];    // 모든 다른 사람들의

     // 첫째 이름을 저장하는 배열

 

        for(int j = i+1 ; j<arraysize ; j++)

        {

            if(names[i] < names[j])

                phrase = "less than";

            else if(names[i] > names[j])

                phrase = " greater than ";

            else if(names[i] == names[j])    // 불필요한 것 - 그러나 그것은 연산자

         // 함수를 호출한다.

 

                phrase = " equal to ";

            jName = new char[names[j].getNameLength()+1];    // 두 번째의 이름을 저장하는 배열

            cout << endl << names[i].getName(iName) << " is" <<     phrase << names[j].getName (jName);

        }

    }

    court << endl;

    return 0;

}

 

여러분은 물론 crtdbg.h에 대한 #include를 파일에 추가해야 한다. 출력을 더 줄이려고 우리는 DebugStuff.h 헤더 안에 있는 제어 심볼들을 커멘트 처리함으로써 추적 기능에 대한 출력을 off시킬 수 있다.

// DebugStuff.h - 디버깅 제어

#ifndef DEBUGSTUFF_H

#define DEBUGSTUFF_H

 

#ifdef _DEBUG

 

//#define CONSTRUCTOR_TRACE    // 컨스트럭터를 추적한 것을 출력한다.

//#define FUNCTION_TRACE    // 함수 호출들을 추적한다.

#endif

 

#endif    //DEBUGSTUFF_H

 

여러분은 이것을 재컴파일하여 다시 실행시킬 수 있다.

작동하는 방식

이것은 기대한 대로 작동한다. 우리는 프로그램이 정말로 메모리 유출을 발생시킨다는 출럭을 얻게 된다. 그리고 프로그램의 끝부분에서 객체들의 리스트를 얻게 된다. free store 디버그 기능에 의해서 생성되는 출력은 다음과 같이 시작한다.

Detected memory leaks!

Dumping objects ->

{55} normal block at Ox007A1FE0,억 FEO, 16 bytes long.

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

{54} normal block at OxO07AO030, 16 bytes long.

Data: <Emily Steinbeck > 45 6D 69 6C 79 20 53 74 65 69 6E 62 65 63 6B 00

{53} normal block at OxO07AO070, 13 bytes long.

Data: <Emily Miller > 45 6D 69 6C 79 20 4D 69 6C 6C 65 72 00

...

 

다음과 같이 끝난다.

...

{32} normal block at OxO07A05BO, 8 bvtes long.

Data: <Dickens > 44 69 63 6B 65 6E 73 00

{31} normal block at OxO07AOC90, 8 bytes long.

Data: <Charles > 43 68 61 72 6C 65 73 00

{30} normal block at Ox077AOCDO, 12 bytes long.

Data: <Ivor Horton > 49 76 6F 72 20 48 6F 72 74 6F 6E 00

{27} normal block at OxO07AOFOO, 7 bytes long.

Data: <Horton > 48 6F 72 74 6F 6E 00

{26} normal block at 0x007A0F40, 5 bvtes long.

Data: <Ivor > 49 76 6F 72 00

Object dump complete.

 

free store 안에 남아 있는 것으로 보고된 객체들은 가장 최근에 할당된 것이 먼저 나타나고, 가장 먼저 할당된 것이 가장 나중에 나타난다. 출력을 보면 Name 클래스가 그것의 데이터 멤버에 대해서 메모리를 할당하고는 릴리즈하지 않았다는 것이 분명하다. 마지막에 있는 세 개의 객체들은 main()에서 할당된 pName 배열과 myName 객체의 데이터 멤버들에 해당된다. 완벽한 이름들에 대한 블록은 main() 안에서 할당된다. 그리고 그것들도 역시 사용하고서 그대로 방치되고 있다. 클래스가 갖고 있는 문제는 메모리를 동적으로 할당하는 클래스와 관계된 기본적인 규칙들을 우리가 잊어버린 데서 발생하였다(그것들은 항상 디스트럭터, copy 컨스트럭터, 그리고 할당 연산자를 정의한다). 클래스는 다음과 같이 선언되어야 한다.

class Name

{

    public:

    Name();    //디폴트 컨스트럭터

    Name(const char* pFirst, const char* pSecond);    // 컨스트럭터

 

    Name(const Name& rName);    // Copy 컨스트럭터

 

    ~Name();    // 디스트럭터

 

    char* getName(char* pName) const;    // 전체 이름을 얻는다.

 

    int getNameLength() const;    // 전체 이름의 길이를 얻는다.

 

    // 이름들에 대한 비교 연산자들

    bool operator<(const Name& name) const;

    bool operator==(const Name& name) const;

    bool operator>(const Name& name) const;

 

    Name& operator=(const Name& rName);    // 할당 연산자들

 

private:

    char* pFirstname;

    char* pSurname;

};

 

다음처럼 copy 컨스트럭터를 정의할 수 있다.

Name::Name(const Name& rName)

{

    pFirstname = new char[strlen(rName.pFirstname)+1];    // 첫 번째 이름에 대한

     // 공간을 할당하여

    strcpy(pFirstname, rName.pFirstname);                // 그것을 복사한다.

    pSurname = new char[strlen(rName.pSurname)+1];        //성의 경우와 동일하다.

 

    strcpy(pSurname, rName.pSurname);

}

 

디스트럭터는 두 개의 데이터 멤버들에 대한 메모리를 해제해야 한다.

Name::~Name()

{

    delete() pFirstname;

    delete() pSurname;

}

할당 연산자에서 우리는 왼쪽과 오른쪽이 동일하게 되도록 준비해야 한다.

 

 

Name& Name::operator=(const Name& rName)

{

    if (this == &rName)    // Ihs가 rhs와 같다면

    return *this;            // 객체를 리턴한다.

 

    delete[] pFirstname;

    pFirstname = new char[strlen(rName.pFirstname)+1];    //첫번째 이름에 대한

     // 공간을 할당하여

    strcpy(pFirstname, rName.pFirstname);                // 그것을 복사한다.

 

    pSurname = new char[strlen(rName.pSurname)+1];        // 성의 경우와 동일하다.

 

    strcpy(pFirstname, rName.pSurname);

 

    return *this;

}

 

또한, 우리는 디폴트 컨스트럭터가 적절하게 작동하도록 해야 한다. 만약 디폴트 컨스트럭터가 free store 안에서 메모리를 할당하치 않으면 free store 안에서 할당되지 않은 메모리를 디스트럭터가 잘못하여 삭제하려고 할 것이다. 우리는 다음과 같이 그것을 수정해야 한다.

Name::Name()

{

#ifdef CONSTRUCTOR_TRACE

    // 컨스트럭터 호출들을 추적한다.

 

    cout << "\nDefault Name constructor called.";

 

#endif

 

    // 빈 문자열들에 대해 1의 배열을 할당한다.

    pFirstname = new char[1];

    pSurname = new char[1];

 

    pFirstname[0] = pSurname[0] ='\0';    // null 문자를 저장한다.

}

만약 여러분이 문장들을 main()에 추가하여 거기서 동적으로 할당된 메모리를 삭제하려고 한다면 프로그램은 메모리 유출에 관계된 어떠한 메시지도 없이 실행되어야 한다.

 

메모리 유출을 검출하는 출력을 표준 출력 스트림으로 전달하려면 다음과 같이 작성 한다.

_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STD0UT);

이제, 우리는 샘플에서 메모리 유출 검사 작업을 해보는 데 충분한 free store 디버그 루틴에 대한 지식을 갖게 되었다.

연습 - 메모리 유출 검사

심지어 우리가 프로젝트 설정 사항을 통하여 표준 출력 스트림을 하나의 파일로 가도록 했다고 해도 그것은 출력의 양을 감소시킬 것이다. 그러므로 우리는 이름들에 대한 배열의 크기를 5개 요소로 줄일 것이다. 다음은 일반적인 free store 디버그 기능과 특별한 메모리 유출 검출 기능을 사용하는 main()의 새로운 버전이다.

int main(int argc, char* argv[])

{

    // free store 디버깅 비트와 유출 검사 비트를 on시킨다.

    _CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF |_CRTDBG_ALLOC_MEM_DF);

 

    // stdout에 대한 직접적인 경고

    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

    _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);

 

    Name myName("Ivor", "Horton");    // 한 개의 객체를 실험해 본다.

 

    // 로컬 char 배열에 있는 이름들을 검색하며 저장한다.

    char theName[12];

    cout << "\nThe name is " << myName.getName(theName);

 

    // free store 안에 있는 배열에 이름을 저장한다.

    char* pName = new char[myName.getNameLength()+1];

    cout << "\n The name is " << myName.getName(pName);

 

    const int arraysize = 5;

    Name names[arraysize];    // 한 개의 배열에 대한 작업

 

    // 이름들을 초기화한다.

    init(names, arraysize);

 

    // 비교 작업을 한다.

    char* phrase = 0;    //비교 문구를 저장한다.

    char* iName = 0;    //전체 이름을 저장한다.

    char* jName = 0;    //전체 이름을 저장한다.

 

    for(int i = 0; i < arraysize ; i++)    // 각 요소를 비교한다.

    {

        iName = new char[names[i].getNameLength()+1];    // 모든 다른 사람들의

     // 첫째 이름을 저장하는 배열

 

        for(int j = i+1 ; j<arraysize ; j++)

        {

            if(names[i] < names[j])

                phrase = "less than";

            else if(names[i] > names[j])

                phrase = " greater than ";

            else if(names[i] == names[j])    // 불필요한 것 - 그러나 그것은 연산자

         // 함수를 호출한다.

 

                phrase = " equal to ";

            jName = new char[names[j].getNameLength()+1];    // 두 번째의 이름을 저장하는 배열

            cout << endl << names[i].getName(iName) << " is" <<     phrase << names[j].getName (jName);

        }

    }

    court << endl;

    return 0;

}

 

여러분은 물론 crtdbg.h에 대한 #include를 파일에 추가해야 한다. 출력을 더 줄이려고 우리는 DebugStuff.h 헤더 안에 있는 제어 심볼들을 커멘트 처리함으로써 추적 기능에 대한 출력을 off시킬 수 있다.

// DebugStuff.h - 디버깅 제어

#ifndef DEBUGSTUFF_H

#define DEBUGSTUFF_H

 

#ifdef _DEBUG

 

//#define CONSTRUCTOR_TRACE    // 컨스트럭터를 추적한 것을 출력한다.

//#define FUNCTION_TRACE    // 함수 호출들을 추적한다.

#endif

 

#endif    //DEBUGSTUFF_H

 

여러분은 이것을 재컴파일하여 다시 실행시킬 수 있다.

작동하는 방식

이것은 기대한 대로 작동한다. 우리는 프로그램이 정말로 메모리 유출을 발생시킨다는 출럭을 얻게 된다. 그리고 프로그램의 끝부분에서 객체들의 리스트를 얻게 된다. free store 디버그 기능에 의해서 생성되는 출력은 다음과 같이 시작한다.

Detected memory leaks!

Dumping objects ->

{55} normal block at Ox007A1FE0,억 FEO, 16 bytes long.

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

{54} normal block at OxO07AO030, 16 bytes long.

Data: <Emily Steinbeck > 45 6D 69 6C 79 20 53 74 65 69 6E 62 65 63 6B 00

{53} normal block at OxO07AO070, 13 bytes long.

Data: <Emily Miller > 45 6D 69 6C 79 20 4D 69 6C 6C 65 72 00

...

 

다음과 같이 끝난다.

...

{32} normal block at OxO07A05BO, 8 bvtes long.

Data: <Dickens > 44 69 63 6B 65 6E 73 00

{31} normal block at OxO07AOC90, 8 bytes long.

Data: <Charles > 43 68 61 72 6C 65 73 00

{30} normal block at Ox077AOCDO, 12 bytes long.

Data: <Ivor Horton > 49 76 6F 72 20 48 6F 72 74 6F 6E 00

{27} normal block at OxO07AOFOO, 7 bytes long.

Data: <Horton > 48 6F 72 74 6F 6E 00

{26} normal block at 0x007A0F40, 5 bvtes long.

Data: <Ivor > 49 76 6F 72 00

Object dump complete.

 

free store 안에 남아 있는 것으로 보고된 객체들은 가장 최근에 할당된 것이 먼저 나타나고, 가장 먼저 할당된 것이 가장 나중에 나타난다. 출력을 보면 Name 클래스가 그것의 데이터 멤버에 대해서 메모리를 할당하고는 릴리즈하지 않았다는 것이 분명하다. 마지막에 있는 세 개의 객체들은 main()에서 할당된 pName 배열과 myName 객체의 데이터 멤버들에 해당된다. 완벽한 이름들에 대한 블록은 main() 안에서 할당된다. 그리고 그것들도 역시 사용하고서 그대로 방치되고 있다. 클래스가 갖고 있는 문제는 메모리를 동적으로 할당하는 클래스와 관계된 기본적인 규칙들을 우리가 잊어버린 데서 발생하였다(그것들은 항상 디스트럭터, copy 컨스트럭터, 그리고 할당 연산자를 정의한다). 클래스는 다음과 같이 선언되어야 한다.

class Name

{

    public:

    Name();    //디폴트 컨스트럭터

    Name(const char* pFirst, const char* pSecond);    // 컨스트럭터

 

    Name(const Name& rName);    // Copy 컨스트럭터

 

    ~Name();    // 디스트럭터

 

    char* getName(char* pName) const;    // 전체 이름을 얻는다.

 

    int getNameLength() const;    // 전체 이름의 길이를 얻는다.

 

    // 이름들에 대한 비교 연산자들

    bool operator<(const Name& name) const;

    bool operator==(const Name& name) const;

    bool operator>(const Name& name) const;

 

    Name& operator=(const Name& rName);    // 할당 연산자들

 

private:

    char* pFirstname;

    char* pSurname;

};

 

다음처럼 copy 컨스트럭터를 정의할 수 있다.

Name::Name(const Name& rName)

{

    pFirstname = new char[strlen(rName.pFirstname)+1];    // 첫 번째 이름에 대한

     // 공간을 할당하여

    strcpy(pFirstname, rName.pFirstname);                // 그것을 복사한다.

    pSurname = new char[strlen(rName.pSurname)+1];        //성의 경우와 동일하다.

 

    strcpy(pSurname, rName.pSurname);

}

 

디스트럭터는 두 개의 데이터 멤버들에 대한 메모리를 해제해야 한다.

Name::~Name()

{

    delete() pFirstname;

    delete() pSurname;

}

할당 연산자에서 우리는 왼쪽과 오른쪽이 동일하게 되도록 준비해야 한다.

 

 

Name& Name::operator=(const Name& rName)

{

    if (this == &rName)    // Ihs가 rhs와 같다면

    return *this;            // 객체를 리턴한다.

 

    delete[] pFirstname;

    pFirstname = new char[strlen(rName.pFirstname)+1];    //첫번째 이름에 대한

     // 공간을 할당하여

    strcpy(pFirstname, rName.pFirstname);                // 그것을 복사한다.

 

    pSurname = new char[strlen(rName.pSurname)+1];        // 성의 경우와 동일하다.

 

    strcpy(pFirstname, rName.pSurname);

 

    return *this;

}

 

또한, 우리는 디폴트 컨스트럭터가 적절하게 작동하도록 해야 한다. 만약 디폴트 컨스트럭터가 free store 안에서 메모리를 할당하치 않으면 free store 안에서 할당되지 않은 메모리를 디스트럭터가 잘못하여 삭제하려고 할 것이다. 우리는 다음과 같이 그것을 수정해야 한다.

Name::Name()

{

#ifdef CONSTRUCTOR_TRACE

    // 컨스트럭터 호출들을 추적한다.

 

    cout << "\nDefault Name constructor called.";

 

#endif

 

    // 빈 문자열들에 대해 1의 배열을 할당한다.

    pFirstname = new char[1];

    pSurname = new char[1];

 

    pFirstname[0] = pSurname[0] ='\0';    // null 문자를 저장한다.

}

만약 여러분이 문장들을 main()에 추가하여 거기서 동적으로 할당된 메모리를 삭제하려고 한다면 프로그램은 메모리 유출에 관계된 어떠한 메시지도 없이 실행되어야 한다.

 

메모리 유출을 검출하는 출력을 표준 출력 스트림으로 전달하려면 다음과 같이 작성 한다.

_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STD0UT);

이제, 우리는 샘플에서 메모리 유출 검사 작업을 해보는 데 충분한 free store 디버그 루틴에 대한 지식을 갖게 되었다.

연습 - 메모리 유출 검사

심지어 우리가 프로젝트 설정 사항을 통하여 표준 출력 스트림을 하나의 파일로 가도록 했다고 해도 그것은 출력의 양을 감소시킬 것이다. 그러므로 우리는 이름들에 대한 배열의 크기를 5개 요소로 줄일 것이다. 다음은 일반적인 free store 디버그 기능과 특별한 메모리 유출 검출 기능을 사용하는 main()의 새로운 버전이다.

int main(int argc, char* argv[])

{

    // free store 디버깅 비트와 유출 검사 비트를 on시킨다.

    _CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF |_CRTDBG_ALLOC_MEM_DF);

 

    // stdout에 대한 직접적인 경고

    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

    _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);

 

    Name myName("Ivor", "Horton");    // 한 개의 객체를 실험해 본다.

 

    // 로컬 char 배열에 있는 이름들을 검색하며 저장한다.

    char theName[12];

    cout << "\nThe name is " << myName.getName(theName);

 

    // free store 안에 있는 배열에 이름을 저장한다.

    char* pName = new char[myName.getNameLength()+1];

    cout << "\n The name is " << myName.getName(pName);

 

    const int arraysize = 5;

    Name names[arraysize];    // 한 개의 배열에 대한 작업

 

    // 이름들을 초기화한다.

    init(names, arraysize);

 

    // 비교 작업을 한다.

    char* phrase = 0;    //비교 문구를 저장한다.

    char* iName = 0;    //전체 이름을 저장한다.

    char* jName = 0;    //전체 이름을 저장한다.

 

    for(int i = 0; i < arraysize ; i++)    // 각 요소를 비교한다.

    {

        iName = new char[names[i].getNameLength()+1];    // 모든 다른 사람들의

     // 첫째 이름을 저장하는 배열

 

        for(int j = i+1 ; j<arraysize ; j++)

        {

            if(names[i] < names[j])

                phrase = "less than";

            else if(names[i] > names[j])

                phrase = " greater than ";

            else if(names[i] == names[j])    // 불필요한 것 - 그러나 그것은 연산자

         // 함수를 호출한다.

 

                phrase = " equal to ";

            jName = new char[names[j].getNameLength()+1];    // 두 번째의 이름을 저장하는 배열

            cout << endl << names[i].getName(iName) << " is" <<     phrase << names[j].getName (jName);

        }

    }

    court << endl;

    return 0;

}

 

여러분은 물론 crtdbg.h에 대한 #include를 파일에 추가해야 한다. 출력을 더 줄이려고 우리는 DebugStuff.h 헤더 안에 있는 제어 심볼들을 커멘트 처리함으로써 추적 기능에 대한 출력을 off시킬 수 있다.

// DebugStuff.h - 디버깅 제어

#ifndef DEBUGSTUFF_H

#define DEBUGSTUFF_H

 

#ifdef _DEBUG

 

//#define CONSTRUCTOR_TRACE    // 컨스트럭터를 추적한 것을 출력한다.

//#define FUNCTION_TRACE    // 함수 호출들을 추적한다.

#endif

 

#endif    //DEBUGSTUFF_H

 

여러분은 이것을 재컴파일하여 다시 실행시킬 수 있다.

작동하는 방식

이것은 기대한 대로 작동한다. 우리는 프로그램이 정말로 메모리 유출을 발생시킨다는 출럭을 얻게 된다. 그리고 프로그램의 끝부분에서 객체들의 리스트를 얻게 된다. free store 디버그 기능에 의해서 생성되는 출력은 다음과 같이 시작한다.

Detected memory leaks!

Dumping objects ->

{55} normal block at Ox007A1FE0,억 FEO, 16 bytes long.

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

{54} normal block at OxO07AO030, 16 bytes long.

Data: <Emily Steinbeck > 45 6D 69 6C 79 20 53 74 65 69 6E 62 65 63 6B 00

{53} normal block at OxO07AO070, 13 bytes long.

Data: <Emily Miller > 45 6D 69 6C 79 20 4D 69 6C 6C 65 72 00

...

 

다음과 같이 끝난다.

...

{32} normal block at OxO07A05BO, 8 bvtes long.

Data: <Dickens > 44 69 63 6B 65 6E 73 00

{31} normal block at OxO07AOC90, 8 bytes long.

Data: <Charles > 43 68 61 72 6C 65 73 00

{30} normal block at Ox077AOCDO, 12 bytes long.

Data: <Ivor Horton > 49 76 6F 72 20 48 6F 72 74 6F 6E 00

{27} normal block at OxO07AOFOO, 7 bytes long.

Data: <Horton > 48 6F 72 74 6F 6E 00

{26} normal block at 0x007A0F40, 5 bvtes long.

Data: <Ivor > 49 76 6F 72 00

Object dump complete.

 

free store 안에 남아 있는 것으로 보고된 객체들은 가장 최근에 할당된 것이 먼저 나타나고, 가장 먼저 할당된 것이 가장 나중에 나타난다. 출력을 보면 Name 클래스가 그것의 데이터 멤버에 대해서 메모리를 할당하고는 릴리즈하지 않았다는 것이 분명하다. 마지막에 있는 세 개의 객체들은 main()에서 할당된 pName 배열과 myName 객체의 데이터 멤버들에 해당된다. 완벽한 이름들에 대한 블록은 main() 안에서 할당된다. 그리고 그것들도 역시 사용하고서 그대로 방치되고 있다. 클래스가 갖고 있는 문제는 메모리를 동적으로 할당하는 클래스와 관계된 기본적인 규칙들을 우리가 잊어버린 데서 발생하였다(그것들은 항상 디스트럭터, copy 컨스트럭터, 그리고 할당 연산자를 정의한다). 클래스는 다음과 같이 선언되어야 한다.

class Name

{

    public:

    Name();    //디폴트 컨스트럭터

    Name(const char* pFirst, const char* pSecond);    // 컨스트럭터

 

    Name(const Name& rName);    // Copy 컨스트럭터

 

    ~Name();    // 디스트럭터

 

    char* getName(char* pName) const;    // 전체 이름을 얻는다.

 

    int getNameLength() const;    // 전체 이름의 길이를 얻는다.

 

    // 이름들에 대한 비교 연산자들

    bool operator<(const Name& name) const;

    bool operator==(const Name& name) const;

    bool operator>(const Name& name) const;

 

    Name& operator=(const Name& rName);    // 할당 연산자들

 

private:

    char* pFirstname;

    char* pSurname;

};

 

다음처럼 copy 컨스트럭터를 정의할 수 있다.

Name::Name(const Name& rName)

{

    pFirstname = new char[strlen(rName.pFirstname)+1];    // 첫 번째 이름에 대한

     // 공간을 할당하여

    strcpy(pFirstname, rName.pFirstname);                // 그것을 복사한다.

    pSurname = new char[strlen(rName.pSurname)+1];        //성의 경우와 동일하다.

 

    strcpy(pSurname, rName.pSurname);

}

 

디스트럭터는 두 개의 데이터 멤버들에 대한 메모리를 해제해야 한다.

Name::~Name()

{

    delete() pFirstname;

    delete() pSurname;

}

할당 연산자에서 우리는 왼쪽과 오른쪽이 동일하게 되도록 준비해야 한다.

 

 

 

Name& Name::operator=(const Name& rName)

{

    if (this == &rName)    // Ihs가 rhs와 같다면

    return *this;            // 객체를 리턴한다.

 

    delete[] pFirstname;

    pFirstname = new char[strlen(rName.pFirstname)+1];    //첫번째 이름에 대한

     // 공간을 할당하여

    strcpy(pFirstname, rName.pFirstname);                // 그것을 복사한다.

 

    pSurname = new char[strlen(rName.pSurname)+1];        // 성의 경우와 동일하다.

 

    strcpy(pFirstname, rName.pSurname);

 

    return *this;

}

 

또한, 우리는 디폴트 컨스트럭터가 적절하게 작동하도록 해야 한다. 만약 디폴트 컨스트럭터가 free store 안에서 메모리를 할당하치 않으면 free store 안에서 할당되지 않은 메모리를 디스트럭터가 잘못하여 삭제하려고 할 것이다. 우리는 다음과 같이 그것을 수정해야 한다.

Name::Name()

{

#ifdef CONSTRUCTOR_TRACE

    // 컨스트럭터 호출들을 추적한다.

 

    cout << "\nDefault Name constructor called.";

 

#endif

 

    // 빈 문자열들에 대해 1의 배열을 할당한다.

    pFirstname = new char[1];

    pSurname = new char[1];

 

    pFirstname[0] = pSurname[0] ='\0';    // null 문자를 저장한다.

}

만약 여러분이 문장들을 main()에 추가하여 거기서 동적으로 할당된 메모리를 삭제하려고 한다면 프로그램은 메모리 유출에 관계된 어떠한 메시지도 없이 실행되어야 한다.

이 중에서 첫번째 플랙만을 제외하고는 모두 디폴트로 꺼져있다. 그러므로 다음과 같이 메모리 검사 기능을 켜도록 한다

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 

위의 프로그램에서는 다음을 삭제한다

_CrtMemDumpAllObjectsSince(0);  

_CrtMemDumpAllObjectsSince 함수는 실제로는 특정 지점에서 지점간에 할당되어 있는 메모리들을 보고해 주는 함수이다. 인자로 0을 넘겨주면 처음부터 지금까지 할당되어 있는 메모리들을 보고해 준다. CRT가 아직 전역으로 할당된 메모리를 완전히 삭제하기도 전에 호출했기 때문에 STL의 메모리가 샌 것 처럼 보인것이다.

CRT 메모리 블럭

다음으로 넘어가기 전에 다음을 짚어보고 넘어가기로 하자. 디버그 버전에서는 메모리가 할당되거나 사용되기 직전에 특정한 값들로 할당된 메모리가 채워진다는 것을 알고 계실것이다. 의미는 다음과 같다

0xFD : 메모리 블록 양 끝의 버퍼에 생성된다
0xCD : 새로 생성된 버퍼에 저장되는 기본값이다
0xCC : 스택에 변수가 생성되면 우선 이값으로 채워진다
0xDD : 삭제된 메모리 블록은 이 값으로 채워진다

다음 예제를 보자

int main() 

{    // 함수 시작지점 

 

       _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 

 

       int *p = new int [10]; // 메모리가 할당되는 지점 

       printf("%d", *p); 

       delete [] p;  // 메모리가 삭제되는 지점 

 

       return 0; 

} 

함수 시작지점에서 포인터 p는 초기값으로 0xCCCCCCCC를 갖게 된다. 디스어셈블 해보면 모든 지역변수들을 0xCC로 채워넣는 것을 볼 수 있다. 이후 메모리가 할당되는 지점에서 아마도 sizeof(int) * 10 바이트의 메모리가 할당될것이다. Sizeof(int)가 4바이트라면(32비트 운영체제에서) 40바이트가 생성되는 것이다.(디버그 버전이라는 가정하에) 이렇게 생성된 40바이트에 0xCD값이 채워진다. 만약 어떤 포인터의 값을 읽었을 때 값이 0xCDCDCDCD와 같은 형식이거든 초기화안된 메모리를 읽었다고 생각하면 된다.

그런데, 위의 할당지점에서 실은 48바이트가 생성된다. 40바이트 할당 명령을 주었는데 왜 48바이트의 메모리가 할당되었는가 이유는 위의 0xFD값에 있다. 40바이트의 블록을 생성시키고 그 블록의 앞과 뒤에 각각 0xFDFDFDFD를 삽입시켰기 때문이다. 이 값이 채워지는 이유는 다음과 같이 접근하는 경우

p[-1] = 10; // underwrite 

p[11] = 10; // overwrite 

0xFD로 채워진 메모리의 일부분이 깨질 것이고, 나중에 사용할 메모리 체크 명령에 의해 overwrite/underwrite가 발생했다는 사실을 알 수 있게된다.

마지막으로 메모리가 삭제되는 지점에서는 이론 대로라면 p값이 0xDD로 채워질 것이다. 그러나 실제로 필자의 컴퓨터에서는 0xFEEE가 채워졌다. 왜 그런지는 좀더 연구해 보고 알려드리도록 하겠다. -.- 오늘은 일단 여기까지 접고 연구결과가 더 나오는대로 여러분께 보고하는 시간을 갖도록 하려한다.

CRT Debug 기능 사용법 3

지난번에 이어 이번에는 메모리에 어떤 문제가 있었는지를 체크해주는 _CrtCheckMemory함수에 대해 연구해 보도록 하겠습니다.

강좌를 진행하기 전에 지난번 강좌 마지막 부분에서 메모리가 삭제될 때 0xDD값 대신에 0xFEEE값이 채워지는 이유를 찾아본 결과 CRT에서는 0xDD값을 정확히 채워 넣는다는 것을 확인하였다. _CRTDBG_DELAY_FREE_MEM_DF 플랙이 디폴트로 꺼져 있기 때문에 삭제했다는 표시를 하고 난 다음 바로 운영체제에서 삭제(진짜 삭제) 해버렸기 때문이다. 이부분은 뒤에서 다시 알아보도록 하겠다.

_CrtCheckMemory()

지난번에 알아보기를 CRT에서 메모리를 할당하려 할 때 몇가지 정보블럭들을 설정한다는 것을 알았을것이다. 자 이제는 설정된 정보블럭들을 검사할 차례이다. 문제는 일일이 디버거의 Watch창이나 Memory창을 이용해 블록이나 스택이 깨졌는지를 확인해야 한다는 것이다. 이런 기법은 아마도 잘 알고 있을 것이다. 특정 메모리 주소를 가르키게 해놓고 의심되는 코드를 실행시켰을 때 Memory창의 내용이 빨간색으로 변하는 모양을 살펴서, 엉뚱한 부분까지 쓰거나, 원치않은 결과를 얻는지를 확인하는 것이다. 문제는 이것이 너무나도 수동적이기 때문에 CRT에서는 _CrtCheckMemory라는 도우미 함수를 곁들였다. 이 함수는 몇가지 경우에 있어서는 아주 쓸만하다. 사용법을 보자

int _tmain(int argc, _TCHAR* argv[]) 

{ 

       if(_CrtCheckMemory() == 0) 

             return -1; 

 

       return 0; 

} 

그냥 메모리 체크를 하기 원하는 위치에 _CrtCheckMemory함수를 삽입하기만 하면된다. 만약 0을 리턴한다면 할당된 메모리에 어떤 문제가 발생했다는 것을 의미한다. 그러나 위의 코드는 문제가 하나 있는데 바로 모든 CRT의 디버그 함수들은 DEBUG버전에서만 의미를 가지기 때문에 RELEASE버전에서는 아무 의미없는 코드가 된다는 것이다. 일단 보기에도 두 줄에 걸쳐 표기되어 있으므로 흉하다. 그러므로 다음과 같이 코딩하도록 한다.

_ASSERTE( _CrtCheckMemory( ) ); 

_ASSERTE는 CRT에서 제공해주는 매크로이다. 또는 assert.h의 assert함수를 이용해도 좋다. MFC등을 사용한다면 ASSERT등을 사용해도 좋고 Robbins의 SUPER_ASSERT를 사용해도 좋다. 각각 약간씩의 차이점이 있기는 결과는 거의 같다. 그러니 여기서는 CRT를 사용한다는 일관성을 유지하기 위해 _ASSERTE를 사용하도록 하겠다.

단순히 위와같이 의심갈때마다 호출해 주기만 한다면, CRT는 지금까지 등록된 모든 할당된 메모리들을 검사해 문제가 발생했는지를 확인한다. 그럼 어떤 종류의 에러들을 잡아주는지 다음의 예제들을 통해 알아보도록 하자

예제1. 경계를 넘어서서 쓰는 경우

int _tmain(int argc, _TCHAR* argv[]) 

{ 

       int *p = new int[10]; 

 

       p[-1] = 100; // 둘다 모두 오류이다 

       p[10] = 100; 

 

       _ASSERTE( _CrtCheckMemory( ) ); 

 

       return 0; 

} 

위의 예제에서는 정수 타입 10개짜리 배열을 할당하고 11번째 멤버에 쓰기를 시도하였다. 이부분에는 지난 강좌에서 알려 드렸듯이 0xfd값이 채워져 있는 영역이다. 주어진 메모리 체크 함수는 0xfd값이 있어야 할 자리에 다른 값이 있는 경우 0을 리턴한다.

예제2. 삭제된 포인터에 접근을 시도하는 경우

int _tmain(int argc, _TCHAR* argv[]) 

{ 

 

       // _CRTDBG_DELAY_FREE_MEM_DF 플랙을 반드시 켜야된다 

       _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_DELAY_FREE_MEM_DF); 

 

       int *p = new int[10]; 

       delete [] p; 

 

       p[0] = 10; 

       _ASSERTE( _CrtCheckMemory( ) ); 

 

       return 0; 

} 

위 예제에서는 이미 삭제된 포인터를 다시 접근하고 있다. 이것은 디폴트로 비활성화된 플랙을 사용하므로 위의 예제에서처럼 프로그램 시작전에 _CRTDBG_DELAY_FREE_MEM_DF 플랙을 켜줘야한다. 이 플랙이 켜지게 되면 CRT는 삭제 명령(free함수)이 호출되는 경우 바로 삭제처리하지 않는다. 대신 삭제 처리했다는 표시(0xdd값)만을 남겨둔다음 필요할 때 이값이 깨졌는지 검사한다. 디버그 버전이라면 이 플랙은 반드시 켜두도록 한다. 물론 메모리 부하가 약간 더 있겠지만 심각한 오류를 검출하는데는 꼭 필요한 플랙이다. 어차피 디버그 버전은 디버깅이 최고의 목표이니까..

이 두가지 기능만해도 일반적인 프로그래머들이 겪는 대부분의 메모리 문제는 해결된다. 아! 필자가 지금까지 봐온 대부분의 메모리 관련 문제들은 거의 60%이상이 초기화가 되지 않았거나 쓰레기값이 들어있는 포인터 접근 문제였다. 이문제는 위의 함수가 잡아주지 않는다. 그럼 쓰레기값이 들어있는 포인터 접근 문제는 어떻게 해결하겠는가? 아시는 분들은 다 아실것이다 0xC0000005 오류가 바로 정답이다. 또 대부분의 컴파일러의 경우 컴파일하는 도중에 이미 초기화 안된 변수를 사용했다고 여러분들에게 알려줄 것이다.

다음에는 구간과 구간 사이에서 메모리 문제를 발견하는 방법에 대해 다룰 것이다. 전체 프로그램에서 문제를 발견했다면 그 범위를 점차 좁혀나가는 것이 중요하다. 아쉽지만 필자 개인적인 사정으로 인해 조금 늦어질지도 모르겠다.

이 중에서 첫번째 플랙만을 제외하고는 모두 디폴트로 꺼져있다. 그러므로 다음과 같이 메모리 검사 기능을 켜도록 한다

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 

위의 프로그램에서는 다음을 삭제한다

_CrtMemDumpAllObjectsSince(0);  

_CrtMemDumpAllObjectsSince 함수는 실제로는 특정 지점에서 지점간에 할당되어 있는 메모리들을 보고해 주는 함수이다. 인자로 0을 넘겨주면 처음부터 지금까지 할당되어 있는 메모리들을 보고해 준다. CRT가 아직 전역으로 할당된 메모리를 완전히 삭제하기도 전에 호출했기 때문에 STL의 메모리가 샌 것 처럼 보인것이다.

CRT 메모리 블럭

다음으로 넘어가기 전에 다음을 짚어보고 넘어가기로 하자. 디버그 버전에서는 메모리가 할당되거나 사용되기 직전에 특정한 값들로 할당된 메모리가 채워진다는 것을 알고 계실것이다. 의미는 다음과 같다

0xFD : 메모리 블록 양 끝의 버퍼에 생성된다
0xCD : 새로 생성된 버퍼에 저장되는 기본값이다
0xCC : 스택에 변수가 생성되면 우선 이값으로 채워진다
0xDD : 삭제된 메모리 블록은 이 값으로 채워진다

다음 예제를 보자

int main() 

{    // 함수 시작지점 

 

       _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 

 

       int *p = new int [10]; // 메모리가 할당되는 지점 

       printf("%d", *p); 

       delete [] p;  // 메모리가 삭제되는 지점 

 

       return 0; 

} 

함수 시작지점에서 포인터 p는 초기값으로 0xCCCCCCCC를 갖게 된다. 디스어셈블 해보면 모든 지역변수들을 0xCC로 채워넣는 것을 볼 수 있다. 이후 메모리가 할당되는 지점에서 아마도 sizeof(int) * 10 바이트의 메모리가 할당될것이다. Sizeof(int)가 4바이트라면(32비트 운영체제에서) 40바이트가 생성되는 것이다.(디버그 버전이라는 가정하에) 이렇게 생성된 40바이트에 0xCD값이 채워진다. 만약 어떤 포인터의 값을 읽었을 때 값이 0xCDCDCDCD와 같은 형식이거든 초기화안된 메모리를 읽었다고 생각하면 된다.

그런데, 위의 할당지점에서 실은 48바이트가 생성된다. 40바이트 할당 명령을 주었는데 왜 48바이트의 메모리가 할당되었는가 이유는 위의 0xFD값에 있다. 40바이트의 블록을 생성시키고 그 블록의 앞과 뒤에 각각 0xFDFDFDFD를 삽입시켰기 때문이다. 이 값이 채워지는 이유는 다음과 같이 접근하는 경우

p[-1] = 10; // underwrite 

p[11] = 10; // overwrite 

0xFD로 채워진 메모리의 일부분이 깨질 것이고, 나중에 사용할 메모리 체크 명령에 의해 overwrite/underwrite가 발생했다는 사실을 알 수 있게된다.

마지막으로 메모리가 삭제되는 지점에서는 이론 대로라면 p값이 0xDD로 채워질 것이다. 그러나 실제로 필자의 컴퓨터에서는 0xFEEE가 채워졌다. 왜 그런지는 좀더 연구해 보고 알려드리도록 하겠다. -.- 오늘은 일단 여기까지 접고 연구결과가 더 나오는대로 여러분께 보고하는 시간을 갖도록 하려한다.

CRT Debug 기능 사용법 3

지난번에 이어 이번에는 메모리에 어떤 문제가 있었는지를 체크해주는 _CrtCheckMemory함수에 대해 연구해 보도록 하겠습니다.

강좌를 진행하기 전에 지난번 강좌 마지막 부분에서 메모리가 삭제될 때 0xDD값 대신에 0xFEEE값이 채워지는 이유를 찾아본 결과 CRT에서는 0xDD값을 정확히 채워 넣는다는 것을 확인하였다. _CRTDBG_DELAY_FREE_MEM_DF 플랙이 디폴트로 꺼져 있기 때문에 삭제했다는 표시를 하고 난 다음 바로 운영체제에서 삭제(진짜 삭제) 해버렸기 때문이다. 이부분은 뒤에서 다시 알아보도록 하겠다.

_CrtCheckMemory()

지난번에 알아보기를 CRT에서 메모리를 할당하려 할 때 몇가지 정보블럭들을 설정한다는 것을 알았을것이다. 자 이제는 설정된 정보블럭들을 검사할 차례이다. 문제는 일일이 디버거의 Watch창이나 Memory창을 이용해 블록이나 스택이 깨졌는지를 확인해야 한다는 것이다. 이런 기법은 아마도 잘 알고 있을 것이다. 특정 메모리 주소를 가르키게 해놓고 의심되는 코드를 실행시켰을 때 Memory창의 내용이 빨간색으로 변하는 모양을 살펴서, 엉뚱한 부분까지 쓰거나, 원치않은 결과를 얻는지를 확인하는 것이다. 문제는 이것이 너무나도 수동적이기 때문에 CRT에서는 _CrtCheckMemory라는 도우미 함수를 곁들였다. 이 함수는 몇가지 경우에 있어서는 아주 쓸만하다. 사용법을 보자

int _tmain(int argc, _TCHAR* argv[]) 

{ 

       if(_CrtCheckMemory() == 0) 

             return -1; 

 

       return 0; 

} 

그냥 메모리 체크를 하기 원하는 위치에 _CrtCheckMemory함수를 삽입하기만 하면된다. 만약 0을 리턴한다면 할당된 메모리에 어떤 문제가 발생했다는 것을 의미한다. 그러나 위의 코드는 문제가 하나 있는데 바로 모든 CRT의 디버그 함수들은 DEBUG버전에서만 의미를 가지기 때문에 RELEASE버전에서는 아무 의미없는 코드가 된다는 것이다. 일단 보기에도 두 줄에 걸쳐 표기되어 있으므로 흉하다. 그러므로 다음과 같이 코딩하도록 한다.

_ASSERTE( _CrtCheckMemory( ) ); 

_ASSERTE는 CRT에서 제공해주는 매크로이다. 또는 assert.h의 assert함수를 이용해도 좋다. MFC등을 사용한다면 ASSERT등을 사용해도 좋고 Robbins의 SUPER_ASSERT를 사용해도 좋다. 각각 약간씩의 차이점이 있기는 결과는 거의 같다. 그러니 여기서는 CRT를 사용한다는 일관성을 유지하기 위해 _ASSERTE를 사용하도록 하겠다.

단순히 위와같이 의심갈때마다 호출해 주기만 한다면, CRT는 지금까지 등록된 모든 할당된 메모리들을 검사해 문제가 발생했는지를 확인한다. 그럼 어떤 종류의 에러들을 잡아주는지 다음의 예제들을 통해 알아보도록 하자

예제1. 경계를 넘어서서 쓰는 경우

int _tmain(int argc, _TCHAR* argv[]) 

{ 

       int *p = new int[10]; 

 

       p[-1] = 100; // 둘다 모두 오류이다 

       p[10] = 100; 

 

       _ASSERTE( _CrtCheckMemory( ) ); 

 

       return 0; 

} 

위의 예제에서는 정수 타입 10개짜리 배열을 할당하고 11번째 멤버에 쓰기를 시도하였다. 이부분에는 지난 강좌에서 알려 드렸듯이 0xfd값이 채워져 있는 영역이다. 주어진 메모리 체크 함수는 0xfd값이 있어야 할 자리에 다른 값이 있는 경우 0을 리턴한다.

예제2. 삭제된 포인터에 접근을 시도하는 경우

int _tmain(int argc, _TCHAR* argv[]) 

{ 

 

       // _CRTDBG_DELAY_FREE_MEM_DF 플랙을 반드시 켜야된다 

       _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_DELAY_FREE_MEM_DF); 

 

       int *p = new int[10]; 

       delete [] p; 

 

       p[0] = 10; 

       _ASSERTE( _CrtCheckMemory( ) ); 

 

       return 0; 

} 

위 예제에서는 이미 삭제된 포인터를 다시 접근하고 있다. 이것은 디폴트로 비활성화된 플랙을 사용하므로 위의 예제에서처럼 프로그램 시작전에 _CRTDBG_DELAY_FREE_MEM_DF 플랙을 켜줘야한다. 이 플랙이 켜지게 되면 CRT는 삭제 명령(free함수)이 호출되는 경우 바로 삭제처리하지 않는다. 대신 삭제 처리했다는 표시(0xdd값)만을 남겨둔다음 필요할 때 이값이 깨졌는지 검사한다. 디버그 버전이라면 이 플랙은 반드시 켜두도록 한다. 물론 메모리 부하가 약간 더 있겠지만 심각한 오류를 검출하는데는 꼭 필요한 플랙이다. 어차피 디버그 버전은 디버깅이 최고의 목표이니까..

이 두가지 기능만해도 일반적인 프로그래머들이 겪는 대부분의 메모리 문제는 해결된다. 아! 필자가 지금까지 봐온 대부분의 메모리 관련 문제들은 거의 60%이상이 초기화가 되지 않았거나 쓰레기값이 들어있는 포인터 접근 문제였다. 이문제는 위의 함수가 잡아주지 않는다. 그럼 쓰레기값이 들어있는 포인터 접근 문제는 어떻게 해결하겠는가? 아시는 분들은 다 아실것이다 0xC0000005 오류가 바로 정답이다. 또 대부분의 컴파일러의 경우 컴파일하는 도중에 이미 초기화 안된 변수를 사용했다고 여러분들에게 알려줄 것이다.

다음에는 구간과 구간 사이에서 메모리 문제를 발견하는 방법에 대해 다룰 것이다. 전체 프로그램에서 문제를 발견했다면 그 범위를 점차 좁혀나가는 것이 중요하다. 아쉽지만 필자 개인적인 사정으로 인해 조금 늦어질지도 모르겠다.

이 중에서 첫번째 플랙만을 제외하고는 모두 디폴트로 꺼져있다. 그러므로 다음과 같이 메모리 검사 기능을 켜도록 한다

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 

위의 프로그램에서는 다음을 삭제한다

_CrtMemDumpAllObjectsSince(0);  

_CrtMemDumpAllObjectsSince 함수는 실제로는 특정 지점에서 지점간에 할당되어 있는 메모리들을 보고해 주는 함수이다. 인자로 0을 넘겨주면 처음부터 지금까지 할당되어 있는 메모리들을 보고해 준다. CRT가 아직 전역으로 할당된 메모리를 완전히 삭제하기도 전에 호출했기 때문에 STL의 메모리가 샌 것 처럼 보인것이다.

CRT 메모리 블럭

다음으로 넘어가기 전에 다음을 짚어보고 넘어가기로 하자. 디버그 버전에서는 메모리가 할당되거나 사용되기 직전에 특정한 값들로 할당된 메모리가 채워진다는 것을 알고 계실것이다. 의미는 다음과 같다

0xFD : 메모리 블록 양 끝의 버퍼에 생성된다
0xCD : 새로 생성된 버퍼에 저장되는 기본값이다
0xCC : 스택에 변수가 생성되면 우선 이값으로 채워진다
0xDD : 삭제된 메모리 블록은 이 값으로 채워진다

다음 예제를 보자

int main() 

{    // 함수 시작지점 

 

       _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 

 

       int *p = new int [10]; // 메모리가 할당되는 지점 

       printf("%d", *p); 

       delete [] p;  // 메모리가 삭제되는 지점 

 

       return 0; 

} 

함수 시작지점에서 포인터 p는 초기값으로 0xCCCCCCCC를 갖게 된다. 디스어셈블 해보면 모든 지역변수들을 0xCC로 채워넣는 것을 볼 수 있다. 이후 메모리가 할당되는 지점에서 아마도 sizeof(int) * 10 바이트의 메모리가 할당될것이다. Sizeof(int)가 4바이트라면(32비트 운영체제에서) 40바이트가 생성되는 것이다.(디버그 버전이라는 가정하에) 이렇게 생성된 40바이트에 0xCD값이 채워진다. 만약 어떤 포인터의 값을 읽었을 때 값이 0xCDCDCDCD와 같은 형식이거든 초기화안된 메모리를 읽었다고 생각하면 된다.

그런데, 위의 할당지점에서 실은 48바이트가 생성된다. 40바이트 할당 명령을 주었는데 왜 48바이트의 메모리가 할당되었는가 이유는 위의 0xFD값에 있다. 40바이트의 블록을 생성시키고 그 블록의 앞과 뒤에 각각 0xFDFDFDFD를 삽입시켰기 때문이다. 이 값이 채워지는 이유는 다음과 같이 접근하는 경우

p[-1] = 10; // underwrite 

p[11] = 10; // overwrite 

0xFD로 채워진 메모리의 일부분이 깨질 것이고, 나중에 사용할 메모리 체크 명령에 의해 overwrite/underwrite가 발생했다는 사실을 알 수 있게된다.

마지막으로 메모리가 삭제되는 지점에서는 이론 대로라면 p값이 0xDD로 채워질 것이다. 그러나 실제로 필자의 컴퓨터에서는 0xFEEE가 채워졌다. 왜 그런지는 좀더 연구해 보고 알려드리도록 하겠다. -.- 오늘은 일단 여기까지 접고 연구결과가 더 나오는대로 여러분께 보고하는 시간을 갖도록 하려한다.

CRT Debug 기능 사용법 3

지난번에 이어 이번에는 메모리에 어떤 문제가 있었는지를 체크해주는 _CrtCheckMemory함수에 대해 연구해 보도록 하겠습니다.

강좌를 진행하기 전에 지난번 강좌 마지막 부분에서 메모리가 삭제될 때 0xDD값 대신에 0xFEEE값이 채워지는 이유를 찾아본 결과 CRT에서는 0xDD값을 정확히 채워 넣는다는 것을 확인하였다. _CRTDBG_DELAY_FREE_MEM_DF 플랙이 디폴트로 꺼져 있기 때문에 삭제했다는 표시를 하고 난 다음 바로 운영체제에서 삭제(진짜 삭제) 해버렸기 때문이다. 이부분은 뒤에서 다시 알아보도록 하겠다.

_CrtCheckMemory()

지난번에 알아보기를 CRT에서 메모리를 할당하려 할 때 몇가지 정보블럭들을 설정한다는 것을 알았을것이다. 자 이제는 설정된 정보블럭들을 검사할 차례이다. 문제는 일일이 디버거의 Watch창이나 Memory창을 이용해 블록이나 스택이 깨졌는지를 확인해야 한다는 것이다. 이런 기법은 아마도 잘 알고 있을 것이다. 특정 메모리 주소를 가르키게 해놓고 의심되는 코드를 실행시켰을 때 Memory창의 내용이 빨간색으로 변하는 모양을 살펴서, 엉뚱한 부분까지 쓰거나, 원치않은 결과를 얻는지를 확인하는 것이다. 문제는 이것이 너무나도 수동적이기 때문에 CRT에서는 _CrtCheckMemory라는 도우미 함수를 곁들였다. 이 함수는 몇가지 경우에 있어서는 아주 쓸만하다. 사용법을 보자

int _tmain(int argc, _TCHAR* argv[]) 

{ 

       if(_CrtCheckMemory() == 0) 

             return -1; 

 

       return 0; 

} 

그냥 메모리 체크를 하기 원하는 위치에 _CrtCheckMemory함수를 삽입하기만 하면된다. 만약 0을 리턴한다면 할당된 메모리에 어떤 문제가 발생했다는 것을 의미한다. 그러나 위의 코드는 문제가 하나 있는데 바로 모든 CRT의 디버그 함수들은 DEBUG버전에서만 의미를 가지기 때문에 RELEASE버전에서는 아무 의미없는 코드가 된다는 것이다. 일단 보기에도 두 줄에 걸쳐 표기되어 있으므로 흉하다. 그러므로 다음과 같이 코딩하도록 한다.

_ASSERTE( _CrtCheckMemory( ) ); 

_ASSERTE는 CRT에서 제공해주는 매크로이다. 또는 assert.h의 assert함수를 이용해도 좋다. MFC등을 사용한다면 ASSERT등을 사용해도 좋고 Robbins의 SUPER_ASSERT를 사용해도 좋다. 각각 약간씩의 차이점이 있기는 결과는 거의 같다. 그러니 여기서는 CRT를 사용한다는 일관성을 유지하기 위해 _ASSERTE를 사용하도록 하겠다.

단순히 위와같이 의심갈때마다 호출해 주기만 한다면, CRT는 지금까지 등록된 모든 할당된 메모리들을 검사해 문제가 발생했는지를 확인한다. 그럼 어떤 종류의 에러들을 잡아주는지 다음의 예제들을 통해 알아보도록 하자

예제1. 경계를 넘어서서 쓰는 경우

int _tmain(int argc, _TCHAR* argv[]) 

{ 

       int *p = new int[10]; 

 

       p[-1] = 100; // 둘다 모두 오류이다 

       p[10] = 100; 

 

       _ASSERTE( _CrtCheckMemory( ) ); 

 

       return 0; 

} 

위의 예제에서는 정수 타입 10개짜리 배열을 할당하고 11번째 멤버에 쓰기를 시도하였다. 이부분에는 지난 강좌에서 알려 드렸듯이 0xfd값이 채워져 있는 영역이다. 주어진 메모리 체크 함수는 0xfd값이 있어야 할 자리에 다른 값이 있는 경우 0을 리턴한다.

예제2. 삭제된 포인터에 접근을 시도하는 경우

int _tmain(int argc, _TCHAR* argv[]) 

{ 

 

       // _CRTDBG_DELAY_FREE_MEM_DF 플랙을 반드시 켜야된다 

       _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_DELAY_FREE_MEM_DF); 

 

       int *p = new int[10]; 

       delete [] p; 

 

       p[0] = 10; 

       _ASSERTE( _CrtCheckMemory( ) ); 

 

       return 0; 

} 

위 예제에서는 이미 삭제된 포인터를 다시 접근하고 있다. 이것은 디폴트로 비활성화된 플랙을 사용하므로 위의 예제에서처럼 프로그램 시작전에 _CRTDBG_DELAY_FREE_MEM_DF 플랙을 켜줘야한다. 이 플랙이 켜지게 되면 CRT는 삭제 명령(free함수)이 호출되는 경우 바로 삭제처리하지 않는다. 대신 삭제 처리했다는 표시(0xdd값)만을 남겨둔다음 필요할 때 이값이 깨졌는지 검사한다. 디버그 버전이라면 이 플랙은 반드시 켜두도록 한다. 물론 메모리 부하가 약간 더 있겠지만 심각한 오류를 검출하는데는 꼭 필요한 플랙이다. 어차피 디버그 버전은 디버깅이 최고의 목표이니까..

이 두가지 기능만해도 일반적인 프로그래머들이 겪는 대부분의 메모리 문제는 해결된다. 아! 필자가 지금까지 봐온 대부분의 메모리 관련 문제들은 거의 60%이상이 초기화가 되지 않았거나 쓰레기값이 들어있는 포인터 접근 문제였다. 이문제는 위의 함수가 잡아주지 않는다. 그럼 쓰레기값이 들어있는 포인터 접근 문제는 어떻게 해결하겠는가? 아시는 분들은 다 아실것이다 0xC0000005 오류가 바로 정답이다. 또 대부분의 컴파일러의 경우 컴파일하는 도중에 이미 초기화 안된 변수를 사용했다고 여러분들에게 알려줄 것이다.

다음에는 구간과 구간 사이에서 메모리 문제를 발견하는 방법에 대해 다룰 것이다. 전체 프로그램에서 문제를 발견했다면 그 범위를 점차 좁혀나가는 것이 중요하다. 아쉽지만 필자 개인적인 사정으로 인해 조금 늦어질지도 모르겠다.

이 중에서 첫번째 플랙만을 제외하고는 모두 디폴트로 꺼져있다. 그러므로 다음과 같이 메모리 검사 기능을 켜도록 한다

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

위의 프로그램에서는 다음을 삭제한다

_CrtMemDumpAllObjectsSince(0);

_CrtMemDumpAllObjectsSince 함수는 실제로는 특정 지점에서 지점간에 할당되어 있는 메모리들을 보고해 주는 함수이다. 인자로 0을 넘겨주면 처음부터 지금까지 할당되어 있는 메모리들을 보고해 준다. CRT가 아직 전역으로 할당된 메모리를 완전히 삭제하기도 전에 호출했기 때문에 STL의 메모리가 샌 것 처럼 보인것이다.

CRT 메모리 블럭

다음으로 넘어가기 전에 다음을 짚어보고 넘어가기로 하자. 디버그 버전에서는 메모리가 할당되거나 사용되기 직전에 특정한 값들로 할당된 메모리가 채워진다는 것을 알고 계실것이다. 의미는 다음과 같다

0xFD : 메모리 블록 양 끝의 버퍼에 생성된다
0xCD : 새로 생성된 버퍼에 저장되는 기본값이다
0xCC : 스택에 변수가 생성되면 우선 이값으로 채워진다
0xDD : 삭제된 메모리 블록은 이 값으로 채워진다

다음 예제를 보자

int main()

{ // 함수 시작지점

 

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

 

int *p = new int [10]; // 메모리가 할당되는 지점

printf("%d", *p);

delete [] p; // 메모리가 삭제되는 지점

 

return 0;

}

함수 시작지점에서 포인터 p는 초기값으로 0xCCCCCCCC를 갖게 된다. 디스어셈블 해보면 모든 지역변수들을 0xCC로 채워넣는 것을 볼 수 있다. 이후 메모리가 할당되는 지점에서 아마도 sizeof(int) * 10 바이트의 메모리가 할당될것이다. Sizeof(int)가 4바이트라면(32비트 운영체제에서) 40바이트가 생성되는 것이다.(디버그 버전이라는 가정하에) 이렇게 생성된 40바이트에 0xCD값이 채워진다. 만약 어떤 포인터의 값을 읽었을 때 값이 0xCDCDCDCD와 같은 형식이거든 초기화안된 메모리를 읽었다고 생각하면 된다.

그런데, 위의 할당지점에서 실은 48바이트가 생성된다. 40바이트 할당 명령을 주었는데 왜 48바이트의 메모리가 할당되었는가 이유는 위의 0xFD값에 있다. 40바이트의 블록을 생성시키고 그 블록의 앞과 뒤에 각각 0xFDFDFDFD를 삽입시켰기 때문이다. 이 값이 채워지는 이유는 다음과 같이 접근하는 경우

p[-1] = 10; // underwrite

p[11] = 10; // overwrite

0xFD로 채워진 메모리의 일부분이 깨질 것이고, 나중에 사용할 메모리 체크 명령에 의해 overwrite/underwrite가 발생했다는 사실을 알 수 있게된다.

마지막으로 메모리가 삭제되는 지점에서는 이론 대로라면 p값이 0xDD로 채워질 것이다. 그러나 실제로 필자의 컴퓨터에서는 0xFEEE가 채워졌다. 왜 그런지는 좀더 연구해 보고 알려드리도록 하겠다. -.- 오늘은 일단 여기까지 접고 연구결과가 더 나오는대로 여러분께 보고하는 시간을 갖도록 하려한다.

CRT Debug 기능 사용법 3

지난번에 이어 이번에는 메모리에 어떤 문제가 있었는지를 체크해주는 _CrtCheckMemory함수에 대해 연구해 보도록 하겠습니다.

강좌를 진행하기 전에 지난번 강좌 마지막 부분에서 메모리가 삭제될 때 0xDD값 대신에 0xFEEE값이 채워지는 이유를 찾아본 결과 CRT에서는 0xDD값을 정확히 채워 넣는다는 것을 확인하였다. _CRTDBG_DELAY_FREE_MEM_DF 플랙이 디폴트로 꺼져 있기 때문에 삭제했다는 표시를 하고 난 다음 바로 운영체제에서 삭제(진짜 삭제) 해버렸기 때문이다. 이부분은 뒤에서 다시 알아보도록 하겠다.

_CrtCheckMemory()

지난번에 알아보기를 CRT에서 메모리를 할당하려 할 때 몇가지 정보블럭들을 설정한다는 것을 알았을것이다. 자 이제는 설정된 정보블럭들을 검사할 차례이다. 문제는 일일이 디버거의 Watch창이나 Memory창을 이용해 블록이나 스택이 깨졌는지를 확인해야 한다는 것이다. 이런 기법은 아마도 잘 알고 있을 것이다. 특정 메모리 주소를 가르키게 해놓고 의심되는 코드를 실행시켰을 때 Memory창의 내용이 빨간색으로 변하는 모양을 살펴서, 엉뚱한 부분까지 쓰거나, 원치않은 결과를 얻는지를 확인하는 것이다. 문제는 이것이 너무나도 수동적이기 때문에 CRT에서는 _CrtCheckMemory라는 도우미 함수를 곁들였다. 이 함수는 몇가지 경우에 있어서는 아주 쓸만하다. 사용법을 보자

int _tmain(int argc, _TCHAR* argv[])

{

if(_CrtCheckMemory() == 0)

return -1;

 

return 0;

}

그냥 메모리 체크를 하기 원하는 위치에 _CrtCheckMemory함수를 삽입하기만 하면된다. 만약 0을 리턴한다면 할당된 메모리에 어떤 문제가 발생했다는 것을 의미한다. 그러나 위의 코드는 문제가 하나 있는데 바로 모든 CRT의 디버그 함수들은 DEBUG버전에서만 의미를 가지기 때문에 RELEASE버전에서는 아무 의미없는 코드가 된다는 것이다. 일단 보기에도 두 줄에 걸쳐 표기되어 있으므로 흉하다. 그러므로 다음과 같이 코딩하도록 한다.

_ASSERTE( _CrtCheckMemory( ) );

_ASSERTE는 CRT에서 제공해주는 매크로이다. 또는 assert.h의 assert함수를 이용해도 좋다. MFC등을 사용한다면 ASSERT등을 사용해도 좋고 Robbins의 SUPER_ASSERT를 사용해도 좋다. 각각 약간씩의 차이점이 있기는 결과는 거의 같다. 그러니 여기서는 CRT를 사용한다는 일관성을 유지하기 위해 _ASSERTE를 사용하도록 하겠다.

단순히 위와같이 의심갈때마다 호출해 주기만 한다면, CRT는 지금까지 등록된 모든 할당된 메모리들을 검사해 문제가 발생했는지를 확인한다. 그럼 어떤 종류의 에러들을 잡아주는지 다음의 예제들을 통해 알아보도록 하자

예제1. 경계를 넘어서서 쓰는 경우

int _tmain(int argc, _TCHAR* argv[])

{

int *p = new int[10];

 

p[-1] = 100; // 둘다 모두 오류이다

p[10] = 100;

 

_ASSERTE( _CrtCheckMemory( ) );

 

return 0;

}

위의 예제에서는 정수 타입 10개짜리 배열을 할당하고 11번째 멤버에 쓰기를 시도하였다. 이부분에는 지난 강좌에서 알려 드렸듯이 0xfd값이 채워져 있는 영역이다. 주어진 메모리 체크 함수는 0xfd값이 있어야 할 자리에 다른 값이 있는 경우 0을 리턴한다.

예제2. 삭제된 포인터에 접근을 시도하는 경우

int _tmain(int argc, _TCHAR* argv[])

{

 

// _CRTDBG_DELAY_FREE_MEM_DF 플랙을 반드시 켜야된다

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_DELAY_FREE_MEM_DF);

 

int *p = new int[10];

delete [] p;

 

p[0] = 10;

_ASSERTE( _CrtCheckMemory( ) );

 

return 0;

}

위 예제에서는 이미 삭제된 포인터를 다시 접근하고 있다. 이것은 디폴트로 비활성화된 플랙을 사용하므로 위의 예제에서처럼 프로그램 시작전에 _CRTDBG_DELAY_FREE_MEM_DF 플랙을 켜줘야한다. 이 플랙이 켜지게 되면 CRT는 삭제 명령(free함수)이 호출되는 경우 바로 삭제처리하지 않는다. 대신 삭제 처리했다는 표시(0xdd값)만을 남겨둔다음 필요할 때 이값이 깨졌는지 검사한다. 디버그 버전이라면 이 플랙은 반드시 켜두도록 한다. 물론 메모리 부하가 약간 더 있겠지만 심각한 오류를 검출하는데는 꼭 필요한 플랙이다. 어차피 디버그 버전은 디버깅이 최고의 목표이니까..

이 두가지 기능만해도 일반적인 프로그래머들이 겪는 대부분의 메모리 문제는 해결된다. 아! 필자가 지금까지 봐온 대부분의 메모리 관련 문제들은 거의 60%이상이 초기화가 되지 않았거나 쓰레기값이 들어있는 포인터 접근 문제였다. 이문제는 위의 함수가 잡아주지 않는다. 그럼 쓰레기값이 들어있는 포인터 접근 문제는 어떻게 해결하겠는가? 아시는 분들은 다 아실것이다 0xC0000005 오류가 바로 정답이다. 또 대부분의 컴파일러의 경우 컴파일하는 도중에 이미 초기화 안된 변수를 사용했다고 여러분들에게 알려줄 것이다.

다음에는 구간과 구간 사이에서 메모리 문제를 발견하는 방법에 대해 다룰 것이다. 전체 프로그램에서 문제를 발견했다면 그 범위를 점차 좁혀나가는 것이 중요하다. 아쉽지만 필자 개인적인 사정으로 인해 조금 늦어질지도 모르겠다.