프로그램을 동적으로 추적하는 도구 본문

Programming

프로그램을 동적으로 추적하는 도구

halatha 2009. 9. 22. 15:33
출처: 열씨미와 게을러의 리눅스 개발 노하우 탐험기
2009/08/17 - [Programming] - diff, find, md5sum, patch
2009/08/17 - [Programming] - find, grep, ctags, cscope, global
2009/08/17 - [Programming] - shared library, strace, ldconfig, ldd, gcc, strings, od, nm, c++filt, readelf
2009/09/14 - [Programming] - configure
2009/09/21 - [Programming] - 자동화된 빌드 시스템 구축
2009/09/21 - [Programming] - 자동화된 빌드 시스템 구축 (2)
2009/09/21 - [Programming] - 숨겨진 1인치의 의존성을 찾아서 - make
2009/09/22 - [Programming] - 메모리 디버깅을 위한 친구

프로그램을 중단하지 않은 상태에서 정보를 얻는 방법 - source code가 없는 경우
  1. dynamic library를 가로채서 필요한 작업을 수행 후 원래 dynamic library에 포함된 함수를 호출
  2. 역 어셈블
  3. gdb등의 debugger를 이용해 동작 중인 process에 dynamic하게 붙어서 정보를 추출 -> low level system call이나 library 호출 sequence를 알아보기에는 gdb가 제공하는 정보는 추상 단계가 너무 높고, process에 gdb를 붙이는 순간 timing을 놓쳐 정보를 제대로 획득하지 못할 수도 있음
  4. 비 파괴식 디버깅
    1. strace, ltrace: 대상 program을 재컴파일하지 않고 각각 system call, library call을 dynamic하게 추적
    2. lsof, fuser: file과 관련해 program을 모두 뒤지지 않고서도 필요한 정보를 적시에 제공
* 파괴식 검사의 대표적인 예 - printk, printf: UART나 serial 통신이 뚫린 다음부터 kernel이나 application program 양쪽 모두에서 사용할 수 있어, 특히 interactive debugger를 사용하기 어렵거나 제약이 있는 embedded 분야에 많이 사용. libc를 사용하는 일반 application program의 경우 printf 계열, linux kernel이나 kernel module 형식을 따르는 kernel 영역 program의 경우 printk 계열을 이용. 특히 printk를 이용하는 경우 중요도에 따라 filtering이 가능하고 syslog 기능과 연동해 console output이 아니라 file/network으로 redirect도 가능

lsof, fuser
lsof 출력 결과
 열 설명
COMMAND
process name 첫 9글자
PID
process ID
USER
user ID or login name
FD
file descriptor or file type(ex: CWD - current directory, rtd - root directory, txt - program, mem - memory 사상)
TYPE
file과 관련한 node type(ex: BLK - 블록 특수 파일, CHR - 문자 특수 파일, DIR - 디렉토리, REG - 일반 파일)
DEVICE
device number(/dev file에 나타나는 주 번호와 부 번호)
SIZE
file size or offset(byte 단위)
NODE
지역 파일의 node number
NAME
file name
-u option: 특정 user가 띄운 process
그 밖에 network 추적도 가능


strace, ltrace
다른 program이 error number만 출력하고 종료가 되는 경우 source code가 없으면 어떤 조건에 문제가 생겼는지 알기 어렵다. 이 경우 application program이 호출하는 system call을 사용하면 도움이 된다. 단 system call을 많이 사용하지 않고 알고리즘 구현이 대부분인 경우에는 strace가 문제점을 추적할만한 정보가 없다. 즉 kernel과 상호작용이 많은 program인 경우 유용하다. strace로 추적하면 program이 멈춘 지점의 조금 앞부분(system을 끝내기 위한 각종 처리와 오류 메시지 출력 부분 전)에서 수행한 작업을 파악할 수 있어 대략적인 문제 원인과 위치를 찾을 수 있다.

자기 자신을 열고 닫는 program

실행방법: program compile후 [strace | ltrace] [executable file name | -p pid]
compile시 debugging option을 넣지 않아도 된다
strace는 system call을 추적하는 program이므로, kernel 수준에서 동작 과정을 분석한 결과이다. 그래서 strace를 제대로 사용하려면 특정 명령어가 system call로 어떻게 바뀌는지에 대한 감이 있어야 한다.
실행 결과중 중요한 부분은 다음과 같다.

