이 시리즈의 원 저자인 Tom Ryder의 허락을 받고 올리는 번역글입니다. IDE가 할 수 있는 기능을 UNIX 계열의 shell 안에서도 원활하게 할 수 있는 비결을 초보자도 알기 쉽게 잘 설명한 글일 뿐만 아니라 UNIX 자체의 철학이나 기본 사용법을 따라잡기에도 굉장히 좋은 글이라 생각되어 우리말로 옮기고자 합니다. 프로그래밍 용어는 웬만하면 원래 영단어로 쓰겠습니다. 언제든지 더 좋은 표현에 대한 의견은 감사합니다.



UNIX as IDE: Debugging

2012년 2월 14일 Tom Ryder가 작성


프로그램에서 예기치 않은 행동이 감지됐을 때, GNU/Linux는 문제를 진단하는 다양한 명령줄 도구를 제공한다. GNU debugger인 gdb와 그 비슷한 도구인 Perl debugger를 사용하면서 IDE에서 breakpoint를 세팅하고 한 줄 한 줄 돌려보듯이 프로그램을 진단하는 것에 익숙해질 수 있다. gdb보다 더 상세한 레벨의 진단을 할 수 있는 도구도 있는데, 프로그램이 어떻게 시스템과 상호 작용하는지, 프로그램이 resource를 어떻게 사용하는 지 등을 관찰할 수도 있다.



Debugging with gdb

gdb로 debugging하는 것은 Eclipse나 MS Visual Studio와 같은 현대적인 IDE에서 하던 것과 크게 다르지 않다. 다만 compile 단계에서 debugging symbol을 binary에 심어 놓도록 해야 하는데, gcc에서는 -g 옵션으로 그 명령을 전달할 수 있다. Compile하는 데 어려움을 겪는다면, -Wall을 이용해서 놓칠 수 있는 에러 사항을 체크하는 것도 도움이 된다.

$ gcc -g -Wall example.c -o example


gdb를 사용하는 전형적인 방법으로는 compile된 프로그램을 부분적으로 실행하면서 프로그램의 상태를 조사할 수 있는 shell로서 사용하는 것이다.

$ gdb example
...
Reading symbols from /home/tom/example...done.
(gdb)


(gdb) 프롬프트가 뜨면, run을 입력해서 프로그램을 실행시킬 수 있다. 그리고 segmentation faults와 같은 에러가 나오는 시점에서 더욱 자세한 정보들을(어느 파일 몇 번째 줄에서 생기는 오류인지 등) 제공해준다. 위에서 설명한 대로 debugging을 위한 compile 후 gdb shell에 진입해서 run까지 할 수 있다면 버그를 잡는 것은 훨씬 쉬워질 것이다.

(gdb) run
Starting program: /home/tom/gdb/example

Program received signal SIGSEGV, Segmentation fault.
0x000000000040072e in main () at example.c:43
43     printf("%d\n", *segfault);


(gdb) shell에서 에러가 나서 프로그램이 끝나면 backtrace 명령을 통해 에러를 일으키는 함수가 뭐였는지 알려준다. 이를 통해 에러의 원인이 될 수 있는 범위를 좁혀서 조사할 수 있다.

(gdb) backtrace
#0  0x000000000040072e in main () at example.c:43


break 명령을 통해서 줄 번호나 함수명에 맞춰서 프로그램 실행을 멈추는 breakpoint를 세울 수 있다.

(gdb) break 42
Breakpoint 1 at 0x400722: file example.c, line 42.
(gdb) break malloc
Breakpoint 1 at 0x4004c0
(gdb) run
Starting program: /home/tom/gdb/example

Breakpoint 1, 0x00007ffff7df2310 in malloc () from /lib64/ld-linux-x86-64.so.2


breakpoint에 다다라서 멈추면 step을 통해 일정 코드 단위에 맞춰 프로그램을 실행시키는 것이 유용하게 쓰인다. 그리고 Enter를 치면 같은 명령을 반복할 수 있다. 이런 Enter를 통한 명령 반복은 어느 gdb 명령에도 적용되는 방법이다.

(gdb) step
Single stepping until exit from function _start,
which has no line number information.
0x00007ffff7a74db0 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6


이미 다른 곳에서 돌아가고 있는 프로세스의 ID를 잡아서 gdb에 붙일 수도 있다.

$ pgrep example
1524
$ gdb -p 1524


