본문 바로가기

Academy I/Tech Academy

Linux gdb 사용법 [I]

오늘 정리할 내용은 리눅스에서의 gdb 사용법입니다.


gdb (GNU Debugger)는 비주얼 스튜디오의 디버깅 툴과 비교해서 절대 뒤지지 않는 막강한 기능을 제공해 주더군요.

 

간단한 예제로 시작해 보도록 합시다.

 

#include <stdio.h>
#include <assert.h>

 

int main()
{
         int i = 0;
         int j = 0;

    

         for( i = 0; i < 4; ++i ) 
                for( j = 0; j < 4; ++j ) 
                     printf( "%d x %d = %d\n", i, j, i * j );
                
          assert( 0 );

          exit( 0 );

}

 

뭐 특별한 소스는 아닙니다. 그저 루프문을 통해 곱셈 결과를 출력하고 마지막에 어설트 문을 이용하여 강제로 오류 발생시켜 종료합니다. 그럼 이 소스를 토대로 gdb 활용법을 배우도록 합시다.

 

먼저 컴파일 시 디버깅 정보를 삽입하기 위해서 반드시 -g 옵션을 추가하도록 하며 gdb의 인자로 디버깅할 파일 이름을 적어줍니다.

 

 $cc -g -o gdb_test gdb_test.c

 $gdb  gdb_test

 

그럼 gdb 정보와 함께 (gdb) 콘솔 command로 바뀝니다.

일단 gdb상에서 이 프로그램을 돌려봅니다. 명령어는 run 입니다.



프로그램 동작 결과가 나오다가  assert문 때문에  SIGABRT 시그널 메세지와 함께 프로그램이 종료됨을 확인할 수 있습니다.


보통  잘못된 프로그램이 동작 중에 갑자기 죽는 상태와 같겠지요. 우리는 gdb를 통하여 추가적인 시스템 오류 메세지를 확인할 수 있었습니다.  여기에 대한 함수 호출에 대한 스택 추적은 backtrace를 통해 확인 수 있습니다.

 

 

main -> __assert_fail -> abort() -> raise() -> __kernel_vsyscall()  순으로 함수가 호출된 걸 확인할 수 있으며 덩달아 각 함수가 어떤 라이브러리에 속해있는지 확인도 가능합니다. 이로써 우리는 갑자기 죽는 프로그램의 위치를 확인할 수 있으며 이를 통해 수정이 용이하겠지요 :)

 

자, 다음은 브레이크 위치를 설정하고 동작 중인 프로그램의 변수 값을 확인해 보도록 합시다.

 

해당 프로그램의 소스를 보기 위해 list 명령을 입력합니다.

 

 

해당 소스와 함께 라인이 표시가 됩니다. 우리는 이 라인 정보를 이용하여 브레이크 위치를 설정 수 있겠지요.

 

 

실제로 run을 해보면 해당 라인에서 프로그램이 gdb로 제어권이 넘어오는데 우리는 그 틈을 타서 각 변수의 값을 확인할 수 있습니다.


값을 확인하고자 하는 변수는 print를 이용하여 가능합니다.

 

 

물론 여기서 $1은 j 의 값이고 $2는 i의 값입니다. 중단점으로부터 이어서 수행하고 싶으면 cont 를 입력하면 됩니다.  여기서는 for문에  브레이크문을 설정했으니 인덱스가 증가할때마다 중단되겠지요.

 

현재 설정된 브레이크에 대한 정보는 info break로 확인 가능하며 설정한 브레이크 지점은 disable를 통해 해제시킬 수 있습니다.

 

 

여기서 브레이크 해제 지점은 라인 번호가 아니라 설정 순서의 번호가 됩니다. 즉, info로 확인했을 경우 Num의 번호가 되겠지요.


더 자세한 정보는 help breakpoint를 통해 확인하시기 바랍니다.




아직 끝이 아닙니다. 이걸론 충분히 편한 디버깅을 할 수가 없지요.

 

 #include <stdio.h>

 

int main()
{
        int a[10];
        int i; 

 

        for( i = 0; i < 10; ++i ) 
               a[ i ] = i;
 

        exit( 0 );

 

}

 

