gdb 사용하기

사실 거의 모든 중요한 프로그램에서 1000라인 코드 당 20에서 50 라인 정도는 문
제가 있는 경우가 많다. 사람들은 종종 실수를 하는 경향이 있고, 이로 인해 프
로그램이나 라이브러리가 애초에 요구된 데로 작동하지 않는 경우가 많다. 이러
한 문제성이 있는 소프트웨어는 버그를 포함하고 있다고 보통 말한다.

버그를 찾아서 없애고 디버깅하는 것은, 소프트웨어 개발시에, 프로그래머의 많은
노력과 시간을 소비한다. 이장에서 우리는 소프트웨어의 결점을 찾고, 비 정상적
인 행동을 유발하는실제의 예를 들어가면서, 알아두면 좋을 툴 몇가지와 테크닉을
소개한다. 테스트 하는 프로그램의 결과가, 여러분들과 같지 않을 수도 있다는 것은
알아두기 바란다.

1) 에러의 유형

버그는 보통 몇가지의 기본적인 원인에 의해 유발된다. 이러한 각각의 원인은
비그를 찾아내고 제거하는 방법을 제시한다.

가. 작성에러

프로그램을 부정확하게 작성하였다면, 당연히 원래 요구대로 동작하지 않을 것이
다. 세상에서 뛰어난 프로그래머는 틀린 프로그램은 간단하게 작성할 것이다. 실
제 프로그램을 디자인하거나 짜기전에 프로그램에 필요한 것이 무엇인지를 분명하
게 인식하는 것이 중요하다.

이렇게 에러는, 원래의 요구사항을 다시 점검하고 프로그램을 사용할 사람의 도움
을 얻어서 찾아내거나 제거할 수 있다.

나. 디자인 에러

어떠한 크기의 프로그램이던 디자인은 필요하다. 프로그램을 짜기 위해, 컴퓨터
앞에 앉아서, 바로 키보드를 두들기며 소스코드를 작성하고, 첫번에 프로그램이
기대한대로 작동하리라 기대하는 것은 욕심이다. 프로그램을 어떻게 만들것인
지, 어떤 데이터 구조가 필요하며, 어디에 사용될 것인가를 사전에 충분히 생각
해 보는 것이 좋다. 이러게 충분히 세부사항까지 생각해두는 자세는 이후에 프로
그램을 짤때 재작성에 필요한 노력을 줄여준다.

다. 코딩 에러

물론, 모든 사람은 타이핑 할때 실수를 한다. 디자인 한것으로부터 바로 작성된
소스코드는 아직까지 완전하게 처리된 것이 아니다. 여기에는 정말 많은 버그가
있을 것이다. 프로그램에서 버그와 마주쳤을 때, 소스코드를 간단히 다시 읽거나
다른 사람에게 물어봐서 해결 할 수도 있다.

프로그램에서 핵심적인 부분은 종이위에 그리면서 충분히 실행시켜보자. 이러한
것을 보통 dry running 이라고 한다. 중요한 루틴들은 단계적으로 입력값을 써내려
가고, 출력값을 계산해 보자.

항상 디버깅하는 데 컴퓨터를 사용할 필요성은 없다. 컴퓨터 자체가 문제를 야기
시킬 수도 있다. 라이브러리와 컴파일러, 운영체제를 만든 사람 조차도 실수를 할
수 있다.

2) 디버깅 준비

이장에서, 우리는 디버깅에 도움을 주는 툴들과 테크닉을 소개하고, 매우 자주 나
타나는 버그의 유형, 코딩 에러에 대해서 집중적으로 살펴볼 것이다.

전형적인 UNIX 프로그램을 디버깅하고 테스트 하는 방법에는 여러가지 다른 접근
방법이 있다. 우리는 보통 프로그램을 실행시키고 무슨 일이 일어나는 지를 관찰
한다. 만일 프로그램이 제대로 동작하지 않는다면, 제대로 동작하게 만들기 위해
어떠한 행동을 할 것이다. 프로그램을 변경하고, 다시 시도하고, 또다시 에러가
나타나는 과정을 반복하거나, 프로그램의 내부에서 무슨 일이 일어나는지에 대해
좀 더 많은 정보를 구하기 위해 노력하거나, 프로그램의 작동을 즉시에 분석할 수
도 있다. 이러한 디버깅의 단계를 아래와 같이 5가지로 분류할 수 있다.

1. 테스트 : 결점과 버그를 찾아낸다.
2. 안정화 : 버그가 반복적이 되도록 만든다.
3. 지역화 : 버그가 있는 줄이 몇번째 인지 확인한다.
4. 수 정 : 코드를 고친다.
5. 확 인 : 수정된 부분이 제대로 동작하는 지 확인한다.

가. 버그가 있는 프로그램

버그가 있는 예제 프로그램을 살펴보자. 이 프로그램은 이번장에서 계속 언급이 될
것이다. 이 프로그램은 큰 소프트웨어 시스템을 개발하는 과정에서 쓰여진 것이다.
여기에서는 sort 라는 하나의 함수를 테스트하고 있다. sort 는 item 형 구조체의
배열을 정렬하는 버블 소트 알고리즘을 수행하는 루틴이다.아이템은 멤버인 key
의 오름차순으로 정렬한다. 이 프로그램은 sort 를 테스트 하기위해 조금의 배열
을 사용한다.

