Valgrind 사용법


Valgrind 사용법
Valgrind 명령어는 C/C++ 프로그램에서 발생 할 수 있는 메모리 누수 등의 문제를 찾을 수 있는 명령어입니다. 패키지 Valgrind 설치, 분석 방법과 함께 예제 프로그램을 통해서 메모리 누수 등의 오류 방법에 대해서 설명드리도록 하겠습니다.

리눅스 Valgrind 설치

우분투 리눅스를 사용할 경우 아래의 명령어를 통해서 valgrind 패키지를 설치 할 수 있습니다.

$ sudo apt install valgrind

패키지 버전과 실제 최신 버전과 차이가 날 경우 아래의 페이지에서 직접 소스코드 등을 다운 받아서 빌드 후 설치 할 수도 있습니다.

http://www.valgrind.org/downloads/current.html

Valgrind 사용법

Valgrind 예제

이 글에서는 두 가지 예제를 통해서 valgrind 사용법과 결과 분석 방법에 대해서 설명드리도록 하겠습니다.

메모리 누수(Memory Leak) 탐지

아래의 예제 프로그램을 작성합니다. 파일명을 main-1.c 이라고 하겠습니다.

#include 

void f(void) {
   int* x = malloc(10 * sizeof(int));
}                    // problem 1: memory leak -- x not freed

int main(void) {
   f();

   return 0;
}

f 함수의 내용을 보시면 아시겠지만, 메모리를 할당하고 해제하는 코드는 없습니다.

이러한 함수를 계속하여 호출하게 되면 메모리 부족 문제가 발생하여, 더 이상 메모리를 할당 받을 수 없는 문제에 직면하게 될 뿐만 아니라 다른 프로세스에서도 물리 메모리 공간이 부족하게 되어 문제가 발생하게 됩니다.

이러한 오류는 컴파일러에서 찾아 낼수는 없는데 valrind 에서 어떻게 찾아낼 수 있는지 확인해 보도록 하겠습니다.

아래의 명령어로 소스코드를 컴파일 후 valgrind 명령어로 실행합니다.

$ make main-1
$ valgrind ./main-1

HEAP SUMMARY 부분을 보시면, 프로그램이 종료가 될 당시에 1개의 블럭 40 Byte 가 사용중하고 있다는 것이 나타납니다.

==11059== HEAP SUMMARY:
==11059==     in use at exit: 40 bytes in 1 blocks
==11059==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated

그리고 LEAK SUMMARY 부분을 보시면 definitely lost 부분에서 1개의 블럭 40 Byte가 확실하게 누수가 발생하고 있다는 사실을 나타냅니다.

==11059== 
==11059== LEAK SUMMARY:
==11059==    definitely lost: 40 bytes in 1 blocks
==11059==    indirectly lost: 0 bytes in 0 blocks
==11059==      possibly lost: 0 bytes in 0 blocks
==11059==    still reachable: 0 bytes in 0 blocks
==11059==         suppressed: 0 bytes in 0 blocks
==11059== Rerun with --leak-check=full to see details of leaked memory

f 함수 마지막 종료 전에 아래와 같은 메모리 해제 코드를 추가합니다.

void f(void) {
   int* x = malloc(10 * sizeof(int));
   free(x);
}

다시 소스코드를 빌드 후 실행하면, 아래와 같이 메모리 누수 정보가 사라진 것을 알 수 있습니다.

잘못된 메모리 주소 접근

다른 하나의 예제는 할당되지 않은 메모리 주소에 값을 작성하는 예제입니다.

#include 

void f(void) {
   int* x = malloc(10 * sizeof(int));
   x[10] = 0;        // problem 2: heap block overrun
   free(x);
}

int main(void) {
   f();

   return 0;
}

위의 소스코드를 main-2.c 파일명으로 저장 후, 아래의 명령어로 컴파일 한 다음 valgrind 명령어로 실행합니다.

$ make main-2
$ valgrind ./main-2

문제가 발생하는 내용은 아래와 같습니다.

==11295== Invalid write of size 4
==11295==    at 0x1086A8: f (in /home/ubuntu/main-2)
==11295==    by 0x1086C5: main (in /home/ubuntu/main-2)
==11295==  Address 0x522d068 is 0 bytes after a block of size 40 alloc'd
==11295==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==11295==    by 0x10869B: f (in /home/ubuntu/main-2)
==11295==    by 0x1086C5: main (in /home/ubuntu/main-2)

함수 f (at 0x1086A8: f) 에서 4Byte의 유효하지 않은 메모리 쓰기 오류가 발생하는 것을 알 수 있습니다.

오류 내용을 확인 후 함수 f 내용을 아래와 같이 수정합니다.

void f(void) {
   int* x = malloc(10 * sizeof(int));
   x[9] = 0;
   free(x);
}

오류를 수정 후 컴파일 합니다. 다시 valrind 명령어로 실행하면 오류가 사라진 것을 알 수 있습니다.

디버깅 정보 추가

valrind 으로 오류정보를 출력할 때에 문제가 발생하는 코드의 라인 번호를 추적하여 확인 할 수 있습니다.
gcc를 이용하여 컴파일 할 때 -g 옵션을 추가 후 컴파일 하게 되면, 디버깅 정보가 바이너리에 추가 됩니다.

이후 valgrind 명령어를 실행하게 되면 아래와 같이 오류가 발생하는 위치의 소스코드 라인 번호까지 출력이 가능합니다.

추가적인 내용으로, 소스코드 수정 내용등을 관리하기 위해서 diff 명령어와 patch 파일 생성하기 위해서는 아래의 글을 참고해 주시기 바랍니다.

diff patch 사용법

( 본문 인용시 출처를 밝혀 주시면 감사하겠습니다.)