다음과 같은 코드에서 a 배열과 같은 리스트의 값들을 일일이 확인하려면 print 를 계속 쳐야 하나? 그렇지 않습니다.


display a[0]@갯수 와 같은 방식으로 한번에 값들을 모두 확인수 있습니다.

 

 

@ 뒤의 수는 출력하고자 하는 리스트에서의 갯수가 되겠습니다. 즉 예제에서 10 은 0 ~9까지의 값들을 모두 출력하겠지요.

 

또한 실행 중 동적으로 값을 변경할 수도 있습니다. 명령어는 set variable 변수 = 값 이 되겠습니다.

 

 

a[3]의 값에 7을 대입하니 실제로 값이 바뀐 것을 확인할 수 있습니다. 유휴~

 

 

그럼 과연 이런 명령어 입력을 매번 브레이크 시점마다 해야하는 건가? 정답은 NO!

 

display란 명령어를 이용하면 해결됩니다. display a[0]@10 이란 명령어를 지정하면 매 브레이크 지점마다 a리스트의 값들이 자동으로 출력됩니다. 이건 직접 확인하시길!

 

자 마지막으로, for문과 같이 계속 반복해서 걸리는 브레이크 시점에서 필요한 여러 명령어들을 매번 입력해야 할까?

 

gdb에서는 commands 라는 일종의 입력 묶음 세트가 있는데 이를 이용하면 해결할 수가 있습니다.

commands에 원하는 명령어들을 지정해 두면 해당 브레이크문에서 지정된 명령어들이 모두 실행이 됩니다.

commands 의 인자로 브레이크 Num 을 지정하면 됩니다. (브레이크 Num은 info break로 확인했었죠.)

 

자, 볼까요?

 

 

9 번 라인에 브레이크를 설정해 두었는데 이 브레이크 설정은 두 번째라서 Num 이 2가 됩니다.

따라서 commands 2로 명령어 세트를 설정을 합니다.

 

매번 9라인에서는 set variable[0]=3 과 cont 명령어가 실행될 것이며

 

commands 설정시 입력을 마치려면 end를 입력해 주면 됩니다.


 

-참고: Beginning Linux Programming 4th Edition, ITC, 600-612-     


gdb 활용법을 보강합니다.

 

이번에는 gdb를 이용하여 스택 탐색을 하는 방법을 보도록 하죠.

 

명령어는 다음과 같습니다.

 

     where - 현재 스택 트레이싱 상태를 출력 

     up - 이전 스택 프레임으로 이동

     down  - 다음 스택 프레임으로 이동 

     frame 번호 - 해당 번호의 프레임 정보 출력

     info frame - 스택 프레임 정보 출력

 

덩달아 where full을 입력하면 지역 변수 정보까지 출력되며

up, down 시 up 2 와 같은 식으로 숫자 정보까지 넣어주면 해당 숫자만큼 스택 프레임을 이동합니다.

 

 

예제는 다음과 같습니다.

 

 #include <stdio.h>

 

void b()

{

     int b = 5;

     fprintf( stderr, "%d\n", b );

}

 

void a()

{

     int a = 10;

     fprintf( stderr, "%d\n", a );

     b();

}

 

int main()

{

      fprintf( stderr, "main\n" );

      a();

      return 0;

}



 

-참고 : 열씨미와 게을러의 리눅스 개발 노하우 탐험기, 한빛 미디어, 박재호, 209p~216p-

 


[출처] gdb를 이용한 디버깅 (GNU Debugger)|작성자 Hermet




[GDB 사용 방법]

<<실행>>
GDB를 이용하기 위해서는 컴파일 과정에서 디버깅 정보를 삽입해야 한다.

    컴파일 시 옵션 'g' 이용
    $ gcc -g -o main main.c

컴파일이 정상 종료 되면 GDB를 실행한다.

    gdb [프로그램명]
    $ gdb main
    gdb [프로그램명] [프로세스PID]
    $ gdb main 1928

GDB가 정상 실행되면 터미널의 프롬프트가 (gdb)로 바뀌게 된다.

<<종료>>
종료방법에는 크게 두가지가 있다.

    ctrl + d
    (gdb) q
    (gdb) quit

