Thinking Different




일반적으로 가장 잡기 힘든 버그의 하나로서 메모리 누수, 메모리 Overwrite등을 꼽을 수 있다. 


이런 문제점을 해결하기 위해 CRT(C Runtime library)에서는 여러가지 다양한 메모리 관련 디버그 함수들을 제공한다. 


그러나 이것들이 디폴트로 사용하기 힘들게 꺼져 있기 때문에 대부분의 프로그래머들은 이 사실을 알지 못하는 경우가 많다. 


그래서 이 글에서는 CRT의 디버그 관련 함수들에 대해 알아보고 어떻게 사용하는 것이 좋은지에 대해 논해 보려고 한다. 




샘플 코드 1

1
2
3
4
5
6
7
8
#include <stdio.h>
 
int main(void)
{
    int *a = new int;
 
    return 0;
}


위와 같은 코드가 있다...  문제점이 뭔지 바로 알 수 있을 것이다.

위의 코드는 int *를 할당하고 해제하지 않았으므로 명백한 메모리 누수이다.  하지만 컴파일 해보면 아무런 에러 체킹이 되지 않고 이상없이 컴파일 된다.




아래와 같이 바꿔본다.



샘플코드 2

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <crtdbg.h>
 
int main(void)
{
    int *a = new int;
 
    _CrtDumpMemoryLeaks();  // 혹은 _CrtMemDumpAllObjectsSince(0);
    
    return 0;
}


여기서 사용된 _CrtMemDumpAllObjectsSince(0); or _CrtDumpMemoryLeaks(); 함수는 프로그램 종료 직전(main함수 종료 직전)에 호출하여 프로그램 구동시에 사용되었던 메모리가 할당을 하고 반환하지 않은것이 있는지 확인해주는 함수이다.


참고로 _CrtMemDumpAllObjectsSince(0) 함수는 내부적으로 _CrtDumpMemoryLeaks()를 호출한다.


자세한 내용은 MSDN 참조

http://msdn.microsoft.com/ko-kr/library/vstudio/hhy09244(v=vs.120).aspx

http://msdn.microsoft.com/ko-kr/library/vstudio/d41t22sb(v=vs.120).aspx




위와 같이 코드를 수정하고 F5 (디버그 모드) 컴파일을 진행한다.

출력창을 보면 아래와 같이 출력되는 것을 확인 할 수 있다.


출력창

Detected memory leaks!

Dumping objects ->

{143} normal block at 0x000000000034CD70, 4 bytes long.

 Data: <    > CD CD CD CD 

Object dump complete.

'[5868] ConsoleApplication1.exe' 프로그램이 종료되었습니다(코드: 0 (0x0)).


이 메모리는 143번째 할당된 일반 메모리이며 0x000000000034CD70번지에 4바이트 할당되었음을 알 수 있다. 또 데이터 내용은 16진수로 CD CD CD CD이다 이 정보만으로도 많은 것을 알 수 있다.


예를들어 데이터가 CD CD CD CD라는 것은 할당만 해놓고 전혀 초기화를 하지 않았다는 의미이다. 단순한 위의 프로그램 만으로도 사용자가 처음 할당한 메모리가 143번째만에 할당되었다. 이유가 무엇일까? 이유는 간단하다. main함수가 호출되기 이전에 이미 많은 메모리 할당 요청이 있었고, 그것은 프로그램을 실행시키기 위해 운영체제나, CRT가 이미 사용했기 때문이다.


CRT 메모리 블럭

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

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

0xDD : 삭제된 메모리 블록은 이 값으로 채워진다



그럼 이제는 메모리 누수가 생긴 부분을 알게 되었다. 이제 누수된 소스코드의 위치를 찾아볼 수 있는 함수가 있다.



사용법을 알아보자, 아래와 같이 코드를 수정한다.



샘플코드 3

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <crtdbg.h>
 
int main(void)
{
    _CrtSetBreakAlloc(143); // 위 출력값에서 나온 누수 메모리 번호
    int *a = new int;
 
    _CrtDumpMemoryLeaks();  // 혹은 _CrtMemDumpAllObjectsSince(0);
 
    return 0;
}


위와 같이 코드를 수정하고 다시 F5 (디버그 모드) 컴파일을 진행한다.


"xxx.exe이(가) 중단점을 트리거했습니다." 라는 컴파일러 alert을 만나게 된다.

중단을 하게 되면 메모리 누수가 일어난 지점 또는 그 근처 루틴에서 브레이크 시켜준다. 생각보다 어렵지 않다.



