[출처] https://kldp.org/node/87778


이 글은 GDB의 기능 중 사용자 명령을 만들어 쓰는 법을 설명합니다. 개발자 여러분들의 칼퇴근에 조금이나마? 도움이 되기를 바랍니다. ^^;

주의! 노파심에서 말씀드리지만, 이 글에서 나온 모든 소스 코드들은 디버깅을 하기 위한 방법을 설명하기 위해, 즉석에서 만든 코드입니다. 이 코드들을 보고 글쓴이를 평가하지 말기 바랍니다. ^^/;

아울러 잘못된 내용이나 오타 등을 발견하셨다면 연락바랍니다. (댓글 남겨 주시면 수정하겠습니다)

Convenience Variables

GDB는 개발자가 디버깅 도중에 사용할 수 있는 일종의 변수(variable)를 제공합니다. 이 변수는 GDB 안에 존재하는 것으로, 디버깅하는 프로그램에 영향을 주지 않습니다. 변수를 쓰는 법은 shell에서 쓰는 것과 비슷하게 이름 앞에 $를 붙여서 변수를 나타냅니다. 변수에 값을 대입하는 것은 set 명령이나 print 명령을 써서 할 수 있습니다. 둘 다 모두 값을 대입하는 데 쓸 수 있으며, print 명령을 써서 대입하게 되면, 대입된 값을 바로 출력한다는 것만 다릅니다.

 (gdb) set $foo = 31         # $foo에 31 대입
 (gdb) p $foo                # $foo 값 출력
 $6 = 31
 (gdb) p $tmp="hello"        # $tmp에 "hello" 대입 및 출력
 $7 = 0x804a068 "hello"
 (gdb) p $tmp                # $tmp 값 출력
 $8 = 0x804a068 "hello"
 (gdb) _

물론, GDB에서 쓰는 변수들은 타입이 지정되어 있지 않습니다. 즉 개발자가 처음에는 정수를 대입했다가, 나중에 포인터 값을 저장하더라도 아무런 문제가 되지 않습니다. 또한 단순한 값 대입 이외에도, 함수를 호출해서 그 결과를 저장할 수도 있습니다.

 (gdb) set $len = list_length(dead_node_list)
 $1 = 13

아래 예제는 매우 쓸모있는 것으로, 구조체를 가리키는 포인터의 배열의 내용을 조사하는 명령입니다. GDB는 <RET>을 누르면, 바로 전 명령을 수행하게 되므로, 단순히 <RET>을 치는 것으로, 배열의 내용을 차례대로 조사할 수 있습니다.

 (gdb) set $i = 0
 ...
 (gdb) print bar[$i++]->contents
 ...
 (gdb) <RET>
 ...
 ...

이런 변수들을 쓸 때, 주의할 점이 하나 있습니다. 이름 앞에 $를 붙이는 것은 단순히 변수 뿐만 아니라, CPU의 레지스터(register)를 다룰 때에도 쓰입니다. 따라서 변수 이름을 지을 때는, CPU 레지스터 이름과 다른 이름을 써야 합니다. 예를 들어 아래 명령을 실행하게 되면 큰일납니다. :)

 (gdb) set $pc = 1234

$pc는 'program counter' 레지스터를 나타내는 이름입니다. 따라서 위 명령을 실행하고 나면 현재 CPU의 program counter register의 값이 바뀌기 때문에 next, run, continue 등의 GDB 명령을 수행할 때, 엉망이 될 확률이 높습니다. GDB는 CPU에 상관없이 $pc, $sp, $fp, $ps를 register 이름으로 사용합니다. 예를 들어, x86 계열에서 프로그램 카운터 레지스터 이름은 $eip이지만, 개발자는 $pc 또는 $eip를 모두 다 사용할 수 있습니다. 이 레지스터 이름의 뜻은 대충 다음과 같습니다:

 $pc - program counter register
 $sp - stack pointer register
 $fp - frame pointer register
 $ps - processor status register