<<소스보기>>
옵션에 따라 실행중인 프로그램의 소스를 다양한 방법으로 볼 수 있다.

    l(list)
    list 10
    list [함수명]
    list -  //이전 10라인을 출력한다.
    list [파일명]:[함수명]
    list [파일명]:10

list 명령어를 사용하면 소스코드가 10줄 단위로 출력된다.
다음의 명령을 통해 출력단위를 변경할 수 있다.

    set listsize 20

<<세그멘테이션 폴트가 발생했을대>>
컴파일한 프로그램을 실행했을때 segmentation fault 가 발생하여
비정상 종료되었다면 다음의 명령어를 통해 오류 지점을 확인할 수 있다.

    (gdb) r(run)

run 명령어는 GDB가 프로그램을 실행시켜 이상이 발생했을때의 파일과 지점을 출력해준다.
또한 관련 함수 또는 변수에 담긴 값을 출력하여 오류수정에 많은 도움을 준다.

오류 지점에 도달하기 전 과정을 확인하기 위해서는 다음 명령어를 이용하면 된다.

    (gdb) bt

bt명령어는 백트레이스로 프로그램 스택을 역으로 탐색한다.

<<브레이크포인트>>
브레이크포인트는 다음의 방법들을 통해 설정 가능하다.

    (GDB) b(break) [함수명]
    (GDB) break 10
    (GDB) break [파일명]:[함수명]
    (GDB) break [파일명]:10
    (GDB) break +2  //현재 행에서 2개 행 이후 브레이크포인트 설정
    (GDB) break -2  //현재 행에서 2개 행 이전 브레이크포인트 설정
    (GDB) break *0x8049000  //메모리주소에 설정(어셈블리로 디버깅시 이용)
    (GDB) break 10 if var == 0  //var 변수의 값이 0일때 10번 행에 설정

브레이크포인트의 발동 조건은 다양하게 변경 가능하다.

    (GDB) condition [N] var == 0   //var변수가 0일때 N번 브레이크포인트 동작
    (GDB) condition [N] func(i) > 5

현재 설정된 브레이크포인트의 목록은 다음의 명령으로 확인 가능하다.

    (GDB) info break

브레이크포인트는 GDB가 종료될때까지 유효하다.
따라서 필요없을때는 다음의 방법들을 통해 설정을 지운다.

    (GDB) cl(clear) [함수명]
    (GDB) clear 10
    (GDB) clear [파일명]:[함수명]
    (GDB) clear [파일명]:10
    (GDB) d   //모든 브레이크포인트 지움
    (GDB) disable br  //모든 브레이크포인트 비활성화
    (GDB) disable br 1 3  //1번, 3번 브레이크포인트 비활성화
    (GDB) ensable br  //모든 브레이크포인트 활성화
    (GDB) ensable br 1 3  //1번, 3번 브레이크포인트 활성화

<<프로그램 실행>>
프로그램의 실행은 run 명령어를 이용한다.
만일 이미 실행중일때는 재실행한다.

    (gdb) r(run)

프로그램 실행시 인자를 지정하기 위해서는 다음과 같이 이용한다.

    (gdb) run arg1 arg2

실행중인 프로그램을 종료할 때는 kill 명령어를 이용한다.

    (gdb) k(kill)

현재 실행중인 행의 수행을 멈추기 위해서는 step 명령어를 이용한다.
step 명령어는 한행씩 동작하도록 한다. next 명령어와는 함수 호출시 다른 결과를 보인다.

    (gdb) s(step)
    (gdb) step 6   //step을 6번 수행

현재 행의 실행이 멈춘상태에서 다음 행을 실행하기 위해서는

    (gdb) n(next)
    (gdb) next 6   //next를 6번 수행

만일 step명령을 이용중 루프에 빠져 나오지 못할경우에는 until 명령어를 이용한다.

    (gdb) u(until)

한행씩이 아닌 다시 연달아서 실행하기 위해서는

    (gdb) c(continue)

함수가 매우 길어 끝나는 지점으로 이동하기 위해서는 finish 명령어를 사용한다.

    (gdb) finish

함수의 남은 부부을 수행하지 않고 빠져나오기 위해서는 return 명령어를 사용한다.

    (gdb) return

return 명령어를 사용시 return 값을 임의로 지정하기 위해서는 다음과 같이 이용한다.

    (gdb) return 1234

