공부

Linux GDB 사용 및 메모리 맵 그리기

푸쥬 ! 2013. 4. 8. 20:43
반응형

GDB 간단한 명령어.

 

※ b

breakpoint 지정.

b *main+0 -> 해당 포인터에 breakpoint

 

※ info

정보를 확인.

info reg -> 레지스트리 확인

info b -> breakpoint 확인

 

※ cl, del

breakpoint 삭제.

cl *main+39 -> 해당 포인터에 걸려있는 breakpoint 삭제

del [num] -> 해당 번호의 breakpoint 삭제

del 을 이용한 삭제 시, [num] 의 정보는 info b 에서 확인 가능

 

※ ni

다음 어셈블리 실행. 함수를 실행하지 않음.(printf 같은 함수 무시)

윈도우 C 디버그 시 F10 같은 명령어

 

※ si

다음 어셈블리 실행. 함수를 실행함.(printf 같은 함수로 들어감)

윈도우 C 디버그 시 F11 같은 명령어

 

※ c

continue. 다음 breakpoint 까지 실행

다음 breakpoint 가 없을 경우 전부 다 실행

 

※ finish

함수를 종료

 

※ p, x/x x/s

변수 출력

x/x 주소 -> 해당 주소의 값 출력

x/8x 주소 -> 해당 주소의 값부터 8개까지 출력

x/s 주소 -> 해당 주소의 값 스트링 형태로 출력

p 주소 -> $1, $2, $3 와 같은 형식으로 해당 주소 저장

 

※ display

어셈블리가 실행될 때 마다 설정한 값을 보여줌

display $esp -> 어셈블리 실행마다 %esp 를 보여줌

 

※ r

실행.

r 1 2 3 ... -> 실행하되 뒤에 인자 값으로 argv 삽입 가능

 

 

※ Sample Code

 

#include <stdio.h>
int main(int argc, char **argv)
{
    int i = 0;
    printf("argc : %d \n", argc);
    for(i=0; i<argc; i++)
    {
        printf("argv[%d] : %s\n", i, argv[i]);
    }
}

 

 

※ Disassemble main

 

0x08048328 <main+0>: push   %ebp
0x08048329 <main+1>: mov    %esp,%ebp
0x0804832b <main+3>: sub    $0x8,%esp
0x0804832e <main+6>: and    $0xfffffff0,%esp
0x08048331 <main+9>: mov    $0x0,%eax
0x08048336 <main+14>: sub    %eax,%esp
0x08048338 <main+16>: movl   $0x0,0xfffffffc(%ebp)
0x0804833f <main+23>: sub    $0x8,%esp
0x08048342 <main+26>: pushl  0x8(%ebp)
0x08048345 <main+29>: push   $0x804843c
0x0804834a <main+34>: call   0x8048268 <printf>
0x0804834f <main+39>: add    $0x10,%esp
0x08048352 <main+42>: movl   $0x0,0xfffffffc(%ebp)
0x08048359 <main+49>: mov    0xfffffffc(%ebp),%eax
0x0804835c <main+52>: cmp    0x8(%ebp),%eax
0x0804835f <main+55>: jl     0x8048363 <main+59>
0x08048361 <main+57>: jmp    0x804838d <main+101>
0x08048363 <main+59>: sub    $0x4,%esp
0x08048366 <main+62>: mov    0xfffffffc(%ebp),%eax
0x08048369 <main+65>: lea    0x0(,%eax,4),%edx
0x08048370 <main+72>: mov    0xc(%ebp),%eax
0x08048373 <main+75>: pushl  (%eax,%edx,1)
0x08048376 <main+78>: pushl  0xfffffffc(%ebp)
0x08048379 <main+81>: push   $0x8048448
0x0804837e <main+86>: call   0x8048268 <printf>
0x08048383 <main+91>: add    $0x10,%esp
0x08048386 <main+94>: lea    0xfffffffc(%ebp),%eax
0x08048389 <main+97>: incl   (%eax)
0x0804838b <main+99>: jmp    0x8048359 <main+49>
0x0804838d <main+101>: leave 
0x0804838e <main+102>: ret   
0x0804838f <main+103>: nop

 

 