불행히도, 이 코드는 매우 읽기도 힘들게 되어있고, 주석도 없으며, 원래의 개발
자도 없고, 어쨌거나, 우리는 이것을 가지고 발버둥을 쳐야 하며, 그 기본
루틴은 다음의 debug1.c 이다.

—————————————————————————
/* 1 */ typedef struct {
/* 2 */ char *data;
/* 3 */ int key;
/* 4 */ } item;
/* 5 */
/* 6 */ itemarray[] = {
/* 7 */ {“bill”, 3},
/* 8 */ {“neil”, 4},
/* 9 */ {“john”, 2},
/* 10 */ {“rick”, 5},
/* 11 */ {“alex”, 1},
/* 12 */ };
/* 13 */
/* 14 */ sort(a,n)
/* 15 */ item *a;
/* 16 */ {
/* 17 */ int i = 0, j = 0;
/* 18 */ int s = 1;
/* 19 */
/* 20 */ for(; i < n && s != 0; i++) {
/* 21 */ s = 0;
/* 22 */ for(j = 0; j < n; j++) {
/* 23 */ if(a[j].key > a[j+1].key) {
/* 24 */ item t = a[j];
/* 25 */ a[j] = a[j+1];
/* 26 */ a[j+1] = t;
/* 27 */ s++;
/* 28 */ }
/* 29 */ }
/* 30 */ n–;
/* 31 */ }
/* 32 */ }
/* 33 */
/* 34 */ main()
/* 35 */ {
/* 36 */ sort(array,5);
/* 37 */ }
————————————————————————-
이제 프로그램을 컴파일 해보자.

$ cc -o debug1 debug1.c

컴파일이 성공했고, 별다른 에러나 경고도 없다.

이 프로그램을 실행시키기 전에, 정렬의 결과를 출력하도록 조금의 코드를 추가
하자. 이런 코드가 없으면, 프로그램이 제대로 실행됐는 지, 아닌 지 알 방법
이 없다. 정렬하고 난 후에, 배열의 내용을 보여주도록 조금의 추가적인 라인을
추가한다. 이것을 우리는 debug2.c 라 하자.

————————————————————————-
/* 34 */ main()
/* 35 */ {
/* 36 */ int i;
/* 37 */ sort(array,5);
/* 38 */ for(i = 0; i < 5; i++)
/* 39 */ printf(“array[%d] = {%d, %s}\n”,
/* 40 */ i, array[i].key, array[i].data);
/* 41 */ }
————————————————————————-

이 추가적인 부분은 엄격히 말하면 원래의 프로그램의 일부는 아니다. 다만 테
스트를 위해서 추가된 부분일 뿐이다. 이제 조심스럽게 다시 컴파일을 해서,
프로그램을 실행시켜 보자.

$ cc -o debug2 debug2.c
$ debug2

프로그램을 실행시켰을 때 어떤 일이 일어나는가는 여러분들의 UNIX 나 Linux
의 특성과 어떻게 설정되어 있느냐에 따라 차이가 날 수 있다. 필자의 시스템에
서는 다음과 같았다.

array[0] = {4, neil}
array[1] = {2, john}
array[2] = {3, bill}
array[3] = (1, alex}
array[4] = {-1, (null))

그러나, 다른 Linux 커널을 탑재한 또다른 필자의 시스템에서는 다음과 같았다.

Segmentation fault

여러분의 UNIX 시스템에서는, 위의 둘중의 하나가 나오거나 또다른 결과를 얻을
것이다.

분명히, 이 프로그램 소스에는 여러가지 문제가 있다. 만일 이 코드를 전체의 일
부분으로 돌린다면, 배열을 올바로 정렬하지 못하고 세그먼테이션 폴트로 중단될
것이다. 이러한 일이 일어나는 이유는, 운영체제가 프로그램이 유효하지 않는 메
모리에 접근하는 것을 발견하고, 메모리가 엉켜지는 것을 방지하기 위해, 재빨리
프로그램에게 중단하라는 시그널을 보내기 때문이다.

유효하지 않는 메모리에 대한 접근을 감지하는 운영체제의 능력은, 하드웨어 구성이
나 몇몇 정교한 메모리 관리 운영에 달려있다. 대부분의 시스템에서는, 운영체제에
의해 프로그램에게 할당된 메모리는 실제로 사용되는 메모리보다는 크다. 만일, 유
효하지 않는 메모리 접근이 이러한 메모리 영역에서 일어난다면 하드웨어는 유효하
지 않는 접근을 알아차리지 못할 것이다. 이것이 바로 모든 UNIX 버젼에서 세그먼
테이션 폴트가 똑같이 일어나지 않는 이유이다.

문제를 일으키는 배열을 추적할 때, 가끔 배열의 원소의 크기를 늘리는 것은 좋은
생각이다. 이것은 마찬가지로 에러가 일어날 확률을 증가시키는 것이기도 하다.
만약, 바이트로 이루어진 배열의 끝부분을 넘어서 한바이트를 읽어들인다면, 프로그
램에 할당된 메모리는 운영체제가 명시한 경계선 (아마도 8K 만큼) 을 넘어설 것이
다.

배열의 원소인, item 멤버 data 를 4096 문자 크기로 늘릴 경우에는, 존재하지
않는 배열 원소에 대한 어떠한 접근도, 할당된 메모리 영역을 벗어나는 위치가 될
것이다. 배열의 각각의 원소는 4K의 크기이고, 따라서 배열의 영역을 벗어난 바로
다음의 원소에 접근한다면, 유효한 메모리의 끝에서 0-4K 영역이 될 것이다.

