gdb watch를 이용한 메모리 감시 기능 – 특정 주소나 변수의 변경을 Hardware 적으로 감시하기

제 블로그의 [프로그래밍 Tip]쪽에 보시면 아마도 스택침범(Stack Overflow)에 대한 강의가 하나 있을겁니다.
그 강의와 이번에 알려드릴 gdb 의 watch 기능을 함께 공부해보시면 밝혀내기 힘든 메모리 깨먹는 버그에 대해 한층 원인 분석이 쉬우실 겁니다.

"gdb 의 watch 기능은 특정 주소 영역의 값을 감시하여, 해당 주소 영역의 값이 변경이 되었을 경우 이를 알려주는 기능"입니다.
만약, 특정 변수가 있는데 이 변수의 값이 어느 순간부터 원하지 않는 이상한 쓰레기(garbage)값으로 할당되어 연속적인 문제를 일으킬 경우, 이 변수 주소 영역을 watch 를 이용하여 감시하면 변경의 원인이 되는 코드를 손쉽게 찾을 수 있습니다.

다음의 코드를 보시죠.
위에서 언급한 강의쪽에 나온 소스를 약간 수정한 버전이며 stack 변수를 깨먹는 버그를 안고 있는 코드입니다.

#include <stdio.h>

int count = 0;

int main(int argc, char **argv)
{
    char  buf[8];

    if( argc > 1 )
    {
        int    x = 1;

        printf("before [%d]n", x);

        char * s = argv[1];
        char * d = buf;

        while(*s != '')
        {
            ++count;
            *d++ = *s++;
        }

        printf("after [%d]n", x);
    }

    return 0;
}

이 코드를 컴파일한 후 수행하면 아래와 같이 변수 x 의 영역이 쓰레기 값으로 변해버립니다.
원인은 위에 언급한 강의쪽에 상세하게 기록되어 있으니 한번 읽어보시기 바랍니다.

sh> gcc x.c

sh> ./a.out 123456789012345678901234567890

before [1]
after [909456435]

그럼 이 변수의 값을 이렇게 바꾸어버리는 코드는 어디일까를 gdb 의 watch 를 통해 찾아보겠습니다.
gdb 의 watch 기능은 gdb 의 command 창에서 아래와 같이 수행하면 됩니다.

wa[tch] *(변수형 *)주소값
example> wa *(int *) 0x7fffffffdd3c

sh> gcc -g x.c

sh> gdb ./a.out

...

(gdb) b main                      # main 함수에 breakpoint
Breakpoint 1 at 0x4004d3: file x.c, line 9.

(gdb) r 012345678901234567890     # buf 크기를 넘는 string 을 인자로 주고 실행
Starting program: /home/bitmyer/tmp/a.out 012345678901234567890
Breakpoint 1, main (argc=2, argv=0x7fffffffde38) at x.c:9

9           if( argc > 1 )

Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.80.el6_3.6.x86_64

(gdb) n

11              int    x = 1;

(gdb)

13              printf("before [%d]n", x);

(gdb) display x                   # 변수 x 의 값을 매번 찍도록 함.

1: x = 1

(gdb) n

before [1]

15              char * s = argv[1];

1: x = 1

(gdb) n

16              char * d = buf;

1: x = 1

(gdb) n

18              while(*s != '')

1: x = 1

(gdb) display count                # 변수 count 의 값을 매번 찍도록 함.

2: count = 0

(gdb) p &x                         # 변수 x 의 주소를 알아냄

$1 = (int *) 0x7fffffffdd3c

(gdb) wa *(int *)0x7fffffffdd3c    # x 변수 주소에 watch point 를 걸어둠 (int 형 주소)

Hardware watchpoint 2: *(int *)0x7fffffffdd3c

(gdb) n

20                  ++count;

2: count = 0

1: x = 1

... 중략

(gdb) n

21                  *d++ = *s++;

2: count = 13

1: x = 1

(gdb) n

Hardware watchpoint 2: *(int *)0x7fffffffdd3c



Old value = 1                                  # 원래 값과 바뀐 값을 알려줌

New value = 50

0x000000000040052c in main (argc=2, argv=0x7fffffffde38) at x.c:21   # 변경을 일으킨 코드 위치

21                  *d++ = *s++;

2: count = 13

1: x = 50

(gdb) p *d

$2 = 50 '2'

(gdb) p *s

$3 = 50 '2'

x 라는 int 형 변수의 주소에 watch point 를 걸어두고 프로그램 실행을 계속하면, while loop 를 돌면서 인자로 들어온 문자 하나하나를 local 배열 buf 에 복사하다가 어느 순간 int 영역까지 넘어서서 값을 변경하게 되어 감시에 걸리게 됩니다.

이 때 debugger 는 어디에서 값 변경을 일으켰는지를 함수와 파일이름, 라인수로 알려줍니다.

참 편리하지 않나요 ?
이 메모리 value 감시기능인 watch 기능은 특히 멀티스레드(multi-thread) 프로그램에서 문제 발생 시 굉장히 유용하게 써먹을 수 있습니다.
멀티스레드 프로그램에서는 아시다시피 다수의 스레드가 같은 영역이나 변수를 공유하기도 합니다.
이 때 예기치 않게 이러한 문제를 발생시키는 경우가 굉장히 많은데 원인을 밝혀내려면 많은 시간이 필요할 때가 많습니다.

여러분은 이제 특정 변수의 값이 원하지 않는 값으로 갑자기 변경이 되었을 때, 이 값을 예전처럼 printf 로 열나게 찍어보려고 하지 마시고 watch 기능을 애용하시기 바랍니다. 디버깅 시간을 1/10 로 줄일 수 있습니다. ^^

You may also like...

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x