※ 메모리 맵 그리기

 

[********@bulpae system]$ !g
gdb -q test.e

(gdb) b *main+0

Breakpoint 1 at 0x8048328


(gdb) display $ebp
(gdb) display $esp

 

main 어셈블리가 시작되기 전에 breakpoint 를 걸어 놓는다. display 옵션을 통해서 ebp의 변화와 esp의 변화를 확인할 수 있다.

 

(gdb) r hi hello test

Starting program: /home/hyeonkon/2013/system/test.e hi hello test

Breakpoint 1, 0x08048328 in main ()
2: $esp = (void *) 0xbffff89c
1: $ebp = (void *) 0xbffff8b8

 

(gdb) si
0x08048329 in main ()
2: $esp = (void *) 0xbffff898
1: $ebp = (void *) 0xbffff8b8


 

(gdb) x/8x $esp
0xbffff898: 0xbffff8b8 0x42015574 0x00000004 0xbffff8e4
0xbffff8a8: 0xbffff8f8 0x4001582c 0x00000004 0x08048278

 

 

(gdb) si
0x0804832b in main ()
2: $esp = (void *) 0xbffff898
1: $ebp = (void *) 0xbffff898

 


(gdb) si
0x0804832e in main ()
2: $esp = (void *) 0xbffff890
1: $ebp = (void *) 0xbffff898

 

r 명령어와 함게 argv 값을 만든 후 프로그램을 디버깅한다.

info reg 명령어를 이용하여 전체 레지스트리의 변화도 확인할 수 있다.

main 의 disassemble 두 줄을 확인해보면, 아래와 같은 것을 확인할 수 있다.

 

0x08048328 <main+0>: push %ebp -------------①

0x08048329 <main+1>: mov %esp,%ebp---------②

0x0804832b <main+3>: sub $0x8,%esp----------③

 

① 0x08048328 라인 시작 전에 esp 의 주소 값은 0xbffff89c 이다. main 함수도 함수이기 때문에, call 명령으로 인해 진입하게 된다. 이때 call 명령에서는 "push eip, jmp 주소" 가 수행되고, 이 때문에 main+0 이 수행되기 전에는 메인함수가 모두 종료 된 뒤 돌아갈 주소가 필요하다. 시작 전 현재 esp 주소에 위치한 것이 이 주소를 뜻하고, 이를 통상적으로 ret 라 부른다. 이 라인에서 push ebp 를 하기 때문에 주소 값이 4 byte 만큼 내려간다. 이는 si 를 통해 첫 라인을 실행했을 때 esp 의 주소 값을 통해 확인할 수 있다.

 

② 0x08048329 에서 move %esp, %ebp 를 하기 때문에 ebp 의 주소 값이 esp 와 같아진다는 것을 알 수 있다. si 를 통해 해당 라인을 실행했을 때 알 수 있다. (0xbffff898 로 동일)

 

③ 0x0804832b 에서 sub $0x8, %esp 를 하기 때문에 esp 의 주소 값이 0x8 byte 만큼 내려간다는 것을 알 수 있다. 이는 역시 si 명령으로 해당 라인을 실행했을 때 알 수 있다.

 

현재까지의 메모리 맵을 그리면 아래와 같다고 할 수 있다.

 

 

 

0x0804832e <main+6>: and $0xfffffff0,%esp-------------①
0x08048331 <main+9>: mov $0x0,%eax-----------------②
0x08048336 <main+14>: sub %eax,%esp
0x08048338 <main+16>: movl $0x0,0xfffffffc(%ebp)
0x0804833f <main+23>: sub $0x8,%esp-----------------③
0x08048342 <main+26>: pushl 0x8(%ebp)

 

①0x0804832e 에서 and 연산을 하고 있는데, 이 연산은 맨 우측 자리를 0 으로 만들기 위한 연산이다. %esp 의 값을 16바이트 정렬하기 위해 사용되는 연산이라 할 수 있다.

 

