메모리 디버깅을 위한 친구 본문

Programming

메모리 디버깅을 위한 친구

halatha 2009. 9. 22. 11:24
출처: 열씨미와 게을러의 리눅스 개발 노하우 탐험기
2009/08/17 - [Programming] - diff, find, md5sum, patch
2009/08/17 - [Programming] - find, grep, ctags, cscope, global
2009/08/17 - [Programming] - shared library, strace, ldconfig, ldd, gcc, strings, od, nm, c++filt, readelf
2009/09/14 - [Programming] - configure
2009/09/21 - [Programming] - 자동화된 빌드 시스템 구축
2009/09/21 - [Programming] - 자동화된 빌드 시스템 구축 (2)
2009/09/21 - [Programming] - 숨겨진 1인치의 의존성을 찾아서 - make

rmalloc 개요
http://www.hexco.de/rmdebug

실행방법
  1. package(rmalloc.tgz) download
  2. rmalloc.c를 -c option과 함께 gcc로 compile
  3. debugging을 원하는 code에 #define MALLOC_DEBUG 추가, #include "rmalloc.h" 추가, link시 rmalloc.o link
  4. program 실행
동작원리
  • rmalloc.h에 일반적인 memory management function인 malloc, calloc, realloc, free, strdup function들을 감싸는 macro가 정의되어, rmalloc.h를 include한 code에서는 libc 대신 debugging function을 호출하게 됨. debugging function은 allocated buffer의 직전과 직후에 특별한 문자열로 표시를 해 돌려주고, 나중에 이 부분이 손상이 되면 문제가 된 file name과 line number를 출력하고 강제로 종료
  • gdb에서 사용하는 것도 가능

rmalloc이 잡아내는 문제점 5가지

rmalloc package의 Makefile을 이용해 make를 하면 rtest.c의 예제를 사용할 수 있음

1. 동적 메모리 1byte overflow(off-by-one) 오류
주로 동적 메모리를 할당한 후 문자열 크기 계산 과정에서 실수하는 경우 발생
흔히 '\0'으로 끝나는 것을 잊어버리고 문자열을 사용할 경우 발생
Test1에서 3byte('0', '1', '\0') str에 4byte('0', '1', '2', '\0') 문자열을 copy
static void Test1(void)
{
  char *str = strdup("01");
  strcpy(str, "012");       /* wrong! */
  free(str);
}

2. 동적 메모리 overflow 오류
할당한 메모리를 넘어서는 문자열을 복사하는 경우 발생
Test2에서 2byte str에 8byte의 문자열을 copy
static void Test2(void)
{
  char *str = strdup("0");
  strcpy(str, "012long");   /* wrong! */
  free(str);
}

3. 동적 메모리 포인터 색인 overflow(1byte 영역을 할당한 후 범위를 초과)
Test3에서 배열을 1개만 잡고 index를 1로 넘김
static void Test3(void)
{
  char **arr = malloc(1*sizeof(char *));
  char *bla = strdup("Kaputt!");

  arr[1] = bla;         /* wrong! */
  free(arr);            /* last chance to find */
  free(bla);
}

4. 동일한 메모리 두 번 해제
Test4에서 bla[0]을 두 번 해제
NULL을 해제할 경우를 찾아내고 싶다면 ALLOW_FREE_NULL macro로 제어 가능
static void Test4(void)
{
  unsigned int u;
  int i;
  char *bla[TEST_SIZE];
  unsigned int count = TEST_SIZE;
  char *foo[20];
  void *pending;

  memset(bla, 0, sizeof(bla));
  srand(time(NULL));

  while (count > 0) {
    u = (u + rand()) % TEST_SIZE;
    if (bla[u] == NULL) {
      bla[u] = malloc(u+1);
      assert(bla[u] != NULL);
      count--;
    }
  }

  for (i = TEST_SIZE-1;   i >= 0;   i--) {
    bla[i] = realloc(bla[i], 2*i+2);
  }

  pending = bla[0];

  count = TEST_SIZE;
  while (count > 0) {
    u = (u + rand()) % TEST_SIZE;
    if (bla[u] != NULL) {
      free(bla[u]);
      bla[u] = NULL;
      count--;
    }
  }

  free(pending);
}