이렇게 변경했을 경우의 소스를 debug3.c 라고 하자. 필자들의 Linux모두에서,
똑같이 세그먼테이션 폴트가 발생했다.

————————————————————————–
/* 3 */ char data[4096];
————————————————————————–

$ cc -o debug3 debug3.c
$ debug3
Segmentation fault

몇몇 다른 Linux 나 UNIX 에서는 아직도 세그먼테이션 폴트가 나타나지 않을 수
있다. ANSI C 표준은 이때의 행동을 정의하지 않았다. 따라서, 이때의 프로그램
은 어떠한 행동을 하여야 한다는 보장은 없다. 하여튼 확실히 우리가 만든 C 프
로그램은 뭔가 이상이 있는 것이 틀림없고, 매우 이상한 행동을 야기시키고 있다.

나. 코드 점검

위에서 이야기 했지만, 프로그램이 제대로 수행되지 않을 경우, 코드를 찬찬히 한
번 다시 읽어보는 것은 좋은 습관이다. 이장에서는 다른 목적을 위해, 코드가 재
조사되었고, 큰 에러는 고쳐졌다고 가정한다.

* 코드 점검은 사실, 개발자들이 만든 프로그램의 적은 분량-수백줄 정도를 자세
하게 추적하는 형식적인 처리이다. 여기에서 프로그램의 규모는 별 상관없다.
그것은 아직 코드점검일뿐이고, 아직까지는 나름대로 유용한 테크닉이다.

코드를 유심히 관찰하는 데 도움이 될 만한 프로그램이 몇게 있다. 이러한 툴들은
프로그램의 형식적인 문법 에러에 대해 알려준다.

몇몇의 컴파일러는 경고를 출력하는 옵션을 가지고 있다. 가령, 변수의 초기화
에 실패하였거나, 조건절에 지시자를 사용했을 경우 등이 있다. 예를 들면, GNU
컴파일러는 다음과 같은 옵션으로 실행할 수 있다.

gcc -Wall -pedantic -ansi

이 옵션은 모든 경고를 켜두고, C 표준과 일치하는 지를 체크하는 것이다.
프로그램의 작동이 실패한다면, 이러한 옵션의 사용은 도움이 될만한 정보를
출력할 것이다.

잠시후에 lint 에 대해서 간단히 설명할 것이다. 컴파일러와 유사하게, 소스코드를
분석해서 문제가 될만한 코드를 보고하는 역할을 한다.

다. 방법

여기서는 실행시에 프로그램의 행동에 대한 정보를 수집하기 위한 목적으로 프로
그램에 코드를 추가하는 방법을 설명한다. 일반적으로 우리가 앞에서도 사용했지
만, 프로그램의 실행시에 각각의 단계에서 변수의 값을 출력하기 위해 printf 를
공통적으로 많이 사용한다. 여기서도 printf 를 여러번 사용할 수 있으나, 소스
를 재편집하거나 프로그램이 바뀔 때 마다 다시 컴파일 하는 경우를 피해보자. 물
론 버그가 고쳐졌을 때에는 해당 코드를 제거하는 것이 좋다.

이런 상황에 도움이 될만한 테크닉이 2개 있다. 첫번째는 C 전처리기를 사용해서
선택적으로 디버깅 코드를 포함하는 것이다. 이것은 디버깅 코드를 포함하거나 포
함하지 않기 위해서는 재컴파일만 하면 된다는 것을 의미한다. 다음과 같이 간단
하게 할 수 있다.

—————————————————————————
#ifdef DEBUG
printf(“variable x has value = %d\n”, x);
#endif
—————————————————————————

컴파일 플래그 -DDEBUG 를 사용하여 DEBUG 심볼을 정의하여 추가적인 코드를 포함
하거나 빼버리기 위해 정의하지 않을 수도 있다. 조금 더 복잡하지만, 정교하게 사
용할 수 있는 디버그 매크로를 정의할 수 있다.

—————————————————————————
#define BASIC_DEBUG 1
#define EXTRA_DEBUG 2
#define SUPER_DEBUG 4

#if (DEBUG & EXTRA_DEBUG)
printf…
#endif
—————————————————————————

이 경우에 우리는 항상 DEBUG 매크로를 정의하여야 한다. 그러나 디버그 정보를
세트로 사용하거나 세부적인 레벨로 사용할 수 있다는 장점이 있다. 컴파일러 플
래그 -DDEBUG=5 는, 위에서 BASIC_DEBUG 와 SUPER_DEBUG 를 켜고 EXTRA_DEBUG 는
끈다. -DDEBUG=0 으로 사용하면 모든 디버그 정보를 끄게 된다. 대체적으로, 아
래의 코드를 삽입함으로써, 디버깅이 필요없을 시에, 항상 명령행에서 DEBUG 를
명시해야 할 필요성을 제거한다.

—————————————————————————
#ifndef DEBUG
#define DEBUG 0
#endif
—————————————————————————

디버깅 정보와 함께 도움이 될만한, C 전처리기에 의해서 정의된 몇가지 매크로가
있다. 이 매크로들은 현재의 컴파일에 대한 정보를 제공하는 데 사용한다.

———————————————————————–
매크로 설 명
———————————————————————–
__LINE__ 현재의 라인 번호를 나타내는 10진 상수
__FILE__ 현재의 파일 이름을 나타내는 문자열
__DATE__ “Mmm dd yyyy” 형식의 현재 날짜를 나타내는 문자열
__TIME__ “hh:mm:ss” 형식의 시간을 타나내는 문자열
———————————————————————–