②0x08048331 에서 eax 값을 0 으로 만들고 있고, 0x08048338 에서 0xfffffffc(%ebp) 만큼을 0x0 으로 movl 하고 있다. 이 의미는 ebp 에서 -4 만큼의 위치를 0 으로 채우겠다는 의미고, 코드에서는 int value = 0 과 같은 것으로 해석할 수 있다.

 

③0x0804833f 에서 현재 esp 를 8 만큼 sub 하고, 0x08048342 에서 0x8(%ebp) 를 pushl 해주고 있다. 0x8(%ebp) 의 의미는 ebp+8 이 가리키고 있는 값이고, push 이기 때문에 현재 esp 를 기준으로 값이 채워진다. 계산해보면 ebp+8 이 가리키는 값은 argc 값이다.

 

현재까지의 메모리 맵을 그리면 아래와 같다고 할 수 있다.

 

 

 

0x08048345 <main+29>: push $0x804843c
0x0804834a <main+34>: call 0x8048268 <printf>
0x0804834f <main+39>: add $0x10,%esp
0x08048352 <main+42>: movl $0x0,0xfffffffc(%ebp)

 

0x08048345 에서 문자열을 push 한 후 printf 를 호출한다.

이후 0x0804834f 에서 add 를 이용하여 esp 의 주소 값을 0x10 byte 만큼 올린다.

이 작업을 거치면 esp 는 0xbffff890 이 된다.

 

0x08048352 에서 movl 로 ebp 에서 -4 만큼 떨어진 곳에 0 을 복사한다.

반복문의 시작에서 i=0 을 의미한다.

 

0x08048359 <main+49>: mov 0xfffffffc(%ebp),%eax----------①
0x0804835c <main+52>: cmp 0x8(%ebp),%eax--------------②
0x0804835f <main+55>: jl 0x8048363 <main+59>
0x08048361 <main+57>: jmp 0x804838d <main+101>
0x08048363 <main+59>: sub $0x4,%esp--------------------③
0x08048366 <main+62>: mov 0xfffffffc(%ebp),%eax
0x08048369 <main+65>: lea 0x0(,%eax,4),%edx-------------④
0x08048370 <main+72>: mov 0xc(%ebp),%eax--------------⑤
0x08048373 <main+75>: pushl (%eax,%edx,1)
0x08048376 <main+78>: pushl 0xfffffffc(%ebp)
0x08048379 <main+81>: push $0x8048448
0x0804837e <main+86>: call 0x8048268 <printf>

 

이번 구문은 jmp 구문과 main+49 같은 구문이 보이기 때문에 분기문 or 반복문이 될 가능성이 높다고 생각했다.

 

① 0x08048359 에서 mov 를 하고 있는데. eax 에 ebp 에서 -4 만큼 떨어진 곳의 값을 대입하고 있다. 이 값은 argc 값이라 생각된다.

 

② 0x0804835c 에서 eax 값과 ebp 에서 8 만큼 떨어진 값과 비교를 하고 있다. cmp 는 왼쪽 값에서 오른쪽 값을 sub, 즉 뺀 값을 0과 비교하는 것이다. jl (jump lower) 이기 때문에 0 - 4 = -4 이 나오고, -4 < 0 이기 때문에 jl 에 해당된다. 따라서 <main+59> 로 가게 된다.

 

③ main+59 (0x08048363) 에서 esp 를 4 byte 만큼 낮추고, 그 후 eax 값을 ebp 에서 -4 만큼 떨어진 곳의 값(0)을 넣었다. 현재 esp 는 0xbffff88c 다.

 

④ 0x08048369 에서 lea 명령어가 있는데, lea 는 주소 값을 다룬다. 0x0([para1],%eax,4) 라는 구문이 있는데, 보통 이런 경우는 0x0 + para1 + eax * 4 라고 해석하면 된다. 이 경우에 적용되는 수식은 0x0 + 0x0 + 0x0 *4 가 되기 때문에 결국 0x0이 되고, 이 주소를 edx 에 넣는다.

 