따라서 위 이름은 자신의 CPU 타입에 상관없이 (변수 이름 지을때) 무조건 피해야 합니다. 위 이름 이외에도 CPU가 지원하는 레지스터 이름을 피해야 하는 것은 말할 것도 없습니다. 현재 자신의 CPU에서 지원하는 register의 이름을 보려면 info all-registers 명령을 쓰면 됩니다. 예를 들어 아래 내용은 제 컴퓨터에서 이 명령을 실행한 결과입니다:

 (gdb) info all-registers
 eax            0x804a008	134520840
 ecx            0x804a048	134520904
 edx            0x804a008	134520840
 ebx            0xb7ed0ff4	-1209200652
 esp            0xbfce8840	0xbfce8840
 ebp            0xbfce8868	0xbfce8868
 esi            0xb7f0eca0	-1208947552
 edi            0x0	0
 eip            0x8048431	0x8048431 <main+122>
 eflags         0x200286	[ PF SF IF ID ]
 cs             0x73	115
 ss             0x7b	123
 ds             0x7b	123
 es             0x7b	123
 fs             0x0	0
 gs             0x33	51
 st0            0	(raw 0x00000000000000000000)
 st1            0	(raw 0x00000000000000000000)
 ...

이외에도 피해야 할 이름이 몇 가지 있는데, GDB가 특수한 목적으로 자동으로 값을 넣어주는 변수들이 있습니다. 각 변수들에 대한 자세한 설명은 info(1) GDB 문서를 참고하기 바랍니다. 여기서는 간단히 소개합니다.

 $_         -- last address examined by 'x' command
 $__        -- last value in $_
 $_exitcode -- the exit code when program terminates.

User Defined Commands

GDB는 사용자가 기존의 GDB 명령을 써서 새로운 명령을 만들 수 있는 기능을 제공합니다. 경험상 GDB에서 사용자가 새로 만드른 명령들은 대부분 출력하는 함수들이며, print 명령과 다른 명령들을 조합해서 쓰는 것이 일반적입니다.

사용자 명령(user-defined command)은 define ... end 형태로 정의합니다. 또, 인자를 받아 처리할 수 있으며, 각 인자들은 $arg0, $arg1, ..., $arg9로, 그 값을 읽을 수 있습니다. 또한 $argc는 전달된 인자의 갯수입니다. 예를 들어 아래 명령은 주어진 세 인자를 더해서 그 결과를 출력하는 사용자 명령입니다:

 define adder
   print $arg0 + $arg1 + $arg2
 end

위와 같이 정의했다면, 다음과 같이 쓸 수 있습니다.

 (gdb) adder 1 2 3
 $1 = 6
 (gdb) _

위 명령을 두 개 또는 세 개의 인자를 처리하도록 고치면 다음과 같습니다:

 define adder
   if $argc == 2
     print $arg0 + $arg1
   end
   if $argc == 3
     print $arg0 + $arg1 + $arg2
   end
 end

GDB에서 제공하는 명령들에 대한 도움말은 항상 help 명령으로 얻을 수 있습니다. 우리가 만든 adder에 대한 도움말을 제공하려면 document ... end 명령을 쓰면 됩니다.

 document adder
 Add 2 or 3 arguments and print the result
 end

사용자 명령을 만들때 쓸 수 있는 유용한 GDB 명령들은 다음과 같습니다.

 if, else      -- 주어진 조건식이 참일 경우, 수행. end로 끝남
 while         -- 주어진 조건식이 참일 경우 반복. end로 끝남
 loop_break    -- 가장 안쪽 while을 벗어남
 loop_continue -- 가장 안쪽 while의 조건식 부분으로 점프
 end           -- 블럭 명령들의 끝을 나타냄
 
 echo TEXT             -- text 출력, 히스토리 저장 안됨
 output EXPR           -- 수식만 출력 (newline, "$$ = " 없이..)
 output/FMT EXPR       -- 주어진 포맷으로 출력
 printf STRING, EXPR.. -- printf(3) 스타일로 출력

Examples

간단한 linked list를 생각해 봅시다. 이 리스트는 단일(singular) 리스트이며, 각 노드는 상황에 따라 int, void *, double 타입의 데이터를 저장할 수 있습니다. 대부분 개발자라면 즉시 머리에 다음과 같은 구조체가 떠오를 것입니다:

enum listtype_ {
  LT_NONE,
  LT_INTEGER,
  LT_DOUBLE,
  LT_POINTER,
};
typedef enum listtype_ listtype_t;
 
struct list_ {
  listtype_t type;         /* type of v member, LT_* */
  union {
    int ival;
    void *pval;
    double dval;
  } v;
  struct list_ *next;
};
typedef struct list_ list_t;

위와 같이 만들었다면, 정수를 저장하는 list_t를 만드는 함수는 다음과 같이 만들 수 있을 것입니다 (편의상 에러 검사 등의 코드는 모두 생략합니다):

