OS/Linux

[Linux] 메모리 누수 위치 찾기 (valgrind, 발그린드)

코끼리 개발자 2024. 8. 20. 15:19
728x90
SMALL

 

 

개발을 마무리 할 때, 메모리 누수가 없는 지 확인하는 작업은 필수적이다.

이 과정에서 정리한 메모리 증가 확인 방법에 대한 포스팅을 꽤나 많은 사람들이 참조하고 있다.

 

[Linux] 메모리 증가 확인 방법(pmap, ps, /proc/pid/status )

리눅스 환경에서 특정 프로세스의 메모리가 증가하는지 확인하는 방법 입니다.C언어를 이용해서 모듈을 만들고 프로세스를 모니터링 할 때 메모리 누수가 있는지 확인할 때 사용했던 방법입니

elephant-dev.tistory.com

 

 

이전에는 간단히 메모리 누수가 있는지 모니터링 해보았다고하면,

이번에는 누수가 발생하고 있는 소스코드 위치를 파악할 수 있는 동적 분석 툴 valgrind를 이용한

메모리 누수 위치 찾기 포스팅을 진행해 보려 한다.

 

 

우선, valgrind가 사용하고자 하는 linux에 존재하는지 확인한 후 없다면 설치를 진행한다.

(valgrind --version 커맨드 입력 시 버전 정보 출력되지 않는 다면 설치를 진행하자.)

$ yum install valgrind

 

 

이후 바로 사용하면 된다.

 

valgrind의 명령어 기본 형식은 다음과 같다.

valgrind [options] [program] [program arguments]

 

 

[options]

 

--leak-check=<yes|no|summary|full>

메모리 누수 검사를 활성화 하는 옵션
  • no : 메모리 누수 검사를 하지 않음
  • summary: 메모리 누수에 대한 요약만 출력
  • full: 누수된 메모리 블록에 대한 모든 세부 정보 출력
  • yes: summay와 동일

--log-file=<file>

valgrind가 생성하는 로그를 지정된 파일로 저장
  • --log-file=out.log 로 지정하면, out.log 파일에 로그를 저장함

-v 또는 --verbose

valgrind가 더 자세한 출력을 하며, valgrind가 수행하는 작업에 대한 세부 정보 제공

 

--error-limit=<yes|no>

발생하는 오류의 수에 대한 제한 설정
  • no로 설정하면 오류 수에 제한을 두지 않고, 모든 오류를 출력
  • 기본값은 yes이며, 오류 수가 많다면 일부만 출력하고 나머지는 생략할 수 있음

--track-origins=<yes|no>

초기화 되지 않은 메모리에 접근하는 오류 발생 시, 해당 메모리가 어디서 부터 유래했는지 추적
  • 기본값은 no이며, yes로 설정하면 메모리 사용 오류의 원인을 추적하는데 도움이 됨

--show-reachable=<yes|no>

프로그램 종료 시 접근 가능한 메모리에 대한 정보를 출력할지 여부를 결정
  • 기본값은 no이며, yes로 설정하면 아직 접근 가능한(해제되지 않은) 메모리 블록을 보여줌

--child-silent-after-fork=<yes|no>

프로그램이 fork()를 호출한 후 자식 프로세스의 valgrind 출력을 억제할지 여부 결정
  • 기본값은 yes

--tool=<toolname>

사용할 valgrind 도구 지정. memcheck를 기본 도구로 사용함.
  • 다른 도구로는 callgrind(프로파일링), cachegrind(캐시 사용 분석), massif(힙 메모리 사용 분석) 등이 있음

--num-callers=<number>

오류가 발생했을 때 보고할 호출 스택의 깊이 지정

 

--trace-children=<yes|no>

자식 프로세스도 함께 추적할지 여부 설정
  • 기본값은 no이며, yes로 설정하면 자식 프로세스에서도 메모리 오류를 추적

 

[Program]

 

분석 할 실행파일의 경로 입력

 

[Program arguments]

 

실행 파일에 전달할 인자들

 

 


 

우선 누수가 발생하도록 간단한 프로그램을 짜서 확인해보도록 해보자.

 

아래와 같이 malloc_free 함수에서는 free를 잘 해주고, malloc_leak 함수에선 할당한 메모리를 해제하지 않고,

지속적으로 1초마다 누수를 발생시키도록 test.c 코드를 짜보았다.

#include<stdio.h>
#include<stdlib.h>