<<와치포인트 설정>>
와치포인트는 변수값의 변화와 코드의 변화를 확인할때 편리하게 이용가능하다.

    (gdb) watch [변수명]   //변수에 값이 써질 때 브레이크
    (gdb) rwatch [변수명]  //변수의 값이 읽혀질 때 브레이크
    (gdb) awatch [변수명]  //변수에 읽기, 쓰기 경우에 브레이크

<<변수와 레지스터 값 검사>>
현재 위치한 행에서 접근 가능한 지역변수들 목록 확인

    (gdb) info locals

현재 위치한 행에서 접근 가능한 전역변수들 목록 확인

    (gdb) info variables

확인하고 싶은 변수의 값을 출력하기 위해서는 print 명령어를 사용한다.

    (gdb) p(print) [변수명]  //변수의 값
    (gdb) print [함수명]   //함수의 주소 값

포인터 변수의 경우 위의 방법으로 하면 주소값만이 출력된다.
포인터 변수의 값 또는 포인터 구조체 등의 값을 보기 위해서는 * 를 붙여준다.

    (gdb) print *[변수명]

이중 포인터라면 ** 를 붙여준다.

GDB는 변수 뿐만 아니라 레지스터의 값도 확인할 수 있다.

    (gdb) print $[레지스터명]

print 명령어는 지역변수를 우선하여 보여주기 때문에
지역변수와 전역변수에서 동일한 이름을 사용할때 전역변수를 확인하기 위해서는 :: 을 이용한다.

    (gdb) print 'main.c'::[변수명]

파일명은 '따옴표' 으로 감싸야한다.

특정 함수에 있는 변수를 확인하기 위해서는

    (gdb) print [함수명]::[변수명]

print 명령어로 변수 또는 레지스터를 확인할 때는 기본적으로 10진수로 출력한다.
이를 다른 형식으로 보고싶을 때는 다음과 같은 방법을 이용한다.

    (gdb) print/t [변수명]    //2진수로
    (gdb) print/o [변수명]    //8진수로
    (gdb) print/d [변수명]    //10진수로 (int)
    (gdb) print/u [변수명]    //부호없는 10진수로 (unsigned int)
    (gdb) print/x [변수명]    //16진수로
    (gdb) print/c [변수명]    //최초 1바이트 값을 문자형으로
    (gdb) print/f [변수명]    //부동소수점값
    (gdb) print/a [변수명]    //가장 가까운 심볼의 오프셋

print 명령어는 값을 보여줄뿐 아니라 값을 설정하는 것도 가능하다.

    (gdb) print [변수명] = [값]

<<화면에 변수의 값을 자동으로 디스플레이하기>>
display 명령어를 이용하면 매 단계가 진행될때마다 자동으로 변수의 값을 출력해준다.

    (gdb) display [변수명]

display 변수를 해제하기 위해서는 undisplay 명령어를 이용한다.

    (gdb) undisplay [N]

display 역시 x,c,o 등등을 이용해 다양한 형태로 출력 가능하다.