list_t *
int_node(int value)
{
  list_t *p = malloc(sizeof(*p));
  p->type = LT_INTEGER;
  p->v.ival = value;
  p->next = NULL;
  return p;
}

마찬가지로 void *, double을 저장하는 함수를 각각 ptr_node(), double_node()로 만들었다고 가정합니다.

그리고, 새 노드를 기존 리스트의 앞부분에 추가하는 함수를 다음과 같이 만들었습니다:

list_t *
prepend_list(list_t *list, list_t *newnode)
{
  newnode->next = list;
  return newnode;
}

이제, 이러한 형태의 리스트가 프로그램 전반에 걸쳐서 매우 널리 쓰인다고 가정해봅시다. 개발자는 이런 리스트를 처리하는 함수들을 많이 만들었을 것입니다. 예를 들면, 이러한 리스트를 인자로 받아서, 리스트에 들어있는 정보를 파일에 저장하거나, 네트웍을 통해 다른 컴퓨터에 전송하는 함수들을 생각하시면 됩니다. 이런 함수들은 대개 다음과 같은 형태로 만들어져 있을 것입니다:

void process_int_list(list_t *list);
void process_short_list(list_t *list);

그리고, 이런 함수들에서 몇몇 버그가 발견되었다고 가정해 봅시다. 그렇다면, 개발자는 무엇이 잘못되었는지 알기 위해 시간을 보내게 됩니다. "혹시 인자로 받은 리스트를 연결하는 포인터들이 잘못되었을까?", "저장된 데이터의 타입과 list_t::type의 값이 서로 다르지 않을까?", "전달받은 리스트가 예상했던 것보다 너무 짧거나 긴 것이 아닐까?" 등등.

한가지 상황을 가정해봅시다. 먼저 process_int_list()는 주어진 리스트가 가진 데이터들이 모두 LT_INTEGER 타입일 경우에만 정상적으로 동작합니다. 또 process_short_list()는 주어진 리스트가 가진 노드들의 갯수가 3개 이하일 경우에만 정상적으로 동작합니다. 이제 이 함수들에 비정상적인 리스트가 전달되었다고 가정해 봅시다. 예를 들어, process_int_list()에 전달된 리스트의 노드 중 하나가 LT_POINTER 타입이고, process_short_list()에 전달된 리스트가 가진 노드의 갯수가 5개라고 가정합니다.

그리고 개발자는 이 두 함수에 breakpoint를 걸고, 조사하기 시작합니다. 예를 들어 아래 GDB session은, 개발자가 proces_int_list()에 breakpoint를 걸고, 전달된 리스트가 정상적인지 확인하는 과정을 담은 것입니다:

 (gdb) br process_int_list
 Breakpoint 1 at 0x804845f: file list.c, line 81.
 (gdb) r
 Starting program: /home/cinsk/src/a.out 
 ...
 Breakpoint 1, process_int_list (list=0x804a050) at list.c:81
 (gdb) p list
 $1 = (list_t *) 0x804a050
 (gdb) p *list
 $2 = {type = LT_INTEGER, v = {ival = 8, pval = 0x8, dval = 3.9525251667299724e-323}, next = 0x804a038}
 (gdb) set print pretty on
 (gdb) p *list
 $3 = {
   type = LT_INTEGER, 
   v = {
     ival = 8, 
     pval = 0x8, 
     dval = 3.9525251667299724e-323
   }, 
   next = 0x804a038
 }
 (gdb) p *list->next
 $4 = {
   type = LT_POINTER, 
   v = {
     ival = -559038737, 
     pval = 0xdeadbeef, 
     dval = 1.8457939563190925e-314
   }, 
   next = 0x804a020
 }
 (gdb) p *list->next->next
 $5 = {
   type = LT_INTEGER, 
   v = {
     ival = 0, 
     pval = 0x0, 
     dval = 0
   }, 
   next = 0x804a008
 }
 (gdb) p *list->next->next->next
 $6 = {
   type = LT_INTEGER, 
   v = {
     ival = 4, 
     pval = 0x4, 
     dval = 1.9762625833649862e-323
   }, 
   next = 0x0
 }
 (gdb) _

위 session을 보시면, 리스트의 노드들을 살펴보기 위해, 다음과 같은 명령을 쓴 것을 알 수 있습니다:

 (gdb) p *list
 (gdb) p *list->next
 (gdb) p *list->next->next
 (gdb) p *list->next->next->next