위와 같은 방법은 어떤 프로그램이 예상치 못하게도 영원히 끝나지 않을 것처럼 돌아가고 있을 때, 그 output stream을 돌리는 데 유용하게 쓰일 수 있다.



Debugging with valgrind

훨씬 최근에 만들어진 valgrindgdb와 비슷한 방법으로 debugging을 할 수 있는 도구이면서 많은 추가적인 검사 기능과 명령을 제공한다. 그 대표적인 도구 중 하나로 buffer overflow와 같은 일반적인 메모리 에러를 감지하는 데 쓰일 수 있는 Memcheck 도구가 있다.

$ valgrind --leak-check=yes ./example
==29557== Memcheck, a memory error detector
==29557== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==29557== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==29557== Command: ./example
==29557==
==29557== Invalid read of size 1
==29557==    at 0x40072E: main (example.c:43)
==29557==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==29557==
...


프로그램의 정말 엄밀히 검사하기 위해서 gdbvalgrind를 모두 사용할 수도 있다. Zed Shaw의 Learn C the Hard Way에서는 일부러 고장난 프로그램을 만들고 valgrind를 이용해서 debugging을 하게 만든다. 이런 방법은 초심자에게 valgrind의 기초적인 사용법을 가르치는데 매우 좋은 방법이다.



ltrace를 이용한 시스템 및 라이브러리 호출 보기

strace, ltrace 도구는 프로그램이 호출하는 시스템 명령, 라이브러리 명령을 감시해서 그 사용 내역을 스크린이나 파일에 기록해준다. strace는 시스템 명령을, ltrace는 라이브러리 명령을 감시한다. 파일에 기록된 호출 내역은 꽤나 유용하게 쓰인다.


ltrace를 프로그램 경로만 넘겨주면 ltrace가 프로그램을 돌리고 그 라이브러리 명령을 호출하는 내역을 프로그램이 끝날 때까지 감지해서 보여준다.

