마이크로소프트웨어지에 나온 내용

[Novice] 건강한 리눅스 프로그래밍을 위한 첫걸음, 디버깅

대화형 디버거 GDB 활용 ‘A에서 Z까지’

프로그램에서 가장 중요한 단계는 단연 설계일 것이다. 그렇다면 프로그램에서 가장 까다로운 단계는 무엇일까? 바로 버그를 잡아주는 디버깅이다. 리눅스 프로그래밍도 예외는 아니다. 작고 단순하면서도 어려운 ‘버그잡이’에 도전해 보자.

박상희 고려대 전자전파전기공학부 하나와영


일반적으로 프로그램의 제작은 ‘스펙(specification) 작성 → 코드 설계 → 코딩 → 테스트 → 디버깅 → 배포 → 유지 보수와 업데이트’ 과정을 거친다. 이 과정에서 가장 중요한 부분은 설계와 코딩 부분이 될 것이다. 하지만 프로그래머에게 가장 어려운 부분을 꼽아보라면 단연코 디버깅(debugging)이라고 말할 것이다. 따라서 리눅스 프로그래밍의 디버깅 방법을 이해하고 실제로 함께 해보자.

디버깅에서 ‘버그’ 정의

디버깅이란 말 그대로 버그(벌레)를 제거하는 작업이다. 그러면 버그란 무엇인가? 조금 장난스럽거나 이상하게 들릴지도 모르는 버그라는 말은 미국의 한 대형 컴퓨터에 벌레가 붙어 시스템을 정지시킨 이후로 쓰였다고 한다. 누가 한 마리의 작은 벌레가 감히 대형 컴퓨터 전체를 마비시키리라고 생각했을까? 프로그램에 있어서도 별 것 아닌 작은 버그가 큰 오류를 일으킨다. 다른 말로 설명하면 컴파일러가 컴파일하거나, 링커가 링크 작업을 하면서 출력하는 문법 에러(syntax error), 논리 에러(logical error), 런타임 에러(runtime error)와는 다르게 컴파일러, 링커가 찾아내지 못하는 에러를 버그라고 한다.

디버깅의 방법

그럼, 디버깅 방법을 살펴보자. 가장 원시적인 디버깅 방법은 Divide and Conquer(분할 정복이라고도 한다)라는 방법이다. 이 방법은 printf 문을 필요한 곳에 넣어주는 것이다. 다음은 Divide and Conquer의 예이다.