5. 잘못된 메모리 번지 해제
Test5에서 0x12345678이라는 잘못된 값으로 free 시도
static void Test5(void)
{
  free((void *)0x12345678);
}

rmalloc test 결과

1. 동적 메모리 1byte overflow(off-by-one) 오류
buffer 손상을 감지하기 위해 rmalloc library가 달아놓은 앞쪽 a5a5a5a5가 00a5a5a5로 변한 것으로 감지
------------------
Running test  1...
------------------
<MALLOC_DEBUG>    Corrupted block end (possibly written past the end)
    should be: a5a5a5a5 5b5b5b5b abababab aa55aa55
    is:        00a5a5a5 5b5b5b5b abababab aa55aa55
    block was allocated in rtest.c:152 [3 Bytes, generation 1]
    error was detected in rtest.c:154
    Looks like string allocated one byte too short
        (forgetting the nul byte)

2. 동적 메모리 overflow 오류
사례 1과 유사한 방법
------------------
Running test  2...
------------------
<MALLOC_DEBUG>    Corrupted block end (possibly written past the end)
    should be: a5a5a5a5 5b5b5b5b abababab aa55aa55
    is:        326c6f6e 67005b5b abababab aa55aa55
    block was allocated in rtest.c:171 [2 Bytes, generation 1]
    error was detected in rtest.c:173
    Looks somewhat like a too long string,
        ending with "2long"

3. 동적 메모리 포인터 색인 overflow(1byte 영역을 할당한 후 범위를 초과)
사례 1, 2와 유사
------------------
Running test  3...
------------------
<MALLOC_DEBUG>    Corrupted block end (possibly written past the end)
    should be: a5a5a5a5 5b5b5b5b abababab aa55aa55
    is:        203c0608 5b5b5b5b abababab aa55aa55
    block was allocated in rtest.c:191 [4 Bytes, generation 1]
    error was detected in rtest.c:195
    First 4 bytes of overwritten memory can be interpreted
        as a pointer to a block  allocated in:
        rtest.c:192 [8 Bytes, generation 2]

4. 동일한 메모리 두 번 해제
double or false delete 감지
------------------
Running test  4...
------------------
<MALLOC_DEBUG>    Double or false delete
    Heap adress of block: 0x8073ad0
    Detected in rtest.c:250
    Trying identification (may be incorrect!):
        Allocated in rtest.c:235 [2 Bytes]

5. 잘못된 메모리 번지 해제
사례 4와 유사
------------------
Running test  5...
------------------
<MALLOC_DEBUG>    Double or false delete
    Heap adress of block: 0x12345678
    Detected in rtest.c:268


rmalloc이 제공하는 switch
macro 설정을 rmalloc.c에서 변경한 후 다시 build해 rmalloc.o와 대상 program을 다시 link
 switch name
설정값
설명
RM_TEST_DEPTH
0, 1, 2
0: 최소, 1: 통계 정보 제공/메모리 해제 시점에 검사, 2: malloc 계열 함수 호출 시점에 검사
GENERATIONS
ON/OFF
debugger에서 rmalloc_generation() 중단점을 걸어 어떤 function stack이 memory allocation을 하는지 추적하는 flag
ELOQUENT
ON/OFF
확장 메모리 할당 정보 출력 유무
WITH_FLAGS
ON/OFF
다음에 설명할 RM_SET 활성 유무
ALLOW_REALLOC_NULL
ON/OFF
realloc(NULL) 허용 유무
ALLOW_FREE_NULL
ON/OFF
free(NULL) 허용 유무
BREAK_GENERATION_COND
외부 환경
GENERATIONS를 활성화했을 경우 rmalloc_generation()
  변수 참조