[출처 : http://kwanseob.blogspot.kr/2012/03/gdb.html]



IDA/GDB 등으로 프로세스를 attach 시켰을때 특별히 참조할 만한 지점이 없을때 마지막 실행흐름을 따라가는 방법으로는 Stack Frame Trace 기능을 이용하면 된다. 먼저 GDB 의 경우 다음과 같다. ps aux 로 원하는 프로세스의 PID 를 얻고 -p 옵션으로 attach 시킨다.


root@ubuntu:/home/meltdown# gdb -p 7918


...


0xb7764424 in __kernel_vsyscall ()

(gdb) x/10i $eip

=> 0xb7764424 <__kernel_vsyscall+16>: pop    %ebp

   0xb7764425 <__kernel_vsyscall+17>: pop    %edx

   0xb7764426 <__kernel_vsyscall+18>: pop    %ecx

   0xb7764427 <__kernel_vsyscall+19>: ret    

   0xb7764428: add    %ch,(%esi)

   0xb776442a: jae    0xb7764494

   0xb776442c: jae    0xb77644a2

   0xb776442e: jb     0xb77644a4

   0xb7764430: popa   

   0xb7764431: bound  %eax,(%eax)

(gdb) up

#1  0xb7655750 in nanosleep () from /lib/i386-linux-gnu/libc.so.6

(gdb) x/10i $eip

=> 0xb7655750 <nanosleep+32>: mov    %edx,%ebx

   0xb7655752 <nanosleep+34>: cmp    $0xfffff001,%eax

   0xb7655757 <nanosleep+39>: jae    0xb7655789 <nanosleep+89>

   0xb7655759 <nanosleep+41>: ret    

   0xb765575a <nanosleep+42>: call   0xb769ad20

   0xb765575f <nanosleep+47>: push   %eax

   0xb7655760 <nanosleep+48>: mov    %ebx,%edx

   0xb7655762 <nanosleep+50>: mov    0xc(%esp),%ecx

   0xb7655766 <nanosleep+54>: mov    0x8(%esp),%ebx

   0xb765576a <nanosleep+58>: mov    $0xa2,%eax

(gdb) up

#2  0xb7655573 in sleep () from /lib/i386-linux-gnu/libc.so.6

(gdb) x/10i $eip

=> 0xb7655573 <sleep+275>: test   %eax,%eax

   0xb7655575 <sleep+277>: mov    %eax,%edi

   0xb7655577 <sleep+279>: je     0xb7655498 <sleep+56>

   0xb765557d <sleep+285>: test   %edi,%edi

   0xb765557f <sleep+287>: jne    0xb7655590 <sleep+304>

   0xb7655581 <sleep+289>: xor    %eax,%eax

   0xb7655583 <sleep+291>: add    $0x1cc,%esp

   0xb7655589 <sleep+297>: pop    %ebx

   0xb765558a <sleep+298>: pop    %esi

   0xb765558b <sleep+299>: pop    %edi

(gdb) up

#3  0x0804846d in main ()

(gdb) x/10i $eip

=> 0x804846d <main+33>: jmp    0x8048455 <main+9>



VDSO 의 __kernel_vsyscall() 에서 멈추게 되는데, 이때 up 명령과 print $eip 명령을 통해 스택프레임을 조사하고, 원하는 지점을 찾아서 브레이크포인트를 걸면된다.


IDA 의 경우는 attach 하는경우 추가적인 쓰레드가 생성되서 멈추게 되는데, 먼저 오른쪽의 쓰레드 정보창에서 원하는 main 쓰레드를 선택한 뒤에 Ctrl+Alt+S 또는 메뉴에 Stack Trace Window 를 열면 함수 스택 트레이스 정보를 얻을 수 있다.



[출처 : http://daehee87.tistory.com/350]



리눅스에서는 GDB라는 디버거를 이용하여 디버깅을 한다.

 

1. Core dump file 분석

 

코어덤프파일을 생성하기 위해서는 다음과 같이 명령어를 입력하여 설정을 변경해야 한다.

 

$ ulimit -c unlimited

 

$ ulimit -a 를 치면 코어덤프파일의 최대 용량이 0에서 unlimited로 변경된 것을 확인할 수 있다.

 

프로그램 실행 후 비정상적인 종료가 발생하면 core.xxxx 형태로 덤프파일이 생성된다. 덤프파일 생성 위치는 실행파일과 동일 디렉토리에 생성되는데 이는 변경이 가능하다.

 

코어덤프파일을 이용하여 gdb를 실행하려면 다음과 같이 명령어를 입력한다.

 

$ gdb [program file] [core dump file]

 

2. 실행중인 프로세스 분석

 

이미 실행중인 프로그램도 디버깅이 가능하다. 다음과 같이 명령어를 입력하면 된다.

 

$ gdb [program file]

$ attach [process ID]

 

혹은

 

$ gdb [program file] [process ID]





'Academy I > Tech Academy' 카테고리의 다른 글

Linux Shell Programming  (0) 2014.12.22
Linux gdb 사용법 [II]  (0) 2014.12.22
Linux make 사용법 [II]  (0) 2014.12.22
VirtualBox의 Linux(CentOS)에서 공유폴더 설정  (0) 2014.12.22
Linux make 사용법 [I]  (0) 2014.12.18
Unix System Programming 10  (0) 2014.12.16
Unix System Programming 9  (0) 2014.12.16
Unix System Programming 8  (0) 2014.12.16