⑤ 0x08048370 에서 eax 값을 ebp 에서 0xc 만큼 떨어진 곳의 값으로 복사한다. 이 값은 argv[0] 라 생각된다. 이후 pushl 명령으로 (%eax, %edx, 1) 를 하고, pushl 0xfffffffc(%ebp) 를 하는데 하는데, 우선 후자는 반복문에 이용되는 변수를 뜻하는 것으로 생각된다.

 

선자의 경우는 eax + edx * 1 로 생각하면 된다. eax 주소 값은 0x0c(%ebp) 이고 나머지는 0 이기 때문에 최종적으로 주소 값은 0xbffff8a4 가 된다. printf 에 %s 포맷의 인자 값으로 생각되고, 이미 C 소스를 알고 있기 때문에 디버깅을 통해 확인해보겠다.

(디버깅을 보통 새로 하기 때문에, 주소 값 자체는 달라질 수 있다.)

 

※ 코드

printf("argv[%d] : %s\n", i, argv[i]);

 

(gdb) info reg
eax            0xbffff464 -1073744796
ecx            0x4212ee20 1108536864
edx            0x0 0
ebx            0x42130a14 1108544020
esp            0xbffff40c 0xbffff40c
ebp            0xbffff418 0xbffff418
esi            0x40015360 1073828704
edi            0x80483c0 134513600
eip            0x8048373 0x8048373
eflags         0x396 918
cs             0x23 35
ss             0x2b 43
ds             0x2b 43
es             0x2b 43
fs             0x0 0
gs             0x33 51


 

(gdb) x/x 0xbffff424
0xbffff424: 0xbffff464

 

(gdb) x/x 0xbffff464
0xbffff464: 0xbffffb67

 

(gdb) x/s 0xbffffb67
0xbffffb67:  "/home/hyeonkon/2013/system/test.e"

 

argv 는 이중 포인터이기 때문에 2번 값을 찾아가야 한다. 이렇게 찾아가면 eax 에 들어있는 값이 문자열 임을 알 수 있다.

 

push 후 printf 구문이 나오는 것으로보아 printf 포맷의 인자 값으로 생각된다. push 3번으로 인해 현재 esp 는 0xbffff880 이다.

 

현재까지의 메모리 맵을 그리면 아래와 같다고 할 수 있다.

 

 

 

0x08048383 <main+91>: add $0x10,%esp-----------------①
0x08048386 <main+94>: lea 0xfffffffc(%ebp),%eax----------②
0x08048389 <main+97>: incl (%eax)
0x0804838b <main+99>: jmp 0x8048359 <main+49>
0x0804838d <main+101>: leave
0x0804838e <main+102>: ret
0x0804838f <main+103>: nop

-------------------------------------------------------------------------------- 

0x08048359 <main+49>: mov 0xfffffffc(%ebp),%eax
0x0804835c <main+52>: cmp 0x8(%ebp),%eax------------③

0x0804835f <main+55>: jl 0x8048363 <main+59>
0x08048361 <main+57>: jmp 0x804838d <main+101>

 

① printf 출력이 끝난 후, 0x08048383 에서 add 를 통해 esp 주소를 0x10 만큼 낮추고 있다. 현재 esp 는 0xbffff890 이다.

 

② 0x08048386 에서 eax 를 ebp 에서 -4 만큼 떨어진 곳의 주소 값으로 채우고 있다. 그리고 바로 다음에 eax 가 가리키는 곳의 값을 1 증가시킨다. 이후 jmp 문을 통해 다시 main+49 로 간다.

 

③ 0x0804835c(main+52) 에서 cmp 구문을 통해 jmp 할 방향을 찾는는다. cmd 결과 값이 음수이거나 0 이면 main+49 로 jmp 하고, 양수일 경우엔 main+101 로 jmp 한다. main+101 로 jmp 할 경우 leave 로 main 함수가 종료되고 ret 를 호출한 후 프로그램이 종료된다.

 

간단한 프로그램이지만 어셈블리로 변환하니 양이 꽤 많았다.

포기했다가 다시 공부하니 재밌기도한데 어려워서 포스팅하는데 꽤 시간이 걸렸다.

많이 해보고 익숙해지는 것이 답인듯 하다.

728x90