호출을 수행하기 위한 조건 정의
MAX_STAT_GENERATIONS
숫자
GENERATIONS를 활성화했을 경우 통계에서 출력할 최대 generation 횟수

rmalloc에서 제공하는 추가 macro
이 macro를 적용하기 위해서는 원본 source code를 수정해야 한다

RM_TEST
모든 할당받은 블록에 대해 새로 점검을 수행
이 macro를 사용하기 위해서는 RM_TEST_DEPTH를 1이상으로 설정해야 한다
RM_TEST; // heap에 있는 모든 memory 요소를 점검

RM_STAT
모든 할당 받은 블록에 대해 새로 점검을 수행한 후 표준 오류로 할당받은 블록 통계를 출력
이 macro를 사용하기 위해서는 RM_TEST_DEPTH를 1이상으로 설정해야 한다
RM_STAT; // 할당 받은 메모리를 보여줌

RM_RETAG
종종 메모리 영역을 받아서 몇몇 기본값을 설정한 다음 사용자에게 돌려주는 함수를 사용하는 경우가 있다. 이런 특별한 함수를 사용할 경우 rmalloc이 메모리 추적에 어려움을 겪을 가능성이 있으므로, macro를 수행한 시점으로 위치를 초기화한다
struct complicated* cpointer = get_new_complicated_struct(any_arg);
RM_RETAG(cpointer); // file 위치를 여기에 설정
or
struct complicated* cpointer = RM_RETAG(get_new_complicated_struct(any_arg));

RM_SET
할당 받은 메모리 영역에 특별한 flag를 붙임
현재 RM_STATIC과 RM_STRING을 사용할 수 있다
RM_STATIC: 해제하지 않을 정적으로 고정된 메모리를 의미
static char* buffer = NULL;
static int length = 0;

...
if ( newlength > length ) {
    if ( buffer == NULL ) {
        buffer = malloc(length = newlength);
        RM_SET(buffer, RM_STATIC); // 결코 해제되지 않음을 명시
    }
    else {
        buffer = realloc(buffer, length = newlength);
    }
    if ( buffer == NULL ) {
        error(NOMEM);
    }
}
RM_STRING: 문자열을 담는 메모리를 의미. ELOQUENT mode에서만 통계에 잡히며, strdup는 자동으로 이 flag를 설정한다
struct numstr {
    int number;
    char* name;
};

...
struct numstr* foo = malloc(sizeof(struct numstr));
foo->name = calloc(1, 2);
foo->name[0] = 'X';
RM_SET(foo->name, RM_STRING);

결론
1. malloc, calloc, realloc, free와 같은 기본 memory management function 숙지
2. rmalloc은 만능이 아니며, 기본적인 memory management function을 사용할 때 발생하는 dynamic memory allocation 관련 문제만 찾을 수 있다. 즉 stack area에 잡히는 auto variable 관련 문제는 찾지 못한다
3. 아주 복잡하거나 timing이 중요한 program에서 rmalloc이 문제를 일으키는 경우도 있다
4. rmalloc에서 찾는 5가지 문제는 일상적으로 발생할 수 있는 것이므로 사전 예방을 위해 code를 검토하는 습관이 필요
5. {}, () matching이 틀려 발생할 수 있는 문제를 예방하기 위해 coding convention이 중요

참고
리눅스 디버깅과 성능 튜닝, 박재호/이해영 역, 에이콘 출판사 2006년: 4장
월간 임베디드 월드 2006년 7월호 '리눅스 개발자를 위한 디버깅 기법: 메모리 관리 디버깅 기법 소개', 박재호 기고
추가적인 메모리 할당 디버깅 라이브러리
MEMWATCH: http://www.linkdata.se/sourcecode.html
YAMD: http://www.cs.hmc.edu/~nate/yamd/
Electric Fence: http://perens.com/FreeSoftware/
Valgrind: http://valgrind.org/
Dmalloc: http://dmalloc.com/
Comments