이러한 매크로들에는 두개의 밑줄이 앞뒤로 붙어 있다. 이러한 형태는 표준 전처
리기 심볼에서 공통적인 것이다. 따라서 충돌할 수 있는 심볼을선택하지 않도록
조심해야 한다. 위에서 “현재”는 전처리기가 수행될 때를 말한다. 마찬가지로 시간
과 날짜는 컴파일러가 실행되어서 파일을 처리할 때의 것이다.

:::::: 프로그램을 테스트 해보자 – 디버깅 정보 ::::::::

여기에 cinfo.c 라는 프로그램이 있다. 이것은 디버깅시에, 컴파일 날짜와 시간에
관한 정보를 출력한다.

—————————————————————————
#include <stdio.h>

int main()
{
#ifdef DEBUG
printf(“Compiled: ” __DATE__ ” at ” __TIME__ “\n”);
printf(“This is line %d of file %s\n”, __LINE__, __FILE__);
#endif
printf(“hello world\n”);
exit(0);
}
—————————————————————————

이 프로그램을 디버깅이 가능하게 하기 위해 -DDEBUG 를 사용하여 컴파일한다면,
다음과 유사한 컴파일 정보가 출력될 것이다.

$ cc -o cinfo -DDEBUG cinfo.c
$ cinfo
Compiled: Feb 4 1996 at 14:01:08
This is line 7 of file cinfo.c
hello world
$

* 어떻게 작동하는 가?

컴파일러의 전처리기 부분은 컴파일 될때, 현재의 라인과 파일에 대한 정보를 유지
한다. 컴파일러는 __LINE__ 이나 __FILE__ 심볼을 만나면 이러한 변수의 현재 (
컴파일 시간) 값으로 대체한다. 컴파일의 날짜와 시간도 이와 마차나지로 생성된
다.

__DATE__ 와 __TIME__ 은 문자열이기 때문에, printf 에서 형식화된 문자열로 연속
적으로 나열할 수 있다. ANSI C 는 인접한 문자열은 하나로 취급한다.

* 컴파일을 다시 하지 않고 디버깅 하기

다음으로 넘어가기 전에, #ifdef DEBUG 테크닉을 사용하지 않고, printf 함수를 사
용하여 디버깅 할 수 있는 방법을 잠시 언급한다. 이전의 #ifdef DEBUG 테크닉은
디버깅 하려면 다시 컴파일 하여야 한다는 단점이 있었다.

재 컴파일 하지 않고 디버깅을 다시 하는 방법은, 명령행에서 디버그 플래그 -d 옵
션을 사용하여 전역변수를 추가하는 것이다. 이러한 방법은 프로그램이 만들어지고
난 이후에도 사용자가 디버깅을 스위치 할 수 있도록 한다. 그리고 디버그 기록 함
수를 추가한다. 이제 다음과 같은 코드를 사용할 수 있다.

—————————————————————————
if (debug) {
sprintf(msg, …)
write_debug(msg)
}
—————————————————————————

디버그 출력은 stderr 로 해야 한다. 이것이 실용적이지 않다면 syslog 함수가 제
공하는 기록 기능을 사용할 수 있다.

프로그램 개발시에, 문제를 해결하기 위해서 이와 같이 추적 코드를 추가한다면,
그곳에 코드를 남겨두면 된다. 조금만 조심한다면 이것은 매우 안전하다. 이러한
방법은 프로그램이 발표되었을 때에도 이득이 있다. 만일 사용자가 문제점을 발
견한다면, 그것을 디버깅을 켜고 실행시켜 당신 대신 에러를 진단할 수도 있다.
프로그램은 segmentation fault 메시지를 출력하는 대신, 사용자가 무엇을 했는지
뿐만 아니라, 프로그램이 그 시간에 정확히 무엇을 했는 지 알아낼 수 있다. 이러
한 차이는 크다.

이러한 접근방법에는 분명히 한계가 있다. 프로그램이 필요한 것 보다는 커지게
된다. 대부분의 경우에, 이것은 명백한 문제가 될 것이다. 이러한 프로그램은 대
략 20-30% 정도 더 크질 것이나 퍼포먼스상에서 어떤 저하는 가져오지 않는다.
퍼포먼스의 저하는 보통, 단순한 크기의 증가가 아닌, 불어난 명령에 의한 크기
의 증가때문에 나타난다.

라. 제어된 실행

예제 프로그램으로 돌아가보자. 그 프로그램에는 아직 버그가 있다. 프로그램이
실행될 때 변수의 값을 출력하기위해 코드를 추가하거나, 디버거를 사용하여 프
로그램의 실행을 제어하거나 상태를 볼 수 있다.

UNIX 상에서 사용할 수 있는 디버거는 벤더의 종류에 따라 많이 있다.
공통적으로 많이 쓰이는 것은 adb, sdb, dbx 등이다. 좀 더 복잡한 디버거들은
소스코드 수준에서 좀 더 상세한 프로그램의 상태를 관찰할 수 있다. 이러한 디
버거에는 sdb, dbx, Linux 에서 사용되는 GNU 디버거인 gdb가 있다. gdb 에 좀
더 사용자에게 친숙한 모양을 입힌, xxgdb 와 tgdb 등과 같은 프로그램이 있다.
(역자의 말: 요즘은 gdb 에 GUI 를 입힌 ddd 가 인기가 좋더군요.)