위 예에서는 리스트가 짧아서 저 정도만 조사해도 되지만, 리스트가 길다면 위와 같이 계속 쫒아가면서 조사하는 것은 매우 번거로운 일이 됩니다. 실제로 우리가 원하는 것은 노드들을 따라가면서, 각 노드의 타입이 LT_INTEGER인지만 조사하면 됩니다. 따라서 다음과 같이 GDB 사용자 명령을 만들 수 있습니다:

 define list_intp
   set $ptr = $arg0
   set $valid = 1
   while $ptr != NULL
     if $ptr->type != LT_INTEGER
       set $valid = 0
       loop_break
     end
     set $ptr = $ptr->next
   end
   p $valid
 end
 
 document list_intp
 Test whether the list consists of LT_INTEGER nodes.
 end

위 명령은 주어진 리스트를 따라가면서, 모든 노드의 타입이 LT_INTEGER이면 1을, 그렇지 않으면 0을 리턴하는 사용자 명령입니다. lst1은 LT_INTEGER 타입만 있는 리스트이고, lst2가 LT_POINTER 타입이 있는 리스트라 가정하면 다음과 같이 테스트할 수 있습니다:

 (gdb) list_intp lst1
 $3 = 1
 (gdb) list_intp lst2
 $4 = 0
 (gdb) _

이제, 주어진 리스트의 길이를 리턴하는 함수를 만들어 봅시다. 이 코드는 list_intp와 거의 비슷합니다:

 define list_len
   set $ptr = $arg0
   set $len = 0
   while $ptr != NULL
     set $len++
     set $ptr = $ptr->next
   end
   print $len
 end
 
 document list_len
 Return the number of nodes in the list
 end

그리고 나서 다음과 같이 쓸 수 있습니다.

 (gdb) list_len list
 $5 = 4
 (gdb) _

주어진 리스트의 모든 링크들을 출력하는 함수도 만들어 봅시다:

 define list_dump
   set $ptr = $arg0
   while $ptr != NULL
     printf "0x%08x: ", $ptr
     output $ptr->type
     printf ", next(0x%08x)\n", $ptr->next
     set $ptr = $ptr->next
   end
 end
 
 document list_dump
 Dump the contents of the list.
 end

아래는 LT_INTEGER 타입으로 이루어진 리스트를 위 명령을 써서 출력한 예입니다:

 (gdb) list_dump lst
 0x0804a050: LT_INTEGER, next(0x0804a038)
 0x0804a038: LT_INTEGER, next(0x0804a020)
 0x0804a020: LT_INTEGER, next(0x0804a008)
 0x0804a008: LT_INTEGER, next(0x00000000)
 (gdb) _

지금까지 간단한 리스트 처리 프로그램에서 GDB 사용자 명령을 쓰는 법에 대해 알아보았습니다. 사실 위에 list_dump나 list_len과 같은 명령들은, 개발자가 소스에 비슷한 함수를 만들어 두었다면 그냥 print 명령으로 불러서 처리할 수도 있습니다. 예를 들어 다음과 같이 주어진 리스트의 길이를 리턴하는 함수가 있다고 가정해 봅시다.

int list_length(const list_t *list);

그럼 GDB에서 주어진 리스트 lst의 길이를 알기 위해 다음과 같이 실행하면 됩니다:

 (gdb) p list_length(lst)
 $1 = 3
 (gdb) _

다만, 이런 함수를 만들어 두지 않았다면, GDB 사용자 명령으로 간단히 만들어서, 디버깅을 쉽게 할 수 있다는 것을 알아두셨으면 합니다.

Command Files

GDB를 매번 실행할 때마다 사용자 명령들을 만들어야 한다면, 차라리 안 쓰는 것이 더 편할지도 모릅니다. 일반적으로 사용자 명령은 별도의 파일로 만들어 두고, GDB를 실행할 때 불러오게 하는 것이 좋습니다. 따로 만들어 둔 파일을 불러오려면 source 명령을 사용합니다. 예를 들어, 앞에서 만든 명령들을 command.gdb에 저장해 두었다면 다음과 같이 불러올 수 있습니다:

 (gdb) source command.gdb