$ ltrace ./example
__libc_start_main(0x4006ad, 1, 0x7fff9d7e5838, 0x400770, 0x400760
srand(4, 0x7fff9d7e5838, 0x7fff9d7e5848, 0, 0x7ff3aebde320) = 0
malloc(24)                                                  = 0x01070010
rand(0, 0x1070020, 0, 0x1070000, 0x7ff3aebdee60)            = 0x754e7ddd
malloc(24)                                                  = 0x01070030
rand(0x7ff3aebdee60, 24, 0, 0x1070020, 0x7ff3aebdeec8)      = 0x11265233
malloc(24)                                                  = 0x01070050
rand(0x7ff3aebdee60, 24, 0, 0x1070040, 0x7ff3aebdeec8)      = 0x18799942
malloc(24)                                                  = 0x01070070
rand(0x7ff3aebdee60, 24, 0, 0x1070060, 0x7ff3aebdeec8)      = 0x214a541e
malloc(24)                                                  = 0x01070090
rand(0x7ff3aebdee60, 24, 0, 0x1070080, 0x7ff3aebdeec8)      = 0x1b6d90f3
malloc(24)                                                  = 0x010700b0
rand(0x7ff3aebdee60, 24, 0, 0x10700a0, 0x7ff3aebdeec8)      = 0x2e19c419
malloc(24)                                                  = 0x010700d0
rand(0x7ff3aebdee60, 24, 0, 0x10700c0, 0x7ff3aebdeec8)      = 0x35bc1a99
malloc(24)                                                  = 0x010700f0
rand(0x7ff3aebdee60, 24, 0, 0x10700e0, 0x7ff3aebdeec8)      = 0x53b8d61b
malloc(24)                                                  = 0x01070110
rand(0x7ff3aebdee60, 24, 0, 0x1070100, 0x7ff3aebdeec8)      = 0x18e0f924
malloc(24)                                                  = 0x01070130
rand(0x7ff3aebdee60, 24, 0, 0x1070120, 0x7ff3aebdeec8)      = 0x27a51979
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++


이 역시 이미 돌고 있는 프로세스의 ID를 잡아서 붙일 수 있다.

$ pgrep example
5138
$ ltrace -p 5138


스크린으로 출력물을 읽는 것보다는 파일에 기록해서 보는 것이 일반적으로 좋은 생각일 것이다. 여기서는 -o 옵션을 통해서 출력물을 파일로 기록할 수 있다.

$ ltrace -o example.ltrace ./example


Vim과 같은 편집기에서는 파일로 저장된 ltrace 기록물을 syntax highlighted된 텍스트로 보여준다.

Vim session with ltrace outputVim session with ltrace output



필자는 debugging을 하면서 실수로 어딘가 linking이 부적절하다든지, chroot 환경에서 어떤 resource가 필요한데 없다든지 하는 추측을 확인하고자 할 때 ltrace가 굉장히 유용하다고 느꼈다. 왜냐하면 ltrace의 결과물에서 동적으로 linking하는 시점에서 라이브러리를 찾으려는 흔적이나 /etc에서 설정 파일을 열려고 하는 흔적, /dev/random, /dev/zero 등의 device를 사용하려고 하는 흔적을 찾을 수 있기 때문이다.



lsof로 열려있는 파일 추적하기

lsof는 실행된 프로그램이 열어 놓은 device, file, stream을 살펴볼 수 있다.

$ pgrep example
5051
$ lsof -p 5051


예를 들어 아래는 필자의 홈 서버에서 돌아가고 있는 apache2 프로세스의 lsof 결과의 첫 부분을 보여드리겠다.

# lsof -p 30779
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
apache2 30779 root  cwd    DIR    8,1     4096       2 /
apache2 30779 root  rtd    DIR    8,1     4096       2 /
apache2 30779 root  txt    REG    8,1   485384  990111 /usr/lib/apache2/mpm-prefork/apache2
apache2 30779 root  DEL    REG    8,1          1087891 /lib/x86_64-linux-gnu/libgcc_s.so.1
apache2 30779 root  mem    REG    8,1    35216 1079715 /usr/lib/php5/20090626/pdo_mysql.so
...


이와 같은 작업을 동적 경로인 /proc에서 프로세스에 해당하는 entry를 체크해서 확인할 수 있다. 재미있지 않은가?

# ls -l /proc/30779/fd


이렇게 동적 경로를 통해 확인하는 것은 파일 잠김이 있는 경우나, 프로세스가 굳이 필요 없는 파일을 붙들고 있는 경우 등의 혼란스러운 상황에서 유용하게 쓰일 수 있는 방법이다.



pmap으로 메모리 할당 내역 보기

마지막으로 debugging 팁을 주자면, pmap을 이용해서 특정 프로세스의 메모리 할당 상황을 볼 수 있다.

# pmap 30779
30779:   /usr/sbin/apache2 -k start
00007fdb3883e000     84K r-x--  /lib/x86_64-linux-gnu/libgcc_s.so.1 (deleted)
00007fdb38853000   2048K -----  /lib/x86_64-linux-gnu/libgcc_s.so.1 (deleted)
00007fdb38a53000      4K rw---  /lib/x86_64-linux-gnu/libgcc_s.so.1 (deleted)
00007fdb38a54000      4K -----    [ anon ]
00007fdb38a55000   8192K rw---    [ anon ]
00007fdb392e5000     28K r-x--  /usr/lib/php5/20090626/pdo_mysql.so
00007fdb392ec000   2048K -----  /usr/lib/php5/20090626/pdo_mysql.so
00007fdb394ec000      4K r----  /usr/lib/php5/20090626/pdo_mysql.so
00007fdb394ed000      4K rw---  /usr/lib/php5/20090626/pdo_mysql.so
...
total           152520K


실행 중인 프로세스가 어떤 라이브러리가 사용하고 있는 지에 대해 shared memory 내역과 함께 보여준다. 위 예제 마지막에 있는 total을 프로세스가 사용하는 shared library의 메모리 총합으로 잘못 이해할까봐 더 설명하자면, 오직 30779 프로세스 하나만이 메모리를 사용하는 것이 아니다. 어떤 프로세스가 "실제로" 사용하는 메모리를 결정하는 것은 위 예제처럼 shared library의 메모리 내역을 더하는 것보다는 조금 더 복잡한 이야기이다.



UNIX as IDE




'GNU-Linux' 카테고리의 다른 글

UNIX as IDE  (0) 2017.11.09
UNIX as IDE: 7. Revisions  (0) 2017.11.09
UNIX as IDE: 5. Building  (0) 2017.11.07
UNIX as IDE: 4. Compiling  (0) 2017.11.06
UNIX as IDE: 3. Editing  (0) 2017.11.06