본문 바로가기
컴퓨터/디버깅

valgrind 를 이용한 메모리 관리

by adnoctum 2011. 3. 3.


   C/C++ 의 골치거리 중 하나인 메모리 문제를 조금이나마 쉽게 해결하고자 나와 있는 프로그램 중 사용이 편리하고 open-source 인 valgrind 에 대해 알아 보자. valgrid는 leak 이 생긴/생길 가능성이 있는 코드의 부분을 찾아 주고, 잘못된 메모리 접근을 알려 주는 등의 기능을 한다. 이것은 특히 다른 사람이 작성한 소스 코드를 이용해야 할 때 유용한데, 코드의 전체적인 내용을 파악하지 못한 상태에서도 메모리 검사를 할 수 있기 때문이다. 나의 경우도 지금 남의 코드를 가져 와서 하고 있기 때문에, 일단 실행 시간 에러는 없이 실행되는 것을 확인했지만 정말로 문제없이 돌아가는 것인지 확인하기 위해 오랜만에 valgrind를 꺼내 들었다. 실제 사용 예는 다음과 같다.

[adnoctum@bioism src]$ g++ ~/LJSLibrary/library/util.o cluster.o cluster.cpp -o c -g
[adnoctum@bioism src]$ valgrind --leak-check=full --log-file-exactly=memcheck.txt -v --error-limit=no ./c test.txt

첫 번째 줄은 test 로 사용할 프로그램을 컴파일 하는 부분. -g 옵션을 줘야 함수 이름과 정확한 코드의 위치(줄 수)가 나와서 보기 쉽다. valgrind 의 option 은 적당히 준다. valgrind --help 에 잘 나와 있다. 사용법은 아주 간단하다.

valgrind [options] executible

을 따른다. option 은 적당히 주면 되고, executible 은 원래 프로그램을 실행시키는 것과 같다. 즉, 위의 경우 실행 파일이 c 이고 입력으로 test.txt 파일을 넣게 되어 있다. 그래서 이 프로그램을 실행시키려면 원래는

./c test.txt

를 하게 되는데, 바로 이 부분을 valgrind 의 입력으로 넣으면 되는 것이다. 그래서 위의 예에서 2 번째 줄처럼 된다.

-v 옵션을 주면 내용이 많기 때문에 좀 복잡할 수 있는데, 차근차근 따라가면 별로 어렵지 않다. 결과 파일이 매우 길기 때문에 부분부분 가져와서 왜 문제가 생겼는지를 소스 코드와 비교해 보자.

일단 가장 아래부분 근처에 보면 다음과 같은 요약이 있다.

==9581== IN SUMMARY: 9 errors from 9 contexts (suppressed: 15 from 1)
==9581==
==9581== malloc/free: in use at exit: 0 bytes in 0 blocks.
==9581== malloc/free: 4,997 allocs, 4,997 frees, 243,658 bytes allocated.
==9581==
==9581== All heap blocks were freed -- no leaks are possible.

일단 메모리 누수는 없는 것으로 보인다. 제일 앞에 쓰여 있는 숫자는 PID 이다. 만약 --PID-- 이면 정보를 보여 주는 것이고, 만약 == PID == 라면 에러가 난 부분을 보여 주는 것으므로 참고한다. 그럼 이젠 제일 앞부분부터 살펴 보자.

==9581== Invalid read of size 8
==9581==    at 0x805DC0F: main (cluster.cpp:149)
==9581==  Address 0x4096238 is 8 bytes after a block of size 224 alloc'd
==9581==    at 0x40053C0: malloc (vg_replace_malloc.c:149)
==9581==    by 0x805B067: treecluster (cluster.c:3656)
==9581==    by 0x805DB52: main (cluster.cpp:134)

내용인 즉, 8 바이트를 읽었는데, 원래는 읽으면 안되는 것이었다는 것이다. 직접적인 줄은 cluster.cpp 파일의 149 번째 줄이고, 밑에 call stack 이 나와 있다. 그래서, cluster.cpp 파일의 149 번째 줄 근처로 가보면,