open("/etc/ld.so.cache", O_RDONLY)      = 3
hello는 표준 library인 glibc를 사용하므로 program 실행 과정에서 runtime linker가 glibc dynamic library를 호출하고 있다. 만일 필요한 dynamic library가 없으면 open 결과가 오류로 떨어지므로 strace는 system에서 어떤 경로에 있는 dynamic library를 읽으려고 시도하는지 파악하는 과정에서 위력을 발휘한다. dynamic library call과 관련된 내용은 program source code 내부에서는 결코 보이지 않는 중요하지만 간과하기 쉬운 부분이다.

write(1, "hellow, world!\n"..., 15hellow, world!)     = 15
printf 부분으로 1번 file descriptor(즉, stdout)으로 write(2) system call이 나타난다.

open("hello.c", O_RDONLY)               = 3
close(3)                                = 0
open(2)와 close(2) system call은 각각 fopen(3)과 fclose(3)에 대응된다. hello.c를 열었을 때 반환되는 3이 file descriptor이다. 만일 file open 과정에서 오류가 발생하면 오류 코드가 발생하므로 이를 토대로 문제의 원인(ex: file name error, file path error, file access permit error...)을 분석할 수 있다. file을 열고 닫지 않으면 당연히 이 값은 증가한다.

ltrace는 dynamic linked library에 들어있는 function들만 호출이 가능하다. 즉 object file 내부에 들어있는 function들을 호출하는 경우 ltrace는 추적하지 못한다.
-S option: library call + system call 출력

puts("hellow, world!"hellow, world!)                          = 15
fopen("hello.c", "rw")                          = 0x8253008
fclose(0x8253008)                               = 0
strace의 결과와 달리 system call이 아니라 library function 형태로 나타난다. fopen 결과도 file descriptor가 아니라 FILE* 이다. 즉 library와 1:1로 일치하므로 분석은 더 쉽다. 하지만, shared library를 읽는 작업과 같이 OS에 밀접한 정보가 누락되므로 중요한 정보를 놓칠 수도 있다.

기타
strace 출력
redirection: (strace /bin/ls > /dev/null) 3>&2 2>&1 1>&3- | less
-o option: file로 저장
-ff option: -o option에서 지정한 file name에 pid를 붙여줌

-f option: 자신을 포함해 child process까지 모두 추적. fork(2)와 같은 system call로 만든 child process를 추적하는 경우 필요. system call 앞에 pid 정보가 붙으므로 이 정보를 사용해 process를 구분할 수 있음

-t option: 출력에 timestamp 추가
-tt option: 하루 중 시각을 msec 단위로 출력
-ttt option: epoch 이후 시각을 msec 단위로 출력
-r option: system call 사이에 흐른 시각 표시
* 주의: 여기서 찍힌 timestamp는 모두 system call에 들어간 시점이므로, system call 사이에 scheduling이 걸리거나 user code가 수행될 경우에는 부정확한 결과를 얻을지도 모른다. 이 경우 실제 system call에 걸린 시간을 표시하는 -T option이나 표로 요약해서 system call 누적 시간을 보여주는 -c option을 활용

원리
strace는 ptrace(2)라는 debugging용 system call interface를 이용해 system call을 추적. kernel에서 제공하는 system call은 개념상으로 일반적인 function call과 유사하지만 system call을 한 시점에서 kernel mode로 전환해서 실제 system call routine을 실행한다는 중요한 차이점이 있다. 이렇게 mode 전환이 가능해야 하므로 x86 linux에서는 system call을 software interrupt로 구현하고 있다. 예를 들어 x86 linux 환경에서 돌아가는 glibc에 open(2) system call은 다음과 같다.
push %ebx
mov 0x10(%esp,1),%edx
mov 0xc(%esp,1),%ecx
mov 0x8(%esp,1),%ebx
mov $0x5,%eax
int $0x80
pop %ebx
강조된 부분이 5번(즉 open system call인 __NR_open)을 %eax register에 넣고 software interrupt command인 int로 0x80을 호출해 현재 실행 thread를 kernel로 전환한다. 비록 open(2) function이 C library에 들어있지만 실제 구현은 kernel에 떠넘긴 형태이므로, C library function은 open system call에 대한 wrapper라고 보면 된다. 응용 프로그램 사이의 호출 규약을 정의한 ABI(Application Binary Interface)와 마찬가지로 system call에도 내부적인 규약이 있는데, x86 linux에서 사용하는 기본 규약은 다음과 같다.
매개변수 순서 1~6은 순서대로 register number %ebx, %ecx, %edx, %esi, %edi, %ebp
앞서 open(2) system call 구현부에 등장한 다음 명령들은 결국 매개 변수를 전달하는 작용을 한다(open(2) system call의 매개변수가 3개이다).
mov 0x10(%esp,1),%edx
mov 0xc(%esp,1),%ecx
mov 0x8(%esp,1),%ebx
결과값은 x86 ABI와 유사하게 %eax에 넘어오는데, 음수일 경우 절대값을 씌운 errno 값에 대입된다. system call에 넘어가는 매개변수가 최대 6개라는 점을 기억하자.
kernel 입장에서 strace를 보면, strace는 ptrace라는 debugging 전용 kernel interface를 사용해 program이 system call에 진입하는 시점과 빠져 나오는 시점에서 program 실행을 멈춘다. ptrace는 process가 각 system call 진입점과 종료점에 멈추도록 지원하며, process에게 system call 종류, system call에 넘어가는 매개변수, 반환값과 같은 추가적인 정보도 넘겨준다.
strace에서 핵심적인 기능을 수행하는 kernel단 debugging 지원 interface인 ptrace(2) 서식은 다음과 같다. 여기서 중요한 매개변수는 request인데, child process를 대상으로 어떤 작업을 수행할지 결정한다.
#include <sys/ptrace.h>
long int ptrace(enum __ptrace_request request, pid_t pid, void* addr, void* data)