매번 위와 같이 실행하는 것도 귀찮다면, GDB가 자동으로 읽는, 설정 파일에 써 두는 것도 좋습니다. 보통 GDB가 실행되면, 먼저 사용자 홈 디렉토리에 있는 .gdbinit을 읽고, 그 다음에 현재 디렉토리에 있는 .gdbinit을 읽습니다. 따라서 같은 프로그램을 매번 디버깅해야 하는 상황이라면 사용자 명령 정의를, .gdbinit에 써 두는 것이 좋습니다. 주의. Windows나 DOS, DJGPP용 GDB일 경우 .gdbinit 대신 gdb.ini를 사용합니다.

source로 불러오는 파일이나 .gdbinit 파일의 형식은 같으며, "#" 다음에 오는 문자는 모두 주석(comment)입니다. Emacs 소스나 Linux kernel 소스에 .gdbinit가 들어 있으니 참고삼아 읽어볼만 합니다. 주의. Linux kernel 소스에 있는 GDB 파일들은 대개 dot.gdbinit* 꼴로 파일 이름이 붙습니다. 다음 명령으로 찾아볼 수 있습니다.

 $ find /usr/src/linux -name 'gdbinit'

끝.

GDB

[출처] https://kldp.org/node/71806



GDB 잘 쓰기



디버깅 작업 또는 프로그램의 안전성을 검사할 때 디버거를 잘 쓰면 꽤 많은 시간을 절약할 수 있습니다. 대부분 개발자들이 GDB를 써서 디버깅을 하고 있지만, GDB가 가지고 있는 강력한 기능들을 거의 쓰지 못하고 있기 때문에, 이 글에서는 자주 쓰이지는 않을 지언정, 알면 매우 도움이 되는 기능들을 위주로 살펴보겠습니다.

먼저, 이 글을 읽는 분들이 GDB의 기본적인 사용 방법 (특히 break, run, continue, file, backtrace, print 등)을 알고 있다고 가정하겠습니다. 기본적인 사용 방법을 모르신다면 Emacs/GDB/etags/cscope나 기타 GDB manual을 참고하기 바랍니다.

Breakpoints

break 명령은 대개 다음과 같이 쓸 수 있다는 것은 이미 알고 계실 것입니다:

(gdb) break                # 현재 줄에 breakpoint 설정
(gdb) break 31             # 현재 파일 31번째 줄에 breakpoint 설정
(gdb) break foo            # 함수 foo에 breakpoint 설정
(gdb) break list::next     # list 클래스 next 멤버 함수에 설정
(gdb) break hello.c:main   # hello.c 파일의 main 함수에 설정
(gdb) break util.c:300     # util.c 파일의 300번째 줄에 설정

특히 C++의 경우, 한 클래스의 모든 멤버 함수에 breakpoint를 설정하고 검사할 필요가 있는데, 이 경우, 정규 표현식(regular expression)으로 breakpoint를 설정하는 rbreak 명령을 쓰면 편리합니다. 예를 들어 보면:

(gdb) rbreak f*o           # "f*o"를 만족하는 심볼 전체에 대해 설정
(gdb) rbreak list::        # "list::.*"를 만족하는 심볼 전체에 대해 설정

특히 위 두번째 예제를 보시면 ".*"이 항상 default로 따라 온다는 것을 알 수 있습니다. 사실 rbreak 명령에 "foo"를 준 경우 사용되는 정규 표현식은, 정확히 말하면 ".*foo.*"가 됩니다. 따라서 "foo"로 시작하는 함수 전체에 대해 breakpoint를 설정하고 싶다면, 다음처럼 쓰면 됩니다:

(gdb) rbreak ^foo

breakpoint를 설정하면, 해당 breakpoint마다 번호(BNUM)가 주어지고, 이 번호를 써서 다양한 작업을 수행할 수 있습니다. 예를 들어, 전체 breakpoint 목록을 보고 싶다면:

(gdb) info b
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x08066b44 in eventShow() at menubar.cpp:1017
        breakpoint already hit 3 time
2   breakpoint     keep y   0x080b06f4 in Play() at thumbview.cpp:416
3   breakpoint     keep y   0x08066e7e in ActPlay() at menubar.cpp:1085
4   breakpoint     keep y   0x08059cd3 in Play_SS(int, int) at widgets.cpp:2183
(gdb)