자 위에서 본 것 처럼 우리는 메모리 릭을 잡기 위해서 _CrtMemDumpAllObjectsSince(0); 나 _CrtDumpMemoryLeaks(); 함수를 main함수 종료 직전에 호출 하여 leak이 어디에 생겼는지 확인할 수 있었다.




자... 그런데 여기서 쉽게 끝나면 좋으련만, 문제점이 생긴다. 다음 코드를 보자.. 


샘플코드 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <crtdbg.h>
 
#include <list>
std::list<int> mList;
 
int main(void)
{
    //_CrtSetBreakAlloc(143); // 위 출력값에서 나온 누수 메모리 번호
 
    _CrtDumpMemoryLeaks();  // 혹은 _CrtMemDumpAllObjectsSince(0);
 
    return 0;
}


위와 같이 코드를 수정하고 다시 F5 (디버그 모드) 컴파일을 진행한다.



Detected memory leaks!

Dumping objects ->

{144} normal block at 0x00000000003BCDF0, 16 bytes long.

 Data: <  ;?            > 80 13 3B 3F 01 00 00 00 00 00 00 00 00 00 00 00 

{143} normal block at 0x00000000003BCD70, 24 bytes long.

 Data: <p ;     p ;     > 70 CD 3B 00 00 00 00 00 70 CD 3B 00 00 00 00 00 

Object dump complete.

'[8096] ConsoleApplication1.exe' 프로그램이 종료되었습니다(코드: 0 (0x0)).


전혀 문제가 되지 않는 코드다. 단순히 list를 사용하기 위해서 전역으로 할당만 한 것뿐이다. 그런데 메모리 누수가 나타난다.


MSDN의 내용이다.

간혹 _CrtDumpMemoryLeaks가 메모리 누수를 잘못 표시하는 경우도 있습니다. 이러한 오류는 내부 할당을 _CRT_BLOCK이나 _CLIENT_BLOCK 대신 _NORMAL_BLOCK으로 표시하는 라이브러리를 사용할 때 발생할 수 있습니다. 결과적으로 _CrtDumpMemoryLeaks가 사용자 할당과 내부 라이브러리 할당을 구별할 수 없게 됩니다. _CrtDumpMemoryLeaks를 호출한 이후에 라이브러리 할당을 위한 전역 소멸자가 실행되면 각 내부 라이브러리 할당이 메모리 누수로 보고됩니다. Visual Studio .NET 이전 버전의 표준 템플릿 라이브러리에서는 _CrtDumpMemoryLeaks가 이러한 가양성(false positives)을 보고했지만, 최신 릴리스에서는 이 문제가 해결되었습니다.


_CrtDumpMemoryLeaks()으로 디버깅 할 때 최종 발생할 수 있는 문제는 전역 변수에 대한 누수 부분이 문제가 된다. 전역 변수에 사용된 동적 메모리는 모두 누수로 보고를 하기 때문에 정확한 판단을 할 수 없는 경우가 있다는 것이다.

즉, 전역 변수는 main함수가 실행되기 전에 먼저 메모리에 올라오기 때문에 _CrtDumpMemoryLeaks() 함수가 구분을 못하는 것이다.


이를 해결하기 위해서 _CrtSetDbgFlag() 함수를 사용한다. _CrtDumpMemoryLeaks() 함수 내에서 _CrtSetDbgFlag()를 호출하게 되는 구조인데, 기본적으로 _CRTDBG_ALLOC_MEM_DF 플래그만 ON 되어있다. 사실 위에서 나타나는 전역 변수의 메모리 누수 부분도 이 플래그 설정에서 문제가 생기는 것이라고 볼 수 있다. 굳이 자세히 설명하지는 않겠다.

''혹시 제가 설명하는 부분이 틀리다면 댓글 오류 alert 부탁합니다.''


_CrtSetDbgFlag() 의 플래그 및 설정 부분은 MSDN 참조하기 바란다

http://msdn.microsoft.com/ko-kr/library/vstudio/5at7yxcs(v=vs.120).aspx



자 그럼 에러가 나지 않게 소스를 수정해봅시다


샘플코드 5

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <crtdbg.h>
 
#include <list>
std::list<int> mList;
 
int main(void)
{
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
 
    return 0;
}


위와 같이 코드를 수정하고 다시 F5 (디버그 모드) 컴파일을 진행한다.


깔끔하게 에러 없이 잘 처리 되는 것을 확인 할 수 있다.



 _CrtMemDumpAllObjectsSince(0) 또는 _CrtDumpMemoryLeaks() 보다는

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 를 사용하자



긴 내용 읽어 주셔서 감사합니다. 오류가 있는 부분은 정정 댓글 부탁합니다.