첫 번째 매개변수인 request의 값은 다음과 같다
 명령 설명
비고
PTRACE_TRACEME
부모 프로세스가 자식 프로세스 통제를 시작, SIGKILL이외 다른 singal을 받으면 기다리는 부모 프로세스에게 SIGTRAP을 걸어 전달
부모 프로세스가 준비되기 전에 호출하면 안됨
PTRACE_ATTACH
프로세스 추적을 위해 해당 프로세스에 대한 부모 프로세스로 결합
 
PTRACE_KILL
자식 프로세스에게 SIGKILL을 보내서 중단하도록 만듦
 
PTRACE_PEEKDATA,
PTRACE_PEEKTEXT
자식 메모리에서 워드를 읽어서 내용을 반환
워드 크기는 OS마다 다름
PTRACE_POKEDATA,
PTRACE_POKETEXT
자식 메모리에 워드를 기록
워드 크기는 OS마다 다름
PTRACE_CONT
정지 상태에 있는 자식 프로세스를 계속 실행하도록 만듦
 
PTRACE_SINGLESTEP
프로세서 트랩 플래그를 켜고 자식 프로세스를 계속 실행하도록 만듦
PTRACE_CONT와 다른 점은 기계어 명령 하나만 실행하고 디버깅 인터럽트를 일으킴
PTRACE_SYSCALL
PTRACE_SINGLESTEP과 동일
PTRACE_SINGLESTEP과 달리 시스템 호출을 대상으로 함
PTRACE_DETACH
PTRACE_ATTACH로 붙은 프로세스를 분리
 
PTRACE_GETREGS,
PTRACE_GETFPREGS
자식 프로세스의 일반 레지스터와 부동소수점 레지스터 내용을 읽음
 
PTRACE_SETREGS,
PTRACE_SETFPREGS
자식 프로세스의 일반 레지스터와 부동소수점 레지스터 내용을 설정
 

다음은 strace를 구성하는 가장 핵심적인 부분이다
//    자식 프로세스에게 시스템 호출 진입이나 종료 시점에서 멈춰 달라고 요청
ptrace(PTRACE_SYSCALL, stopped_pid, 0, 0);