첫번째 컬럼(Num)은 각 breakpoint에 대한 고유번호(BNUM)를 나타냅니다. 그리고 두번째 컬럼(Type)은 breakpoint인지 watchpoint인지 catchpoint인지를 나타냅니다. (watchpoint와 catchpoint는 다음에 설명..) 그리고 세번째 컬럼(Disp)은 이 breakpoint의 특징을 나타냅니다. (다음에 설명). 네번째 컬럼(Enb)는 현재 이 breakpoint가 활성화되어 있는지를 나타냅니다. 비활성화(n)로 표시된 breakpoint는 동작하지 않습니다. 활성화/비활성화는 'enable br [BNUM]' 또는 'disable br [BNUM]'으로 변경할 수 있습니다. 예를 들어 1번 breakpoint를 비활성화하고 싶다면:

(gdb) disable br 1

전체 breakpoint를 활성화하고 싶다면:

(gdb) enable br

2번, 4번 breakpoint를 비활성화하고 싶다면:

(gdb) disable br 2 4

2번부터 5번까지 breakpoint를 활성화 하고 싶다면:

(gdb) enable br 2-5

등으로 할 수 있습니다.

때때로, 딱 한 번만 쓸 breakpoint가 필요한 경우가 있습니다. 이 경우 쓸 수 있는 명령은 enable br once [BNUM] 또는 enable br delete [BNUM]을 쓸 수 있습니다. 예를 들어 아래 명령은 1번, 3번 breakpoint를 활성화하고, 사용된 경우 바로 비활성화시킵니다:

(gdb) enable br once 1 3

아래 명령은 4번 breakpoint를 활성화하고, 사용된 경우, 이 breakpoint를 삭제합니다:

(gdb) enable br delete 4

쓸모있는 기능 중 하나가 바로 breakpoint에 조건을 지정하고, 해당 조건을 만족할 경우에 멈추도록 하는 것입니다. 예를 들어 다음과 같은 코드가 있다고 가정해 봅시다:

int i = 0;

/* do something #1 */

for (i = 0; i < 1000; i++) {
  /* do something #2 */
  /* do something #3 */
}

이상하게도 i가 456일때 반복문 안에서 프로그램이 이상하게 동작한다고 가정해 봅시다. 이 때 "do something #2" 부분에 breakpoint를 걸었다면 (이 breakpoint의 번호는 8번이라고 가정합시다), 반복할 때마다 계속 프로그램 실행이 멈출 겁니다. 정확히 1000번 멈추겠죠. 456번까지 진행한다는 것은 매우 귀찮은 일입니다. 이 경우, 다음과 같이 조건을 지정할 수 있습니다:

(gdb) cond 8 i == 456

즉, 8번 breakpoint는 i == 456을 만족할 때에만 멈추도록 지정합니다. 조건식에는 단순한 상수 비교 이외에, 복잡한 함수 호출도 가능합니다. 예를 들면 다음과 같습니다:

(gdb) cond 8 foo(i) > bar(rand())

앞에서 예로 든 코드는 단순 반복문이기 때문에, 처음 456 - 1번에 발생하는 breakpoint는 무시하라고 지정할 수도 있습니다. 처음 N번 발생하는 breakpoint를 무시하라는 명령은 다음과 같습니다:

(gdb) ignore 8 455

즉, 8번 breakpoint는 455번 동안 무시됩니다.

또, 다음과 같은 코드를 가정해 봅시다:

int i = 0;
int j, k;
long l;

while (1) {
  j = rand();
  k = some_funtion(j, time());

  /* do something #1 */
  l = j & 0xFF00 + (int)(log(k) * 3.2108) - ...;

  if (some_condition)
    break;
}

위 코드는 j와 k가 실행할 때마다 값이 변합니다. 그리고 이상하게도 j < k 일때 변수 l이 이상한 결과를 가지는 것 같지만, 확실하지는 않습니다. 우리가 확신할 수 있는 것은 j < k일 경우, l은 항상 양수이어야 한다는 것입니다. 그래서 l의 값이 전체 반복을 끝낼 동안 어떤 값을 가지고 있는지 검사해보고 싶습니다. 이 경우 해당 breakpoint에서 멈출 때, 특정 명령을 수행하도록 하는 GDB 명령인 commands를 쓰면 됩니다.

일단 "l = j & 0xFF00..." 부분에 breakpoint를 걸고 (9번 breakpoint라고 가정), 다음 명령을 내립니다:

(gdb) commands 9
Type commands for when breakpoint 9 is hit, one per line.
End with a line saying just "end".
>silent
>if j < k
 >printf "l is %d\n", l
 >end