프로그램을 디버깅 하기 위해서는 그 프로그램을 컴파일 할 때 하나 이상의 특정
한 컴파일러 옵션을 사용해야 한다. 이 옵션들은 컴파일러가 프로그램에 어떤
외부적인 디버깅 정보를 추가하도록 하는 것이다. 이 디버깅 정보에는 심볼과, 소스
코드 내에서의 위치를 알려주는 데 필요한 라인 번호가 포함되어 있다.

보통 디버깅 정보를 추가하는 옵션으로는 -g 가 사용된다. 디버깅 할 소스파일마다
디버깅 추가옵션을 사용하여 컴파일하고 링킹하여야 한다. 그래서 표준 C 라이브러
리의 어떤 버젼은 라이브러리 함수차원에서 디버깅 기능을 제공하는 수도 있다.
컴파일러는 이 추가옵션을 링커에서 자동적으로 전달한다. 디버깅은 또한 의도적으
로 컴파일되지 않은 라이브러리를 사용할 수도 있으나, 이는 유연성이 뒤떨어진다.

디버깅 정보를 포함하면 프로그램은 10배 정도까지 커질 수 있다. 디버깅 하고난
후에, 프로그램을발표하기 직전에 디버깅 정보를 제거하는 것은 좋은 방법이다.

3) gdb 로 디버깅 하기

이제, 프로그램을 디버깅하기 위해서 GNU 디버거인 gdb 를 사용할 것이다.
gdb 는 아주 강력하며, 자유롭게 구할 수도 있으며, 다양한 UNIX 플랫폼에서 돌아
간다. gdb 는 Linux 시스템에서는 기본 디버거이다. gdb 는 수많은 다른 플랫폼으
로 포팅이 되어 있어서, 내장 실시간 시스템을 디버깅하는 데도 사용할 수 있다.

가. gdb 를 실행하기

앞전의 예제 프로그램을 디버깅 정보를 포함하도록 재컴파일하고 gdb 를 불러보자.
(이후의 결과는 gdb 의 버젼과 시스템의 특성에 따라 다를 수 있다. )

$ cc -g -o debug3 debug3.c
$ gdb debug3
gdb is free software and you are welcome to 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.
gdb 4.14 (i486-unknown-linux),
Copyright 1995 Free Software Foundation, Inc…
(gdb)

gdb 는 방대한 온라인 도움말과 info 나 emacs내부에서 볼 수 있는 파일묶음으로
되어 있는 완벽한 매뉴얼이 있다.

(gdb) help
List of classes of commands:

running – Running the program
stack – Examining the stack
data – Examining data
breakpoints – Making program stop at certain points
files – Specifying and examining files
status – Status inquiries
support – Support facilities
user-defined – User-defined commands
aliases – Aliases of other commands
obscure – Obscure features
internals – Maintenance commands

Type “help” followed by a class name for a list of commands in that class.
Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.
(gdb)

나. 프로그램을 실행하기

run 명령어를 사용하여 프로그램을 실행시킨다. run 명령어에 함께 적는 모든 인
자들은 프로그램의 인자로 넘겨진다. 우리의 프로그램은 어떠한 인자도 필요없다.

이제 여러분들의 시스템이 필자들의 시스템과 같고, 세그먼테이션폴트를 일으킨다
고 가정한다. 그렇지 않다면, 그냥 읽기만 하기 바란다. 프로그램이 세그먼테이션
위반을 유발할 때, 무엇을 할 것인지 잘 찾아보길 바란다. 여러분들의 시스템에서
는 세그먼테이션 위반이 발생하지 않았으나 이 예제를 테스트 해보고 싶다면, 첫
번째 메모리 접근 문제가 고쳐졌을 때, debug4.c 를 선택하여 시도해 볼 수 있다.

(gdb) run
Staring program: /usr/neil/debug3

Program received signal SIGSEGV, Segmentation fault.
0x8000613 in sort (a=0x8001764, n=5) at debug3.c:25
25/* 25 */ a[j] = a[j+1];
(gdb)

프로그램은 여전히 옳지않게 수행된다. 프로그램이 문제를 발생하였을 경우에,
gdb 는 그 이유와 위치를 보여준다. 이제, 문제를 야기시킨 애매한 부분을 조사
해보자.

다. 스택 추적

프로그램은 debug3.c 의 25번째 라인, sort 함수에서 멈춰섰다. 프로그램을 디버
깅 정보를 포함하도록 컴파일 하지 않았다면, 프로그램이 어디서 문제가 있는지와
데이터를 알아보기 위해 변수명을 사용할 수 없다.

backtrace 명령어를 사용하면, 문제가 발생한 위치를 확인할 수 있다.

(gdb) backtrace
#0 0x8000613 in sort (a=0x8001764, n=5) at debug3.c:25
#1 0x8000692 in main () at debug3.c:37
#2 0x8000455 in ___crt_dummy__ ()
(gdb)

테스트 프로그램이 간단하고 다른 함수내에서 많은 함수를 호출하지 않았기 때문에
추적내용도 짧다. sort 는 main 의 37번째 라인에서 호출되었음을 알 수 있다.
보통, 문제는 좀 더 복잡하기 때문에, backtrace 는 에러가 발생한 위치의 뿌리를
찾고자 하는 데 사용된다. 이 명령어는 여러 다른 장소에서 호출된 함수를 디버깅
할 때 유용하다.

backtrace 는 bt 로 줄여 쓸 수 있고, 다른 디버거와의 호환성을 위해 where 명령어
를 사용할 수도 있다.