#ifdef_DEBUG
    printf(“##after scanf operator”);
#endif /* DEBUG */

일반적인 프로그램에서는 디버그를 정의하지 않고 컴파일하며, 디버깅하기 위해 프로그램을 컴파일할 때는 디버그를 정의하면 된다. 물론 단순히 디버그라는 것 하나만 정의하지 않고 여러 단계의 디버그 레벨을 정의해 최소의 디버그 정보만을 출력하도록 할 수 있다. 이와 같은 방법은 디버그 정보를 얻는 좋은 방법으로 지금까지 사용돼 왔지만 디버그 정보를 너무 많이 출력하기 때문에 원하는 정보를 놓쳐버릴 수 있다. 따라서 이 방법으로 디버깅한다면 리다이렉트(redir ect)하고 다음과 같이 디버그 정보가 저장된 파일을 검색해보는 것이 좋다.

jerry@kuee~$./a.out -D DEBUG > bug.txt

리눅스의 디버깅 방법으로 가장 많이 쓰이는 것은 대화형 디버거(interactive debugger)이다. 대화형 디버거란 특정 변수를 검사하기 위해 지정된 위치에서 프로그램을 중지하고 한 줄씩 실행할 수 있도록 해주는 프로그램으로, 터보 C++ 디버거, dbx, gdb 등이 있다. 여기서는 gdb를 사용해 디버깅해보도록 하겠다.

GDB의 명령어

이 글을 읽는 독자는 MySQL을 다뤄봤을 것이다. 혹시 MySQL을 다뤄본 적이 없다면 본지 과월호를 뒤져보면 곳곳에 숨어있으니 꼭 찾아보길 권한다. 디버거에서 MySQL을 말하는 이유는 MySQL의 명령어 입력 방법이 gdb에서 명령어를 입력하는 방법과 비슷해 참고가 되기 때문이다.

gdb의 명령어는 크게 실행(run)에 관련된 명령어와 트레이스(trace)관련 명령어로 나눌 수 있다.

<표 1>과 <표 2>에 소개한 명령어는 gdb에서 사용하는 명령어의 극히 일부이다. 게다가 이러한 구분은 필자가 임의로 나눠본 것이다. 하지만 여기에 소개한 명령어만으로도 버그를 잡는데 어려움이 없을 것이다. 참고로 gdb에서 나눠 놓은 명령어의 종류는 <표 3>과 같다.

물론 <표 3>의 분류에 따른 세부적인 명령어를 보고자 하면 ‘help <cate gory>’라고 입력해 자세한 설명을 출력할 수 있다.

간단하게 GDB 사용하기

gdb를 사용하는 목적은 버그를 잡기 위한 것이기 때문에 우선 버그가 있는 간단한 프로그램 <리스트 1>을 만들어 보자.

<리스트 1>의 bug1.c는 간단한 프로그램이다. 하지만 이렇게 간단한 소스에도 몇 개의 버그(또는 에러)가 있다. 우선 컴파일해 보자

[jerry@kuee1 tmp]$ gcc bug1.c -g

프로그램을 디버깅하기 위해서는 컴파일할 때 반드시 -g 옵션을 붙여 컴파일, 링크한다. 또한 -format-frame-pointer 스위치는 빼줘야 한다. 기본적으로 이 스위치는 빠져있기 때문에 별 문제가 없다. 가끔 공유 라이브러리(shared library)가 이 스위치를 빼지 않고 컴파일돼 있기 때문에 디버깅하지 못할 수 있지만 크게 신경 쓸 문제는 아니다. 컴파일이 잘 됐으면 실행시켜 보자.

[jerry@kuee1 tmp]$ ./a.out
j is 0.000000
j is 0.000000
j is 1.000000
j is 1.000000
j is 2.000000
Segmentation fault

두 가지의 버그가 보인다. 나누기한 결과가 의외로 나왔다는 것과 ‘Segmentation fault’가 나온 것이다. 앞서 언급했던 Divide and Conquer를 이용해 알 수 있다. 즉, Divide and Conquer는 코드의 중요한 부분에 printf 문을 넣어 버그가 나타난 곳을 찾아내는 방법이다. Segmentation fault는 for 문이 끝난 이후에 나타난 것을 알 수 있을 것이다. 그러면 우리의 목표인 gdb를 이용해 정확히 어디에 버그가 있는지 찾아보자.

[jerry@kuee1 tmp]$ gdb a.out
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License,
and you are welcome to change it and/or distribute copies of it under
certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty”
for details.
This GDB was configured as “i386-redhat-linux”...
(gdb)

이제 <표 1>, <표 2>에서 설명했던 gdb 명령어를 사용해 보자. 우선 break point를 잡아야 한다. 버그가 어디에 있는지 알 수 없기 때문에 일반적으로 처음에는 main 함수에 break point를 잡는다. 그리고 프로그램을 실행시키고 한 줄씩 진행시켜 보자.

(gdb) list
1 #include <stdio.h>
2
3 main()
4 {
5 int i;
6 double j;
7 char *bug = NULL;
8
9 for(i = 0 ; i < 5 ; i++){
10 j = i/2;
(gdb) list 1, 7
1 #include <stdio.h>
2
3 main()
4 {
5 int i;
6 double j;
7 char *bug = NULL;
(gdb) break main
Breakpoint 1 at 0x8048406: file bug1.c, line 7.
(gdb) run
Starting program: /home/jerry/tmp/a.out

Breakpoint 1, main () at bug1.c:7
7 char *bug = NULL;
(gdb) next
9 for(i = 0 ; i < 5 ; i++){
(gdb) n
10 j = i/2;
(gdb)
11 printf(“j is %lf”, j);
(gdb)
j is 0.000000
9 for(i = 0 ; i < 5 ; i++){
(gdb)
10 j = i/2;
(gdb)
11 printf(“j is %lf”, j);
(gdb)
j is 0.000000

우선 list 명령어로 프로그램의 소스를 볼 수 있다. list 명령은 10줄을 기본적으로 보여준다. 필요한 곳을 필요한 만큼 보려면 list n1, n2 형식을 사용하면 된다. 그 다음에 break point를 지정하고 프로그램을 실행시킨다. 그러면 main 함수에서 실행이 멈춘다. 이 때 프로그램을 진행시키기 위해서 next를 입력하고 엔터키를 누르면 된다. gdb의 명령어는 일일이 타이핑할 필요 없이 next라면 n을 누르고, break라면 b를 누르면 된다. 다른 명령어도 마찬가지로 명령어의 첫 글자만 입력하면 된다. 코딩에서 지친 손가락을 디버깅에서까지 혹사시킬 필요는 없을 것이다.

11번째 줄을 실행시킬 때는 단순히 엔터키만 입력했다. gdb에서 엔터키만을 입력하면 바로 전에 입력했던 명령어가 실행된다. 즉, 10번째 줄을 실행시킬 때 n을 지정했기 때문에 11번째 줄에서는 자동으로 n 명령어가 지정되는 것이다. 또한 MySQL에서와 같이 gdb에도 히스토리(history) 기능이 있다. 솔직히 화살표를 누르는 것보다는 명령어를 누르는 것이 빠르기 때문에 자주 쓰지는 않는다.

마지막 10번째 줄을 보면 i가 1일 때 당연히 0.5가 나와야함에도 불구하고 0이 나왔다. 이런 것은 죖C 언어를 며칠만 해봤어도 알 수 있는 것이 아니냐′고 하는 독자가 있을지도 모르지만, gdb에서 루프(loop) 문이 실행되는 것을 보여주기 위한 것이었으니 이해해주기 바란다. 답이 잘 나오게 하기 위한 방법은 물론 알 것이라 믿고 넘어가자. 혹시 C 언어 문법을 공부했음에도 불구하고 ‘잘 모르겠다’는 생각이 드는 독자는 필자가 했던 것처럼 처음에 공부하던 책을 다시 한번 읽어보기 바란다.

이제 11번째 줄을 실행시킬 때 next가 아닌 step을 사용해 보자. 어떻게 될까? 당연히 gdb는 printf라는 함수의 코드를 찾으려 할 것이다. 하지만 printf는 우리가 만든 함수가 아니므로 다음과 같은 에러 메시지가 나타난다.

11 printf(“j is %lf”, j);
(gdb) s
printf (format=0x80484e0 “j is %lf”) at printf.c:30
30 printf.c: 그런 파일이나 디렉토리가 없음.
(gdb)

혹시 볼랜드 C에서 디버깅을 해본 독자가 있다면, Trace Into(F7)과 Step Over(F8)의 차이가 next와 step의 차이라고 생각하면 된다.

다음으로 Segmentation fault가 나온 원인을 찾아보자. 일반적으로 Segmentation fault는 포인터의 문제라고 한다. 이는 세그먼트가 뭔지를 안다면 쉽게 생각할 수 있다(혹시 세그먼트가 궁금하다면 Operating Systems(Prentice hall)이라는 책의 347쪽을 참고하기 바란다). 물론 여기에서도 포인터 때문에 이 문제가 발생했다. 정확히 어디에서 문제가 발생했는지 찾아보자.

(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) b 13
Breakpoint 2 at 0x8048460: file bug1.c, line 13.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/jerry/tmp/a.out
j is 0.000000
j is 0.000000
j is 1.000000
j is 1.000000
j is 2.000000

Breakpoint 2, main () at bug1.c:14
14 strcpy(bug, “hi”);
(gdb) n

Program received signal SIGSEGV, Segmentation fault.
0x4007d964 in strcpy () from /lib/libc.so.6
(gdb)

우선 for 문의 문제는 고쳤다고 생각하고 처음에 잡았던 break point를 delete 명령어로 없애자. 그리고 for 문이 끝나는 13번째 줄에 다시 break point를 잡고, 프로그램을 다시 실행시키면 14번째 줄에서 SIGSEGV라는 시그널을 받아 에러 메시지를 띄우면서 프로그램이 중지될 것이다.

그러면 무엇이 잘못된 것인가? strcpy라는 함수는 한 스트링을 다른 스트링 포인터에 복사한다. 물론 두 인자는 포인터로 받게 된다. <리스트 1>을 보면 bug라는 포인터 변수를 잡아줄 때 NULL로 지정했다. 즉 ‘hi’라는 문자를 저장할 공간을 할당하지 않은 것이다 그렇기 때문에 커널이 SIGSEGV 시그널을 보낸 것이다.

(gdb) p bug
$1 = 0x0
(gdb)

위에서 볼 수 있는 것과 같이 bug라는 포인터에는 NULL이 지정돼 데이터를 저장할 수 있는 공간이 전혀 잡혀있지 않았다. 문제 해결 방법은 간단하다. ‘hi’라는 문자열을 저장할 수 있는 메모리를 할당해주면 된다. 그런 다음에 bug의 주소를 보자.

bug = (char *)malloc(sizeof(char)*3);
free(bug);

9 bug = (char *)malloc(sizeof(char)*3);
(gdb)
11 for(i = 0 ; i < 5 ; i++){
(gdb) p bug
$5 = 0x8049820 “”

보는 바와 같이 bug 변수에는 0x8049820라는 주소가 할당돼 있다. 따라서 다음과 같이 예상된 결과를 볼 수 있다.

[jerry@kuee1 tmp]$ ./a.out
j is 0.000000
j is 0.500000
j is 1.000000
j is 1.500000
j is 2.000000
bug is hi

지금까지는 gdb를 보고 “이게 뭐야?”라고 생각할지도 모른다. 하지만 앞에서 보여준 예는 gdb를 처음으로 접한 독자를 위한 것이다. 물론 앞으로 전개될 예제 역시 gdb를 처음 사용하는 독자를 위한 것이지만, gdb의 강력한 기능을 조금이나마 맛볼 수 있는 예제가 될 것이다.

약간의 생각이 필요한 예제

이번에 사용할 예제는 알고리즘 관련 서적의 실제 알고리즘 중에서 처음 부분에 나오는 바이너리 검색에서 보여줄 수 있는 디버깅의 예이다. 우선 바이너리 검색은 검색하려는 목록의 중앙에 있는 것과 검색하고자 하는 것을 비교해 목록의 위나 아래 부분을 검색하면서 검색 목록을 줄여나가는 방법이다. <리스트 2>의 bug2.c 파일에서 잘못된 부분을 찾아보자. 물론 컴파일하면 에러 메시지도 보이지 않는다. 디버거를 사용하기 전에 어디에 문제가 있는지 직접 찾아보기 바란다. 하지만 지금은 어디에 문제가 있겠거니 하고 막연히 문제점을 찾아야 할 것이다.

에러가 없을 것이라 예상하지만, 의심을 완전히 없애기 위해 컴파일해 보자. data.dat 파일은 다음과 같다.

[jerry@kuee1 tmp]$ gcc bug2.c -g
[jerry@kuee1 tmp]$ cat data.dat
4
6
14
16
17
19
22
23
26

이제는 실행시켜 보자. 당연히 에러가 없이 깨끗하게 컴파일됐기 때문에 예상한 결과가 나오리라고 생각할 것이다. 하지만 결과는 다음과 같다.

[jerry@kuee1 tmp]$ ./a.out
Segmentation fault

허탈하지 않나. 아무런 에러 없이 컴파일된 프로그램이 제대로 기능을 발휘하지 못하고 메모리가 이상하다면서 죽어버리다니! 이번에도 메모리(포인터)에 문제가 있는 것이 분명하다. 일단 gdb를 사용하지 않고 어디에 문제가 있는지 찾아 보라. 단, 이번에는 포인터를 사용한 어느 부분에 문제가 있을 것이라는 예상을 하고 찾아볼 수 있기 때문에 조금은 쉬울 것이다. 바로 문제점을 찾았다면 참 기분 좋은 일이다. 하지만 예제가 1000줄 이상 되는 프로그램이라고 생각해보자. 단순히 ‘포인터 중에 어딘가에 문제가 있구나’라는 생각만으로 문제점을 발견할 수 있을까? 이것이 바로 디버거의 필요성이다. 따라서 gdb를 이용해 효율적으로 문제점을 찾아보자.

[jerry@kuee1 tmp]$ gdb a.out
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License,
and you are welcome to change it and/or distribute copies of it under
certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty”
for details.
This GDB was configured as “i386-redhat-linux”...
(gdb) r
Starting program: /home/jerry/tmp/a.out

Program received signal SIGSEGV, Segmentation fault.
0x4006ea5e in _IO_vfscanf (s=0xbffff910, format=0x8048761 “%d”,
argptr=0xbffff9d8, errp=0x0) at vfscanf.c:963
963 vfscanf.c: 그런 파일이나 디렉토리가 없음.
(gdb) where
#0 0x4006ea5e in _IO_vfscanf (s=0xbffff910, format=0x8048761 “%d”,
argptr=0xbffff9d8, errp=0x0) at vfscanf.c:963
#1 0x4007274e in _IO_vsscanf (string=0xbffff9e0 “4”,
format=0x8048761 “%d”,
args=0xbffff9d8) at iovsscanf.c:44
#2 0x4007036d in sscanf (s=0xbffff9e0 “4”, format=0x8048761 “%d”)
at sscanf.c:38
#3 0x804857f in main () at bug2.c:25
(gdb)

왜 break point조차 잡지 않고 실행만 시켰을까? 그 이유는 where 명령어 때문이다. where 명령어는 어떤 함수가 어떤 함수를 호출했는지에 관한 정보를 보여준다. where의 출력문에는 최근에 불려진 함수부터 나타난다. 여기서는 main 함수 → sscanf 함수 → _IO_vsscanf 함수 → _IO_vfscanf 함수 순서로 불렀다. 하지만 마지막 두 개의 함수는 sscanf 함수가 호출하는 서브루틴이므로 우리가 생각할 필요는 없다. 따라서 프로그램은 sscanf 함수를 마지막으로 SIGSEGV 시그널을 받은 것이다. where의 출력문에 나타난 sscanf 함수를 보면 소스의 25번째 줄에서 sscanf가 사용됐다. 그러면 여기에서 소스의 24번째 줄이 어떻게 돼있는지 보자.

sscanf(line, “%d”, data[max_count]);

이 줄을 보고 ‘아하!’ 하는 독자도 있을 것이다. scanf 류의 함수는 저장할 곳의 포인터를 인자로 받는다. 하지만 여기에서는 포인터가 쓰이지 않았다. 따라서 3번째 인자 앞에 포인터 ‘&’만 붙여주면 되는 간단한 에러였다.

센스있는 독자라면 첫 번째 예제와 이번 예제에서 잘못된 곳을 찾는 방법이 다르다는 것을 인지했을 것이다. 첫 번째 예제에서는 약간의 막노동이였다면, 이번은 gdb를 효율적으로 이용한 방법이다.

소스를 수정하고 다시 컴파일한 후에 실행하면 잘 돌아갈까?

[jerry@kuee1 tmp]$ ./a.out
Enter number to search for or -1 to quit: 4
Found at index 0
Found at index 0
Not found
Enter number to search for or -1 to quit: -1
[jerry@kuee1 tmp]$

드디어 두 번째 오류가 발견됐다. 문제점은 분명히 ‘Enter number…’ 문장 다음에 있을 것이다. 그리고 0번째 줄에 찾으려는 숫자가 있다는 것을 출력했다. 따라서 ‘Found at index…’가 있는 라인에 break point를 잡으면 될 것이다. 물론 이보다 앞에 잡을 수도 있겠지만 while 문을 상당히 여러 번 지나치는 번거로움이 있다. 디버깅하다 보면 break point를 잘못 잡아 for 문이나 while 문을 빙빙 도는 경우가 종종 있다. 디버깅의 고통에서 빨리 벗어나고 싶다면 break point를 신중하게 선택하기 바란다.

[jerry@kuee1 tmp]$ gdb a.out
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License,
and you are welcome to change it and/or distribute copies of
it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty”
for details.
This GDB was configured as “i386-redhat-linux”...
(gdb) b 45
Breakpoint 1 at 0x8048655: file bug2.c, line 45.
(gdb) r
Starting program: /home/jerry/tmp/a.out
Enter number to search for or -1 to quit: 4

Breakpoint 1, main () at bug2.c:45
45 printf(“Found at index %d”, middle);
(gdb) n
Found at index 0

48 if(low == high){
(gdb)
53 if(data[middle] < search)
(gdb)
56 high = middle;
(gdb)
57 }
(gdb)
41 while(1){
(gdb)
42 middle = (low + high) /2;
(gdb)
44 if(data[middle] == search){
(gdb)
45 printf(“Found at index %d”, middle);
(gdb)
Found at index 0
48 if(low == high){

출력된 내용을 모두 보려면 루프가 100번쯤 돌아야 하는 상당한 시간이 소요되기 때문에 생략하겠다. 그냥 봐도 문제점을 쉽게 알아볼 수 있을 것이다. 파일에서 숫자를 찾았다면 루프가 끝나야함에도 불구하고 계속 다음 라인을 실행하고 있다. 문제점을 해결하기 위해 break 문을 살짝 추가함으로써 이번에 발견한 문제는 쉽게 해결될 것이다. 가끔 switch 문에서 break를 잊어 이상한 프로그램을 만들었던 기억이 한번쯤 있을 것이다.

다시 프로그램을 실행시켜 보자.

[jerry@kuee1 tmp]$ ./a.out
Enter number to search for or -1 to quit: 4
Found at index 0
Enter number to search for or -1 to quit: 5

2차원적인 지면에서 보여줄 수는 없지만 ‘5’라는 숫자(data.dat 파일에 없는 숫자)를 찾으려고 하면 프로그램이 끝나지 않는다. 어디에서인가 무한 루프가 돌고 있을 것이다. 이번 예제의 마지막인 만큼 열심히 찾아보자.

[jerry@kuee1 tmp]$ gdb a.out
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License,
and you are welcome to change it and/or distribute copies of it under
certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty”
for details.
This GDB was configured as “i386-redhat-linux”...
(gdb) r
Starting program: /home/jerry/tmp/a.out
Enter number to search for or -1 to quit: 5
Program received signal SIGINT, Interrupt.
0x8048698 in main () at bug2.c:54
54 if(data[middle] < search)
(gdb) s
55 low = middle;
(gdb)
58 }
(gdb) p middle
$1 = 0
(gdb) p high
$2 = 1
(gdb) s
41 while(1){
(gdb)
42 middle = (low + high) /2;
(gdb)
44 if(data[middle] == search){
(gdb)
49 if(low == high){
(gdb)
54 if(data[middle] < search)
(gdb) p low
$3 = 0
(gdb) p high
$4 = 1

우선은 gdb를 실행해 프로그램을 돌려 무한 루프가 되는 부분에서 Ctrl+C를 눌러 인터럽트시키자(SIGINT). 그 이후에는 step 명령어를 사용해 다음 단계로 진행할 수 있다. 우리가 인터럽트시키기 전에 루프는 충분히 돌았기 때문에 middle, high, low의 값은 충분히 조정됐을 것이다. 따라서 low와 high 값을 출력해보면 0과 1이 된다. 그리고 루프를 한번 더 돌린 다음에 low와 high 값을 출력하면 역시 0과 1이다. 물론 앞으로 100년이 지난다고 해도 high와 low는 같아질 수 없기 때문에 프로그램은 끝나지 않는다. 문제의 원인은 int 형을 int 형으로 나눴을 때 값이 int 형이 아니더라도 캐스팅시키기 때문이다. 이 문제는 <리스트 1>에서도 문제가 됐던 것이다.

문제의 해결 방법은 간단하다. 2로 나눴을 때 나머지 값이 없게 만들려면 홀수에 1을 더하거나 빼면 된다. 정확한 수정은 직접 해보기 바란다.

‘버그가 있다는 것은 증명할 수 있어도, 버그가 없다는 것은 증명할 수 없다’는 말이 있다. 프로그램에 엄청난 자신감이 있어 ‘난 프로그램에 버그 같은 것은 만들지 않는다’고 생각할 수도 있지만, 설사 그렇다고 하더라도 여러 사람이 공동으로 프로젝트를 진행할 경우를 생각해서라도 디버깅 기술은 익혀둬야 한다. 또한 필자는 디버깅하면서 포인터, 스택, 큐 등의 여러 언어에 대한 이해를 높힐 수 있었다. 앞으로 이 글을 읽은 독자가 ‘Segmentation fault’라는 메시지를 보고 난감해하는 일이 없기를 바란다.

필자 연락처 : jerry@kuee.korea.ac.kr
정리 : 위윤희 iwish@sbmedia.co.kr


‘Segmentation fault’의 세그먼테이션 이란?

프로그램을 중단시킬 수 있는 대부분의 버그들은 ‘Segmentation fault’라는 메시지를 남긴다. 이 메시지의 ‘fault’라는 말은 결함, 실수 등의 의미임을 쉽게 생각할 수 있다. 하지만 ‘segmentation’은 무슨 의미일까?

세그먼테이션은 운영체제에서 메모리 관리(Memory Management)의 한 종류이다. 세그먼테이션을 정의한다면 완전히 머신(machine) 독립적인 주소 공간을 할당해주는 직접적인 방법이라고 할 수 있다.

예를 들어, 컴파일러가 소스를 컴파일 할 때 다음과 같은 테이블을 만들 수 있다.

  1. 텍스트 소스를 저장하는 공간(테이블)
  2. 변수의 특성, 이름 등을 저장하는 심벌 공간
  3. 정수, 부동소수 등으로 지정된 상수를 저장하는 공간
  4. 프로그램을 문법적으로 분석한 Parse tree
  5. 컴파일러가 사용한 프로시저 콜 스택

이러한 여러 가지의 테이블을 세그먼테이션 방법에서는 각각 세그먼트라는 독립된 주소 공간을 할당하게 된다. 앞의 그림은 세그먼트를 나타낸 것이다. 반면에 가상 주소(Virtual Memory-pa ging의 대표적인 방법)라는 방법에서는 일정한 크기의 주소 공간을 여러 영역으로 나눠(세그먼테이션과는 다르게 일차원적이다) 앞의 여러 가지 테이블을 저장하게 된다.

세그먼테이션을 사용할 때의 장점에는 여러 가지가 있지만 우선 각 프로시저가 주소 번지 0에서 시작되는 분리된 세그먼트를 갖기 때문에 독립된 프로시저의 연결이 단순해진다. 또한 여러 개의 프로세스가 프로시저나 데이터를 쉽게 공유할 수 있다. 이러 장점은 세그먼테이션이 가상 메모리와는 달리 그 크기가 유동적이라는 데서 기인한다.



박상희
명규의 정리/추가(2002-06-01 3:09오전)

BREAKPOINT(help break)
break: breakpoint지정. 함수명 or 라인넘버
clear: 지정된 breakpoint제거

run: 프로그램 시작. run뒤에 추가로 argv와 같이 인자를 적어줄 수도 있다.

continue: 다음 breakpoint까지 계속 실행
          여러개의 breakpoint를 설정하고 각 breakpoint에서 디버깅시 next, step등을 이용하여
          디버깅하고, 각 breakpoint간의 이동을 continue를 사용하면 유용
          
next: 다음 라인 실행(subrutine calls의 처리과정 보여주지 않음)
step: next와 동일하나 subrutine calls을 만났을때 해당 subrutine으로 분기하여
      subrutine처리과정을 보여줌
      
print: 지정한 변수값 출력
display: 지정한 변수값을 지속적으로 출력
undisplay: display로 지정한 변수값을 더이상 출력하고 싶지 않을때

set args: set args argument1, argument1,... 처럼 실행인자를 지정하고, show args로 지정된 인자를 볼 수 있다.
(gdb) set args air  
(gdb) show args     
Argument list to give program being debugged when it is started is "air".