void malloc_free()
{
        char *good = NULL;
        good = (char *)malloc(sizeof(char )*100000000);

        free(good);
}

void malloc_leak()
{
        char *bad = NULL;
        bad = (char *)malloc(sizeof(char )*100000000);

        //free(bad); // 메모리 해제 주석

}

void main()
{
        while(1)
        {
                malloc_free();
                malloc_leak();
                sleep(1);
        }
}

 

 

이제 빌드를 해주고, valgrind를 통해 누수를 찾아보자.

gcc -o test test.c

 

그 다음, --leak-check=full,  --log-file=mem.log -v(verbose를 통한 자세한 출력), --error-limit=no 옵션을 사용해서

누수된 메모리 블록에 대한 세부 정보를 확인하고, 해당 내용을 로그로 남겨보도록 해볼 예정이다.

 

valgrind --leak-check=full --log-file=mem.log -v --error-limit=no ./test

 

그 다음 ctrl+c 로 인터럽트를 걸어 실행을 멈춘 후 mem.log를 확인해보면 다음과 같다.


==121593== Memcheck, a memory error detector
==121593== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==121593== Using Valgrind-3.15.0-608cb11914-20190413 and LibVEX; rerun with -h for copyright info
==121593== Command: ./test
==121593== Parent PID: 112655
==121593== 
--121593-- 
--121593-- Valgrind options:
--121593--    --leak-check=full
--121593--    --log-file=mem.log
--121593--    -v
--121593--    --error-limit=no
--121593-- Contents of /proc/version:
--121593--   Linux version 3.10.0-1160.119.1.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) ) #1 SMP Tue Jun 4 14:43:51 UTC 2024
--121593-- 
--121593-- Arch and hwcaps: AMD64, LittleEndian, amd64-cx16-lzcnt-rdtscp-sse3-ssse3-avx-avx2-bmi-f16c-rdrand
--121593-- Page sizes: currently 4096, max supported 4096
--121593-- Valgrind library directory: /usr/libexec/valgrind
--121593-- Reading syms from /home/test/test
--121593-- Reading syms from /usr/lib64/ld-2.17.so
--121593-- Reading syms from /usr/libexec/valgrind/memcheck-amd64-linux
--121593--    object doesn't have a symbol table
--121593--    object doesn't have a dynamic symbol table
--121593-- Scheduler: using generic scheduler lock implementation.
--121593-- Reading suppressions file: /usr/libexec/valgrind/default.supp
==121593== embedded gdbserver: reading from /tmp/vgdb-pipe-from-vgdb-to-121593-by-test-on-localhost.localdomain
==121593== embedded gdbserver: writing to   /tmp/vgdb-pipe-to-vgdb-from-121593-by-test-on-localhost.localdomain
==121593== embedded gdbserver: shared mem   /tmp/vgdb-pipe-shared-mem-vgdb-121593-by-test-on-localhost.localdomain
==121593== 
==121593== TO CONTROL THIS PROCESS USING vgdb (which you probably
==121593== don't want to do, unless you know exactly what you're doing,
==121593== or are doing some strange experiment):
==121593==   /usr/libexec/valgrind/../../bin/vgdb --pid=121593 ...command...
==121593== 
==121593== TO DEBUG THIS PROCESS USING GDB: start GDB like this
==121593==   /path/to/gdb ./test
==121593== and then give GDB the following command
==121593==   target remote | /usr/libexec/valgrind/../../bin/vgdb --pid=121593
==121593== --pid is optional if only one valgrind process is running
==121593== 
--121593-- REDIR: 0x4019e40 (ld-linux-x86-64.so.2:strlen) redirected to 0x580c7ed5 (???)
--121593-- REDIR: 0x4019c10 (ld-linux-x86-64.so.2:index) redirected to 0x580c7eef (???)
--121593-- Reading syms from /usr/libexec/valgrind/vgpreload_core-amd64-linux.so
--121593-- Reading syms from /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so
==121593== WARNING: new redirection conflicts with existing -- ignoring it
--121593--     old: 0x04019e40 (strlen              ) R-> (0000.0) 0x580c7ed5 ???
--121593--     new: 0x04019e40 (strlen              ) R-> (2007.0) 0x04c2d1b0 strlen
--121593-- REDIR: 0x4019dc0 (ld-linux-x86-64.so.2:strcmp) redirected to 0x4c2e300 (strcmp)
--121593-- REDIR: 0x401aa80 (ld-linux-x86-64.so.2:mempcpy) redirected to 0x4c31f90 (mempcpy)
--121593-- Reading syms from /usr/lib64/libc-2.17.so
--121593-- REDIR: 0x4ec71d0 (libc.so.6:strcasecmp) redirected to 0x4a247a0 (_vgnU_ifunc_wrapper)
--121593-- REDIR: 0x4ec3f40 (libc.so.6:strnlen) redirected to 0x4a247a0 (_vgnU_ifunc_wrapper)
--121593-- REDIR: 0x4ec94d0 (libc.so.6:strncasecmp) redirected to 0x4a247a0 (_vgnU_ifunc_wrapper)
--121593-- REDIR: 0x4ec69a0 (libc.so.6:memset) redirected to 0x4a247a0 (_vgnU_ifunc_wrapper)
--121593-- REDIR: 0x4ec6950 (libc.so.6:memcpy@GLIBC_2.2.5) redirected to 0x4a247a0 (_vgnU_ifunc_wrapper)
--121593-- REDIR: 0x4ec5930 (libc.so.6:__GI_strrchr) redirected to 0x4c2cb70 (__GI_strrchr)
--121593-- REDIR: 0x4ebc740 (libc.so.6:malloc) redirected to 0x4c29eec (malloc)
--121593-- REDIR: 0x4ebcb60 (libc.so.6:free) redirected to 0x4c2afe6 (free)
==121593== 
==121593== Process terminating with default action of signal 2 (SIGINT)
==121593==    at 0x4EFC9E0: __nanosleep_nocancel (in /usr/lib64/libc-2.17.so)
==121593==    by 0x4EFC893: sleep (in /usr/lib64/libc-2.17.so)
==121593==    by 0x40062F: main (in /home/test/test)
==121593== 
==121593== HEAP SUMMARY:
==121593==     in use at exit: 900,000,000 bytes in 9 blocks
==121593==   total heap usage: 18 allocs, 9 frees, 1,800,000,000 bytes allocated
==121593== 
==121593== Searching for pointers to 9 not-freed blocks
==121593== Checked 70,064 bytes
==121593== 
==121593== 400,000,000 bytes in 4 blocks are definitely lost in loss record 1 of 2
==121593==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==121593==    by 0x400602: malloc_leak (in /home/test/test)
==121593==    by 0x400620: main (in /home/test/test)
==121593== 
==121593== 500,000,000 bytes in 5 blocks are possibly lost in loss record 2 of 2
==121593==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==121593==    by 0x400602: malloc_leak (in /home/test/test)
==121593==    by 0x400620: main (in /home/test/test)
==121593== 
==121593== LEAK SUMMARY:
==121593==    definitely lost: 400,000,000 bytes in 4 blocks
==121593==    indirectly lost: 0 bytes in 0 blocks
==121593==      possibly lost: 500,000,000 bytes in 5 blocks
==121593==    still reachable: 0 bytes in 0 blocks
==121593==         suppressed: 0 bytes in 0 blocks
==121593== 
==121593== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)


 

