이 시리즈의 원 저자인 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
훨씬 최근에 만들어진 valgrind
는 gdb
와 비슷한 방법으로 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==
...
프로그램의 정말 엄밀히 검사하기 위해서 gdb
와 valgrind
를 모두 사용할 수도 있다. 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 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 |