라. 변수값 보기

gdb 가 프로그램을 중단시켰을 때에 출력하는 정보와 스택 추적 정보는 함수의 인자
값을 보여준다.

sort 함수는 0x8001764 의 값을 가지는 매개변수 a 로 호출되었다. 이 값은 배열의
주소이며 여러 시스템의 사용된 컴파일러와 운영체제에 값이 다르다.

25번째 라인은 하나의 배열원소를 다른 배열로 값을 대입하는 부분이다.

/* 25 */ a[j] = a[j+1];

디버거를 사용하여 지역변수와 전역변수 등의 함수의 매개변수의 내용을 알아 볼 수
있다. print 명령어는 다양한 표현을 사용하여 변수의 내용을 보여준다.

(gdb) print j
$1 = 4

여기에서, 지역변수 j 의 값은 4라는 것을 알 수 있다. 이제 프로그램은 다음 구문
을 실행하려고 할 것이다.

a[4] = a[4+1];

sort 에 넘겨준 배열 array 는 5개의 원소를 가지고 있으며, 그 인덱스는 0에서 4
까지이다. 따라서 이 구문은 존재하지 않는 array[5] 를 읽을 것이다. 루프 변수
j 는 틀린 값을 가지게 되는 것이다.

print 에서는 배열의 원소의 값도 검사할 수 있다. gdb 에서는 print 명령에
거의 대부분의 정규 C 표현을 사용하여 변수, 배열의 원소, 포인터의 값을 알아볼
수 있다.

(gdb) print a[3]
$2 = {key = 1, data = “alex”, ‘\000’ <repaeats 4091 times>}
(gdb)

gdb 는 명령의 결과를 의사변수, $<number> 과 같은 형태로 가지고 있다. 마지막
결과는 항상 $ 이고, 하나 앞의 결과는 $$ 이다. 이러한 방식은 하나의 결과가
다른 명령어에 사용될 수 있도록 한다. 예를 들어보자.

(gdb) print j
$3 = 4
(gdb) print a[$-1].key
$4 = 1

마. 프로그램의 목록 보기

list 명령을 사용하면 소스코드의 목록을 볼 수 있다.
list 는 현재 위치 주위의 약간의 코드를 출력한다. 다음에 list 를 사용하면
그 다음의 코드를 출력할 것이다.