134   Node* tree = treecluster(nRow, nCol, data, mask, weight, transpose, metric, method, NULL);
135
136   int nNodes = 0;
137   if(transpose != 0){
138           nNodes = nCol;
139   }
140   else{
141           nNodes = nRow;
142   }
143   double* order = (transpose==0) ? new double[nCol] : new double[nRow];
144   for(i = 0; i<nNodes; i++) order[i] = i;
145   if (metric=='e' || metric=='b')
146   /* Scale all distances such that they are between 0 and 1 */
147   { double scale = 0.0;
148     for (i = 0; i < nNodes; i++)
149       if (tree[i].distance > scale) scale = tree[i].distance;
150     if (scale) for (i = 0; i < nNodes; i++) tree[i].distance /= scale;
151   }

위 코드인데, 149번째 줄에서는 tree를 접근하고 있다. 위에서 8 바이트라 했으므로, 아마도 double 형 요소인 distance 를 말하는 것일테고, 위 코드와 잘 맞는다. 그럼 왜 읽지 말아야 하는 부분을 읽었다고 했을까? 조건(148라인)을 보면 for 문에서 i 를 nNodes 까지 돌리고 있다. 즉, nNodes-1번까지 돌테고, 일반적으로 잘 맞는듯이 보인다. 그런데, 134 번째 줄을 보니 tree 는 treecluster 라는 함수의 반환값이고, nNodes 는 137번째 줄에서 적당히 설정되고 있다. 이 부분은 좀 이 예 의존적이긴 한데, 어쨌든 어떻게 풀어 나가는지에 대한 흐름만 따라가 보자. 위 코드는 Cluster 3.0 의 소스 코드를 가져 온 것인데, 메뉴얼에 보면 hierarchial clustering 결과 반환되는 노드 수는 원래의 item 보다 한 개 적다고 한다. 즉, 지금 nCol을 기준으로 clustering 을 하면 반환되는 노드 수는 nCol - 1 이어야 하는 것이다. 따라서 tree의 index 는 nCol - 2 까지만 가야 하겠지. 따라서 위의 코드 중에는 138, 141 번에서 - 1 을 추가해 주어야 한다. 이런 에러는 중요하다. 왜냐 하면, 이렇게 읽지 말아야 하는 곳을 읽을 경우 에러가 났다 안났다 하기 때문에 잡기 좀 어렵기 때문이다. 매번 죽으면 상관이 없는데, 같은 release mode라 해도 어떤 때는 잘 되고 어떤 때는 죽는 경우는 디버깅 하기 참 어려운데, 이렇게 접근하면 안되는 곳을 접근하는 경우 자주 그렇게 된다. 언뜻 봐선 에러 없이 끝난 것 같아도 실은 언제 터질지 모르는 고장난 시한 폭탄을 갖고 있는 격. 결국, 데모할 때 터지겠지, >.<"" 어쨌든, 이 부분은 이렇게 해결하고, 다음으로 넘어 가면,

==9649== Mismatched free() / delete / delete []
==9649==    at 0x4004973: operator delete[](void*) (vg_replace_malloc.c:256)
==9649==    by 0x805E268: main (cluster.cpp:235)
==9649==  Address 0x4096E98 is 0 bytes inside a block of size 120 alloc'd
==9649==    at 0x40046FF: calloc (vg_replace_malloc.c:279)
==9649==    by 0x805D164: TreeSort(char, int, double const*, double const*, int const*, Node*, double**) (cluster.cpp:25)
==9649==    by 0x805DFF9: main (cluster.cpp:215)

malloc/free, new/delete, new []/delete [], 에 대한 대응이 잘못되었다는 얘기다. 실제로 235번째 줄은 delete [] neworder; 로 되어 있는데, 소스 코드를 보면 neworder 는 calloc 으로 할당이 된다. 따라서 이 부분에 맞게 free로 바꿔 주면 문제 해결. 원래의 코드가 ANSI  C 로 되어 있고 난 C++을 써버릇해서 이런 문제가 생겼다.

   초기화되지 않은 변수에 의존해서 jmp 나 move, 즉 if 문 안쪽에 초기화되지 않은 변수에 의해 비교, 할당이 일어날 수 있다는 경고도 있는데 그것은 생략한다.