//    핵심 추적 루프: 자식 프로세스가 멈출 때, 시스템 호출 번호와 매개 변수 검사
while ((stopped_pid = waitpid(traced_pid, &status, 0) != -1) {
        //    자식 프로세스 레지스터 값 읽기:
        //    %eax에 시스템 호출 번호를 넘기고 %eax로 결과를 받는다는 사실을 기억하자
        result = ptrace(PTRACE_GETREGS, stopped_pid, 0, &registers);

        if ( registers.eax = -ENOSYS ) {    //    시스템 호출 진입
            //    시스템 호출 번호 출력
            fprintf(stderr, "%d: syscall_%3d( ", stopped_pid, registers.orig_eax);
            //    시스템 호출 매개 변수 출력
            switch ( registers.orig_eax ) {
                case __NR_open:
                    //    open system call: 좀더 이해하기 쉽도록 "file name"을 출력해야 한다
                    break;
                case __NR_exit:
                    //    추적 중인 process가 끝나면 같이 종료
                    fprintf(stderr, "%#08x, %#08x, *#08x ) = ?\n",
                            registers.ebx, registers.ecx, registers.edx);
                    goto exit;
                    break;
                default:
                    //    기타 system call 처리(편의상 매개 변수 3개만 출력)
                    fprintf(stderr, "%#08x, %#08x, *#08x ) = ?\n",
                            registers.ebx, registers.ecx, registers.edx);
                    break;
            }
            fprintf(stderr, " ) = ");
        }
        else {    //    system call 종료
            if ( registers.eax < 0 ) {    // error condition
                fprintf(stderr, "errno: %d\n", registers.eax);
            }
            else {    //    normal result
                fprintf(stderr, "result: %d\n", registers.eax);
            }
        }
        //    계속해서 자식 프로세스에게 시스템 호출 진입이나 종료 시점에서 멈춰 달라고 요청
        ptrace(PTRACE_SYSCALL, stopped_pid, 0, 0);
}

exit:

ltrace의 동작 원리도 strace와 유사한데, 추적 대상 program을 읽어서 function symbol name과 PLT 내부 주소를 얻은 다음에 ptrace(2) system call을 사용해서 추적 기능을 활성화한 다음에 자식 프로세스를 exec로 실행한다. 그리고 wait에서 기다리는 부모(strace)에 SIGTRAP이 전달되면, 앞서 얻은 function symbol name과 PLT 내부 주소 정보를 사용해서 각 함수 PLT 내부 주소에 중단점을 걸어버린다. 결국 해당 함수를 호출할 때마다 SIGTRAP이 발생하므로 library call에 대해 dynamic하게 추적이 가능해진다.

결론
  1. debugging 과정에서 정보를 얻는 방법에는 파괴식/비파괴식 검사가 있다. 파괴식 검사는 source code를 수정해 직접 필요한 정보를 얻는 방식이고, 비파괴식 검사는 program을 수행하는 도중에 일어나는 각종 움직임을 외부에서 동적으로 파악하는 방식이다.
  2. linux에는 비파괴식 검사를 지원하는 다양한 utility가 존재한다. 대표적인 것이 system call을 추적하는 strace와 library call을 추적하는 ltrace이다.
  3. file을 어느 사용자가 열고 있는지 확인하려면 lsof와 fuser를 사용한다.
  4. strace는 program이 kernel과 상호 작용하는 모든 system call 내역을 보여준다. program 규모가 커지면 strace 결과가 많아지므로, filtering해서 해석하는 방법을 숙지해야 한다. 또한 system call을 많이 사용하지 않고 알고리즘 위주로 작성한 program의 경우에는 strace로 추적해도 별 소용이 없다.
  5. ltrace는 strace보다 한 단게 추상 수준을 높인 도구로 library call을 보여주지만 dynamic library link에 대해서만 추적이 가능하므로 object file 내부에서 이뤄지는 자체적인 호출은 추적 불가능하다.
  6. strace와 ltrace 모두 ptrace(2)라는 kernel에서 제공하는 debugging interface용 system call을 사용한다.
참고
리눅스 문제 분석과 해결, 박재호/이재영 역, 에이콘 출판사 2006년: 2장 - strace, system call
리눅스 디버깅과 성능 튜닝, 박재호/이재영 역, 에이콘 출판사 2006년: 6장 - strace, lsof등
Binary hacks: 해커가 전수하는 테크닉 100선, 진명조 역, ITC 2007년: 82, 83번 항목 - strace, ltrace
월간 임베디드 월드 연재 기사 '리눅스 개발자를 위한 디버깅 기법: 1회 리눅스 디버깅 개괄', 2006년
lsof: http://en.wikipedia.org/wiki/Lsof
lsof tutorial: http://www.linux.com/archive/articles/114089
lsof manual: http://linux.die.net/man/8/lsof
fuser manual: http://linux.die.net/man/1/fuser
strace manual: http://linux.die.net/man/1/strace
ltrace manual: http://linux.die.net/man/1/ltrace
OS별 strace와 유사한 명령 소개: http://support.zeus.com/zws/faqs/2005/09/19/what_is_truce_or_strace
하이젠버그 소개: http://en.wikipedia.org/wiki/Heisenbug#Heisenbug
ptrace manual: http://linux.die.net/man/2/ptrace
ptrace system call 설명: http://www.osweekly.com/index.php?option=com_content&task=view&id=2230
ptrace 활용 예제 소개: http://www.linuxjournal.com/article/6100 http://www.linuxjournal.com/article/6210
Comments