valgrind를 통해 작성된 mem.log 내용을 확인하는 중요한 섹션은 다음과 같다.

 

HEAP SUMMARY

프로그램 종료 시점에 사용된 메모리와 할당된 메모리의 요약이 제공됨

 

위에 로그를 확인해 보면 HEAP SUMMARY에서  in use at exit: 900,000,000 bytes in 9 blocks라고 출력되어 있다.

프로그램 종료 시점에 할당된 메모리 중 해제되지 않은 메모리를 나타내는 것이다.

 

따라서 내가 만든 test 모듈은 인터럽트를 걸어 종료했을 당시 약 900메가가 할당되지 못하고 여전히 사용중이었다는 의미이다

 

total heap usage 를확인해 보면 총 할당 및 해제된 메모리 블록 수와 그 크기를 확인할 수 있는데, 

총 18번의 할당과 9번의 해제가 있었으며, 총 1,800,000,000바이트가 할당됨을 알 수 있다.

따라서 while문이 18번 도는 동안 2개의 함수를 실행하는데, malloc_free함수에서는 free를 하지만 malloc_leak에서는 free를 하지 않으므로 정확히 절반만 해제되었다는 리포트를 받을 수 있다.

 

 

 

 

LEAK SUMMARY

프로그램 실행 후 메모리 누수 상태를 요약하여 제공

 

 definitely lost 는 메모리 누수가 확실한 경우를 나타낸다.