>cont
>end

대충 눈치가 빠른 분은 아시겠지만 'commands [BNUM] ... end'는, BNUM breakpoint에서 멈췄을 때, "..."에 지정한 GDB 명령들을 수행합니다. 일단 silent 명령으로 명령 자체가 출력되지 않도록 한 다음, GDB printf 명령으로 변수 l 값을 출력합니다. 그리고 continue 명령으로 계속 프로그램을 진행하도록 합니다. 그 결과, 프로그램을 실행할 경우, breakpoint에서 멈추고 l 값을 출력한 다음 프로그램을 자동으로 진행합니다. 이 과정은 반복문이 끝날 때까지 계속되기 때문에, 다음과 같은 비슷한 출력을 얻을 수 있습니다.

(gdb) continue
l is 3
l is -2
l is 2
l is 1
l is -3

앞에서 j < k일 때, l은 항상 양수여야 한다고 말했습니다. 위 결과를 보고 우리는 l 값이 때때로 잘못된다는 것을 쉽게 알 수 있습니다.

commands에 쓸 수 있는 GDB 명령어 형태는 다음 기회에...

가끔 next나 step으로 실행 과정을 따라 가다가 반복문을 만날 경우, 반복문 끝난 부분으로 바로 건너뛰거나, 현재 함수의 실행을 정상적으로 끝내고 상위 함수로 돌아가야할 경우가 있습니다. 예를 들어:

for (i = 0; i < 1000; i++) {
  /* do something #1 */
  /* do something #2 */
}
/* do something #3 */

현재 "/* do something #2 */" 부분까지 실행했고, 이 반복문에 이상이 없다고 판단되면, 반복문 다음까지 빠르게 진행하고 싶을 겁니다. 이 경우, until 명령이나 advance 명령을 쓰면 편리합니다.

until 명령을 쓰면, 반복문이 아닌 경우에는 next 명령과 똑같이 동작합니다.

(gdb) until

반복문일 경우, 현재 스택 프레임 (즉, 현재 함수) 안에서, 현재 줄 다음 줄에 올 때까지 프로그램을 실행합니다. 쉬운 말로, 루프를 진행하고 빠져 나오는 순간까지 실행한 다음 "(gdb)" 프롬프트를 보여줍니다.

advance 명령은 continue 명령과 마찬가지로 프로그램을 주욱 실행시키는 대신, 지정한 곳에 코드 흐름이 오면 바로 멈춥니다. 예를 들어 위 코드의 "/* do something #3 */" 부분의 줄 번호가 34였다면, until 명령 대신 다음과 같이 실행할 수도 있습니다:

(gdb) advance 34

advance 명령은 스택 프레임에 대한 제한이 없기 때문에, 현재 함수가 아닌, 아무 곳이나 설정할 수 있으며, 위치 지정은 줄 번호 뿐만 아니라, break 명령에 쓰는 모든 형식을 다 지원합니다.


네트워크로 서비스 요청 데이터를 전송받아 분석하고, 적절한 기능을 수행하고, 그 결과를 돌려주는 서버 프로그램을 생각해 봅시다. 그리고 다음과 같은 꼴로 되어 있다고 가정해 봅시다:

#define PACKET_MAX      10

int
fetch(void)
{
  int packet_received = 0;
  int received[PACKET_MAX];

  while (1) {
    if (!packet_received) {
      if (recv_data(received, PACKET_MAX) == 0)
        packet_received = 1;
    }

    /* do work here */

    process_packet(received, PACKET_MAX);
  }
  return 0;
}

이 프로그램은 평소에는 정상적으로 잘 동작하지만, 특정 패킷을 받으면 이상하게 동작한다고 가정합시다. 그리고 이 패킷은 아주 가끔 들어온다고 가정해 봅시다. 원하는 대로 패킷을 보내주는 프로그램을 따로 작성해 두지 않았다면, 이 프로그램을 디버깅하기 위해서, 문제를 일으키는 패킷이 올 때까지 하염없이 기다려야할 지도 모릅니다. 실제 코드는 다음과 같습니다:

만약 원하는 패킷이 recv_data()를 통해 들어왔다고 가정합시다. 이 때 packet_received는 1이 되고, 그에 따라 처리 작업이 이상하게 동작할 것입니다. 이 때, received의 내용을 저장하기 위해, 다음 명령을 쓸 수 있습니다:

(gdb) dump binary value buggy.dat received

위 명령을 수행하면 배열 received의 내용을 파일 buggy.dat에 저장합니다. 만약 시작 주소와 끝 주소를 알고 있다면 다음 명령을 쓸 수 있습니다:

dump binary data buggy.dat START-ADDR END-ADDR

이 때, START-ADDR는 시작 주소를, END-ADDR는 끝 주소를 나타냅니다. 즉, 앞 received 배열의 경우, 다음과 같이 쓸 수 있습니다.

(gdb) dump binary memory buggy.dat received received+10

어느 방법을 썼든지, 현재 디렉토리에는 buggy.dat이라는 파일로, 배열 received의 내용이 저장될 것입니다. 이는 메모리 내용을 그대로 dump시킨 것이므로 od(1)와 같은 툴을 써서 그 내용을 직접 볼 수 있습니다. received 배열은 int 배열이므로 다음과 같이 확인 가능합니다:

$ od -td buggy.dat 
0000000         163         151         162          85
0000020          83         190         241         252
0000040         249         121
0000050
$ _

만약, 바로 디버깅을 성공적으로 끝냈다면, 사실 위와 같은 기능은 큰 역할을 발휘하지 못합니다. 하지만, 계속해서 디버거를 실행해서 여러번 디버깅을 해야 한다면 꽤 쓸모있다는 것을 알 수 있습니다.

일단, 새로 GDB를 띄워 디버깅을 시작했다고 합시다.

    if (!packet_received) {

위 코드를 실행할 때, 강제로 packet_received를 1로 만들어, 패킷을 받는 부분을 건너뜁니다. 변수의 값 변경은 print 명령으로 쉽게 할 수 있습니다:

(gdb) p packet_received = 1

그리고 나서, received 배열을 아까 저장해 두었던 buggy.dat에서 다음과 같이
불러올 수 있습니다:

(gdb) restore buggy.dat binary received
Restoring binary file buggy.dat into memory (0xbfeda890 to 0xbfeda8b8)

이 외에도, GDB는 타 디버거에 비해 강력한 기능들을 많이 제공합니다. 다음 기회에 좀 더 알아보겠습니다.




=======

네트워크 서버등을 볼때는 bit계산이나 8진수로 봐야할때가 종종 생기죠. 이럴때 print 명령에
포매팅을 하면 좋은데 많은 분들이 쓰지 않더군요.

예를 들어 rts_mask 라는 곳에 값을 p rts_mask 하면 값이 그냥 10진수로 출력되지만,
p/t rts_mask 하면 2진수로 출력되죠.

주소값의 내용을 출력하는 x 명령도 x &buf 하면 그냥 buf 의 내용을 형에 따라서 출력하지만,
x/wx &buf 하면 buf에서 w(word)사이즈만큼 x(hex) 출력으로 출력하죠. 이런식으로
x/20bt 0xe1b40123 하면 해당 주소에서 20b(20바이트)를 로 인해서 2진수(t)로 출력해주죠.

gdb는 참 쓰면 쓸수록 dbx보다 좋은것 같습니다. 솔직히 dbx로 디버깅할려면 너무 힘들어요.
의외로 old version의 dbx를 쓰시는 업체도 많은듯 싶습니다. 좀 gdb좀 깔아주면 덧나낭... -_-;;

PS) 이외에 gdb는 명령어를 안치고 그냥 엔터만 쳐도 이전 명령어가 반복되는데,
이거 몰라서 계속 n 치는 분도 많이 봤습니다.

=======

gdb 실행파일이름 processid


이렇게 하면 해당 processid 로 attach 상태가 됩니다. 프로그램은 일시 멈춤 상태가 됩니다.

이때 'generate-core-file' 명령으로 강제로 코어 파일을 만들 수 있습니다.

'c'를 입력하면 gdb가 attach 된 상태에서 continue 상태가 되겠죠.

'detach' 또는 'quit' 하면 detach 되서 gdb를 실행하기 전 상태로 복귀 됩니다.

'kill' 명령으로 프로세스를 죽일 수도 있습니다.

========

윈도우쪽 프로그램은 

프로그램 로직 내에서 __asm { int 3 }; 을 호출하면

자동으로 기본 디버거를 호출해서 디버깅 할 수 있습니다.

(프로그램에 예외가 발생해서, 디버깅하시겠냐고 물어보지요.)

========

GDB