GDB 잘 쓰기 2: User Defined Commands
[원문 링크]
- GDB 잘 쓰기
- GDB 잘 쓰기 2: User defined commands
이 글을 처음 보시는 분은 먼저 GDB 잘 쓰기 먼저 읽어보시기 바랍니다.
이 글은 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'
끝.
댓글
wow. 매우 유용합니다~
wow. 매우 유용합니다~ 1년만의 업데이트군요!
+1점 날립니다~
+1점 날립니다~
cinsk님 위키에서
출력할 방법이 있을까요?
그럼. 잘 보겠습니다.
유용한 지식 감사합니다.
아주 시기적절하고 유용한 내용이군요.
지금 바로 적용하러 갑니다.
감사합니다.
좋은정보
좋은정보 감사합니다.
--------------------------------------------------------
남이 가르쳐주는 것만 받아들이는 것이 아니라, 스스로 만들고, 고쳐가는 사람을 '해커'라고 부른다.
그리고 자신이 쌓아온 노하우를 거리낌없이 나눌 줄 아는 사람을 '진정한' 해커라고 한다.
-Rob Flickenger 'Linux server hacks'
http://heu
-----------------------------------------------------
남이 가르쳐주는 것만 받아들이는 것이 아니라, 스스로 만들고, 고쳐가는 사람을 '해커'라고 부른다.
그리고 자신이 쌓아온 노하우를 거리낌없이 나눌 줄 아는 사람을 '진정한' 해커라고 한다.
-Rob Flickenger 'Linux server hacks'
DEBIAN TESTING, KDE...
debpolaris.blogspot.kr
댓글 달기