(gdb) list
20 /* 20 */ for(; i < n && s != 0; i++) {
21 /* 21 */ s = 0;
22 /* 22 */ for(j = 0; j < n; j++) {
23 /* 23 */ if(a[j].key > a[j+1].key) {
24 /* 24 */ item t = a[j];
25 /* 25 */ a[j] = a[j+1];
26 /* 26 */ a[j+1] = t;
27 /* 27 */ s++;
28 /* 28 */ }
29 /* 29 */ }
(gdb)

22 번째 줄에서 루프는 j 의 값이 n 의 값보다 작을 동안 실행되도록 정해져있다.
이경우에, n 이 5 이면, j는 최종적으로 4의 값을 가질것이다. 이제 a[4] 와 a[5]
의 값을 비교하게 되고, 가능하다면 값을 교환하려 할 것이다. 이 문제의 하나의 해
결책은 루프의 중단 조건을 j < n-1 가 되도록 설정하는 것이다.

이렇게 수정한 프로그램을 debug4.c 라고 하고, 컴파일을 다시 해보자.

—————————————————————————
/* 22 */ for(j = 0; j < n-1; j++) {
—————————————————————————

$ cc -g -o debug4 debug4.c
$ debug4
array[0] = {2, john}
array[1] = {1, alex}
array[2] = {3, bill}
array[3] = {4, neil}
array[4] = {5, rick}

아직도 정렬되지 않은 부분이 있다. 이제 gdb 를 사용하여 단계적으로 실행시켜보
자.

바. 중단점 (breakpoint) 설정

프로그램이 어디에서 문제가 있는 지를 알아보려면, 실행될 때 어떻게 작동하는 지
세부적으로 관찰해 볼 필요가 있다. 프로그램을 어떤 임의의 지점에서 멈추게 하
려면 중단점을 설정하면 된다. 이렇게 하면 프로그램은 중단점에서 멈추고 제어를
gdb 로 넘기게 된다. 이때 변수의 값을 조사해보거나 실행을 계속 시킬 수도 있
다.

sort 함수에는 두개의 루프가 있는 데, 루프 변수 i 가 있는 바깥쪽 루프는 배열의
각각의 원소 당 한번 실행되고, 안쪽 루프는 해당 값들이 오름차순이 되도록 교환
한다. 이 작업은 결국 값이 작은 원소를 앞쪽으로 끌어모으는 것이 된다. 바깥 루
프가 한번 실행되면 값이 제일 큰 원소는 가장 뒷쪽으로 밀려나가게 된다. 이것을
확인해 보기 위해, 외부루프에서 프로그램을 멈추고 배열의 상태를 알아볼 수 있다.

중단점을 설정하는 명령어는 여러개가 있다. gdb 명령행에서 help breakpoint 을
타이핑해보자.

(gdb) help breakpoint
Making program stop at certain points.

List of commands:

awatch — Set a watchpoint for an expression
rwatch — Set a read watchpoint for an expression
watch — Set a watchpoint for an expression
catch — Set breakpoints to catch exceptions that are raised
break — Set breakpoint at specified line or function
clear — Clear breakpoint at specified line or function
delete — Delete some breakpoints or auto-display expressions
disable — Disable some breakpoints
enable — Enable some breakpoints
thbreak — Set a temporary hardware assisted breakpoint
hbreak — Set a hardware assisted breakpoint
tbreak — Set a temporary breakpoint
condition — Specify breakpoint number N to break only if COND is
true
commands — Set commands to be executed when a breakpoint is hit
ignore — Set ignore-count of breakpoint number N to COUNT

Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.

이제, 줄 번호 20에 중단점을 설정하고 프로그램을 실행시켜 보자.

$ gdb debug4
(gdb) break 20
Breakpoint 1 at 0x80004ba: file debug4.c, line 20.
(gdb) run
Staring program: /usr/neil/debug4

Breakpoint 1, sort (a= 0x8001754, n=5) at debug4.c:20
20 /* 20 */ for (; i < n && s != 0; i++) {

여기에서, 배열의 값을 출력하고 cont 명령을 사용하여 프로그램을 재개
할 수 있다. 여기서는 다음 중단점인 줄 번호 20까지 다시 실행한다.
또한 중단점은 언제든지 여러개를 설정할 수 있다.

(gdb) print array[0]
$1 = {key = 3, data = “bill”, ‘\000’ <repeats 4091 times>}

몇개의 연속적인 아이템을 한꺼번에 보려면, @<number> 형태를 사용하면
된다. gdb 는 배열 원소의 갯수만큼 출력한다.

(gdb) print array[0]@5
$2 = {{key = 3, data = “bill”, ‘\000’ <repeats 4091 times>},
{key = 4, data = “neil”, ‘\000’ <repeats 4091 times>},
{key = 2, data = “john”, ‘\000’ <repeats 4091 times>},
{key = 5, data = “rick”, ‘\000’ <repeats 4091 times>},
{key = 1, data = “alex”, ‘\000’ <repeats 4091 times>}}

출력은 읽기 쉽고 비교적 깔끔하게 나온다. 루프의 처음이라서 아직 배열의 내용은
변하지 않았다. 프로그램의 실행을 계속하면 array 의 내용은 변하게 될 것이다.

(gdb) cont
Continuing.

Breakpoint 1, sort (a=0x8001754, n=4) at debug4.c:20
20 /* 20 */ for (; i < n && s != 0; i++) {

(gdb) print array[0]@5
$3 = {{key = 3, data = “bill”, ‘\000’ <repeats 4091 times>},
{key = 2, data = “john”, ‘\000’ <repeats 4091 times>},
{key = 4, data = “neil”, ‘\000’ <repeats 4091 times>},
{key = 1, data = “alex”, ‘\000’ <repeats 4091 times>},
{key = 5, data = “rick”, ‘\000’ <repeats 4091 times>}}
(gdb)

gdb 에서 display 명령을 사용하면 프로그램이 중단점에서 멈추면 자동
적으로 배열의 내용을 보여줄 수 있도록 할 수 있다.

(gdb) display array[0]@5
1: array[0] @ 5 = {{key = 3, data = “bill”, ‘\000’ <repeats 4091 times>},
{key = 2, data = “john”, ‘\000’ <repeats 4091 times>},
{key = 4, data = “neil”, ‘\000’ <repeats 4091 times>},
{key = 1, data = “alex”, ‘\000’ <repeats 4091 times>},
{key = 5, data = “rick”, ‘\000’ <repeats 4091 times>}}

더 나아가, 프로그램을 중지시키는 대신, 중단점을 변경하여, 간단히 우리가 바라는
데이터를 볼 수 있다. commands 명령을 사용하여 이러한 작업을 할 수 있다. 이렇게
하면 프로그램이 중단점에 도달했을 때, 어던 디버거 명령을 실행할 것인지를 명시
할 수 있다. 이미 display 를 명시했으므로, 이제 cont 로 계속 수행하도록 만들
어 보자.

(gdb) commands
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just “end”.
> cont
> end

이제, 단지 프로그램을 계속 수행하도록 한다면, 매번의 바깥 루프에서 배열의 값을
출력하고, 완전하게 실행할 것이다.

(gdb) cont
Continuing.

Breakpoint 1, sort (a=0x8001754, n=3) at debug4.c:20
20 /* 20 */ for (; i < n && s != 0; i++) {
1: array[0] @ 5 = {{key = 2, data = “john”, ‘\000’ <repeats 4091 times>},
{key = 3, data = “bill”, ‘\000’ <repeats 4091 times>},
{key = 1, data = “alex”, ‘\000’ <repeats 4091 times>},
{key = 4, data = “neil”, ‘\000’ <repeats 4091 times>},
{key = 5, data = “rick”, ‘\000’ <repeats 4091 times>}}

Breakpoint 1, sort (a=0x8001754, n=2) at debug4.c:20
20 /* 20 */ for (; i < n && s != 0; i++) {
1: array[0] @ 5 = {{key = 2, data = “john”, ‘\000’ <repeats 4091 times>},
{key = 1, data = “alex”, ‘\000’ <repeats 4091 times>},
{key = 3, data = “bill”, ‘\000’ <repeats 4091 times>},
{key = 4, data = “neil”, ‘\000’ <repeats 4091 times>},
{key = 5, data = “rick”, ‘\000’ <repeats 4091 times>}}
array[0] = {2, john}
array[1] = {1, alex}
array[2] = {3, bill}
array[3] = {4, neil}
array[4] = {5, rick}

Program exited with code 025.
(gdb)

마지막으로 gdb 는 프로그램이 비정상적인 종료코드로 종료되었음을 알려준다.
이러한 비정상적인 종료코드는 프로그램 자신이 exit 를 호출한 것이 아니고 main
에서의 리턴값도 아니다. 이 경우의 종료코드는 별 다른 뜻이 있는 것은 아니며,
종료코드가 의미있는 것이 되려면, 정상적인 exit 호출에 의한 것이어야 한다.

프로그램에서 바깥루프가 우리가 원하는 만큼 수행되지 않았다. 루프에서 중단
조건으로 사용된 매개변수 n 의 값은 매번의 중단점마다 하나씩 감소되었음을 알
수 있다. 이것이 의미하는 바는 루프가 우리가 생각하는 횟수 만큼 실행되지 않았
다는 것이다. 문제는 라인 30 에서 n 의 감소에 있다.

————————————————————————
/* 30 */ n–;
————————————————————————

이 부분은, 바깥 루트의 끝부분에서 매번, array 의 가장 큰 원소는 가장 뒷쪽으로
밀려나므로 그 뒷 부분은 정렬이 필요없다는 사실에 착안해서 최적화시키려고 한 것
이였다. 그러나, 우리가 보았듯이 바깥루프에서 이 부분이 문제를 야기시키고 있다.
가장 간단하게 문제를 고치는 방법은 코드를 제거하는 것이다. 이런 패치를 적용
하여 문제가 제대로 고쳐지는 지 확인해보자.

사. 디버거로 패치하기

이미 위에서 중단점을 설정하여 변수의 값을 볼 수 있음을 알았다. 중단점에서 수
행될 명령을 나열함으로써, 소스코드를 수정하고 컴파일을 다시 시도하기 전에,
문제점을 고쳐보려고 노력해보자. 라인 30에 중단점을 설정하고, n 의 값을 하나
증가시킨다면, 라인 30 이 수행되고 난 후의 n 의 값에는 변함이 없게 된다.

프로그램을 재시작하기 전에 먼저, 이전의 중단점과 display 를 없애자.
어떤 중단점과 display 가 사용가능한지는info 명령을 사용해서 알아볼 수 있다.

(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1: y array[0] @ 5
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x080004ba in sort at debug4.c:20
breakpoint already hit 4 times
cont

이것들을 모두 기능정지 시키거나 없앨 수 있지만, 기능정지만 시킨다면, 나중에
필요할 때 다시 사용할 수 있다.

(gdb) disable break 1
(gdb) disable display 1
(gdb) break 30
Breakpoint 2 at 0x8000650: file debug4.c, line 30.
(gdb) commands 2
Type commands for when breakpoint 2 is hit, one per line.
End with a line saying just “end”.
>set variable n = n+1
>cont
>end
(gdb) run
Starting program: /usr/neil/debug4

Breakpoint 2, sort (a=0x8001754, n=5) at debug4.c:30
30 /* 30 */ n–;
Breakpoint 2, sort (a=0x8001754, n=5) at debug4.c:30
30 /* 30 */ n–;
Breakpoint 2, sort (a=0x8001754, n=5) at debug4.c:30
30 /* 30 */ n–;
Breakpoint 2, sort (a=0x8001754, n=5) at debug4.c:30
30 /* 30 */ n–;
Breakpoint 2, sort (a=0x8001754, n=5) at debug4.c:30
30 /* 30 */ n–;
array[0] = {1, alex}
array[1] = {2, john}
array[2] = {3, bill}
array[3] = {4, neil}
array[4] = {5, rick}

Program exited with code 025.
(gdb)

프로그램은 완전하게 수행되었고 결과도 올바르게 출력되었다. 이제 소스코드를
변경하여 좀 더 많은 데이터를 사용하여 테스트를 해 볼 수도 있다.

아. gdb 에 대한 더 자세한 내용들

GNU 디버거는 실행되는 프로그램의 내부 상태에 대한 수많은 정보를 제공하는 전례
가 없이 강력한 디버거이다. 하드웨어 중단점을 지원하는 시스템에서 gdb 를 사용
하면 실시간으로 변수들의 변화를 관찰할 수 있다. gdb 에서는 또한, ‘watch’ 기
능을 사용할 수 있다. watch 에서는 어떤 표현이 어떤 특정한 값이 될 때, 어느
곳에서 계산이 수행되느냐와는 상관없이, 프로그램을 정지시킨다.

중단점은 횟수와 조건을 수반하여 설정될 수 있다. 이때는, 일정한 횟수가 지났을
때나, 조건이 만족되었을 때에만 트리거한다.

gdb 는 이미 실행되고 있는 프로그램에도 접근할 수 있다. 이 기능은 클라이언트/
서버 시스템을 디버깅 할 때 매우 유용하다. 이미 수행되고 있는 서버 프로세스를,
중지하여 재시작 시키지 않고도 디버깅 할 수 있다. 컴파일 할 때, 최적화의 잇점
과 디버깅 정보를 동시에 포함하기 위하여, gcc -O -g 와 같이 할 수 있다. 이러
한 최적화에서는 코드가 약간 재정렬되고, 코드를 한단계씩 밟아나간다면, 원래의
소스코드에서 의도한 것과 같은 효과를 가져오기 위해 주위로 점프하는 것을 발견
할 지도 모른다.

gdb 는 GNU 공공 라이센스 하에 구할 수 있고, 대부분의 UNIX 시스템 용으로 제공된
다. 우리는 여러분들이 gdb 를 익혀보기를 강력히 추천한다.