접근을 더이상 참조되지 않는 메모리를 나타내는데,  위 로그를 확인해보면 약 400MB가 확실히 누수되고 있음을 확인할 수 있다.

possibly lost 는 메모리 누수 가능성이 있는 경우를 나타낸다.

메모리 블록들은 접근할 수 없는 상태일 가능성이 있으나, 완전히 누수되었다고 확정할 수는 없는 범주를 말한다.

위 로그에서 는 약 500MB가 이 범주에 속한다.

예를 들면, 다음과 같다.

  • 손상된 포인터
    : 프로그램 내에서 포인터가 손상되었거나, 잘못된 위치를 가리키고 있는경우
  • 이중해제 이후의 접근
    : 메모리를 두 번 해제한 후 해당 메모리에 접근하려고 시도하면, 포인터는 더 이상 유효한 메모리를 가리키지는 않지만 여전히 참조하는 것으로 간주될 수 있다.
  • 오프셋된 포인터
    : 동적 메모리 할당 후, 포인터가 실제 할당된 메모리의 시작 위치에서 오프셋된 위치를 가리키는 경우.
    예를 들어 포인터를 이동한 후 해제하지 않고 프로그램을 종료하는 경우(인터럽트의 경우 가능성이 높음)를 말함
  • 중간에 사라진 참조
    : 프로그램이 동적으로 메모리를 할당한 후, 그 메모리를 가리키는 포인터를 중간에 잃어버린 경우.
    예를 들어, 포인터를 잘못된 위치로 설정하거나 포인터를 다른 메모리 주소로 덮어쓰는 경우 
  • 맞지 않는 메모리 관리
    : 메모리 할당과 해제가 다른 라이브러리나 모듈에서 이루어질 때, 메모리가 제대로 관리되지 않았다고 추정될 수 있음
    예를 들어, 커스텀 메모리 관리 함수나 서브 시스템이 있다면 그 내부에서 메모리가 어떻게 관리되고 있는지 valgrind는 명확히 파악할 수 없으므로 누수 여부를 알 수 없다.

indirectly lost 는 누수된 포인터가 다른 메모리를 가리키고 있던 경우를 말한다.

예를 들면 A구조체 안에 B구조체 포인터가 있는데, B를 메모리 할당하고, A를 해제하지 않고 지속적으로 새로 할당 해 누수가 발생되면 B는 이 항목의 누수로 집계된다.

 

still reachable 는 프로그램 종료 시점에 여전히 접근 가능한 ㅔ모리 블록을 나타낸다.

메모리가 해제되지 않았지만, 프로그램 종료 시점까지 여전히 참조 가능한 상태로 남아있는 메모리다.

해당 항목은 의도적으로 사용중인 메모리 일 수 있으므로, 의도한 접근이라면 누수로 판단하지는 않을 수 있다.

 

suppressed 는 무시된 메모리 누수 오류의 크기와 개수를 나타낸다.

이 항목은 개발자가 이전에 무시하도록 설정한 것이며 valgrind는 이를 보고하지 않는다.

 

 

 

 

 

이 로그를 통해 종합적으로 봤을 때 main에서 호출 된  malloc_leak 에서 메모리 누수가 발생하고 있음을 예측할 수 있다.

그럼 다시 free 주석을 풀어 누수를 막고 다시 확인해보자.

그리고 이번엔 로그에 담지않고, 바로 콘솔에 출력하도록 해보겠다.

 

valgrind --leak-check=summary -v --error-limit=no ./test

 


==8457== Process terminating with default action of signal 2 (SIGINT)
==8457==    at 0x4EFC9E0: __nanosleep_nocancel (in /usr/lib64/libc-2.17.so)
==8457==    by 0x4EFC893: sleep (in /usr/lib64/libc-2.17.so)
==8457==    by 0x40063B: main (in /home/test/test)
==8457== 
==8457== HEAP SUMMARY:
==8457==     in use at exit: 0 bytes in 0 blocks
==8457==   total heap usage: 4 allocs, 4 frees, 400,000,000 bytes allocated
==8457== 
==8457== All heap blocks were freed -- no leaks are possible
==8457== 
==8457== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0


 

누수가 없어지니 위와 같이 깔끔하게 뜨는 것을 확인할 수 있다.

728x90
LIST