disassemble #1 - how to use gdb?

disassemble을 공부하면서 기본적인 사용법을 적어봅니다.

시작에 앞서..


먼저 assembly language Cpu의 제조사에 따라, 혹은 32bit / 64bit에 따라 그 모습을 달리 합니다.

기본적으로 32bit cpu는 알고 계시듯 메모리를 4GB까지 사용 할 수 있습니다.
2의 23승을 타나내며 값으로는 4,294,967,296 이고,
이는 cpu가 한번에 처리 할 수 있는 값의 범위이며 주소의 범위입니다.

이후 64bit cpu에서는 총 처리할 수 있는 값이 2의 64승으로 늘어났으며, 이는 16엑사바이트 입니다. 1엑사바이트는 1,048,576 테라바이트로 엄청난 숫자임을 이해 할 수 있습니다.

또한 어셈블리에서는 레지스터의 이름도 변하였고, 사용 할 수 있는 레지스터의 수도 증가하였습니다. 이를 감안하여 공부해야 합니다.

기본적인 코드 작성과 gdb 사용



위에 작성한 간단한 프로그램을 gcc를 이용해 컴파일 해줍니다.
(-g옵션을 이용해 디버깅 가능한 컴파일을 합니다.)
gcc -g first.c

그리고 gdb를 이용해 ./a.out를 실행시켜 줍니다.

[root@localhost assem]# gdb ./a.out
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-83.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/assem/a.out...done.
(gdb)

아래 명령어를 통해 break point를 main 함수에 걸어주고, 프로그램을 실행 하도록 합니다.

(gdb) break main
Breakpoint 1 at 0x4004cc: file first.c, line 4.
(gdb) run
Starting program: /root/assem/a.out

Breakpoint 1, main () at first.c:4
4 int i = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.149.el6.x86_64

위의 run에서 현재 4번행 int i = 0;을 가르키고 있는 것을 확인 할 수 있습니다.
이상태로 disassemble해보도록 하겠습니다.

(gdb) disassemble
Dump of assembler code for function main:
     0x00000000004004c4 <+0>: push   rbp
     0x00000000004004c5 <+1>: mov    rbp,rsp
     0x00000000004004c8 <+4>: sub    rsp,0x10
=> 0x00000000004004cc <+8>: mov    DWORD PTR [rbp-0x4],0x0
     0x00000000004004d3 <+15>: jmp    0x4004e3 <main+31>
     0x00000000004004d5 <+17>: mov    edi,0x4005e8
     0x00000000004004da <+22>: call   0x4003b8 <puts@plt>
     0x00000000004004df <+27>: add    DWORD PTR [rbp-0x4],0x1
     0x00000000004004e3 <+31>: cmp    DWORD PTR [rbp-0x4],0x1
     0x00000000004004e7 <+35>: jle    0x4004d5 <main+17>
     0x00000000004004e9 <+37>: leave
     0x00000000004004ea <+38>: ret
End of assembler dump.

위와 같은 모습을 보실 수 있고, 다음에 실행될 위치를 (=>) 가르킵니다.

아래 명령어를 통해 현재 rip 레지스터(Instruction pointer register)를 통해 현재 위치를 확인 하겠습니다.

이 레지스터 RIP(EIP - 32bit)는 이후에 실행될 명령어의 위치를 가르키게 됩니다.

(gdb) info register rip
rip            0x4004cc 0x4004cc <main+8>

위를 통해 현재 rip의 주소는 0x4004cc 임을 확인 할 수 있고,

(gdb) x/i 0x4004cc    혹은 (gdb) x/i $rip 를 통해 현재 위치의 명령어를 확인 할 수 있습니다.

(gdb) x/i 0x4004cc
=> 0x4004cc <main+8>: mov    DWORD PTR [rbp-0x4],0x0

어셈블리어는 기본 명령 <목적지>, <근원지> 로 표현되며, 위 코드에선
명령 - mov
목적지 rbp-0x4
근원지 0x0
이 됩니다.

DWORD PTR은 4 바이트의 값을 읽어들인다는 뜻이 됩니다. (Int형 변수)

먼저 위 행을 실행하기 앞서 rbp에 어떤 값이 들어있는지 조사 해 보도록 하겠습니다.

이는 아래와 같이 표현합니다.

(gdb) x/xw $rbp-4
0x7fffffffe2bc: 0x00000000

현재 rbp의 주소는 0x7fffffffe2bc임을 확인 할 수 있고 내부에는 0이 값이 들어 있는 것을 확인 할 수 있습니다.

x/xw의 앞의 x 는 examine 명령어를 나타내며, 메모리 값을 조사 할 때 사용됩니다.

뒤의 x는 진법을 표시하며 아래 알파벳을 이용 할 수 있습니다.

o - 8진법
x - 16진법
u - Unsigned 10진법
t - 2진법

그리고 마지막 w는 내부 값을 조사 할 때 유효한 크기를 나타냅니다.

b - 단일 바이트
h - 2바이트 (하프워드)
w - 4바이트 (워드)
g - 8바이트 (자이언트)

다시 돌아와 mov명령어는 근원지에 있는 값을 목적지에 집어 넣으라는 이야기 입니다.

nexti를 통해 한 라인을 실행 하도록 하겠습니다.

(gdb) nexti
0x00000000004004d3 6 for(i=0; i<2; i++){
(gdb) x/i $rip
=> 0x4004d3 <main+15>: jmp    0x4004e3 <main+31>

명령어를 확인하기 앞서 앞에서 봤던 mov명령어가 실행된걸 확인 하면,

(gdb) x/xw $rbp-4
0x7fffffffe2bc: 0x00000000

0이 들어가 있는 것을 확인 할 수 있습니다.

이후 x/i $rip를 통해 다음 명령어를 확인 해 보겠습니다.

jmp는 jump명령어로 0x4004e3번지로 jump하라는 명령입니다.

(gdb) x/i 0x4004e3 을 통해 번지를 조사하면
   0x4004e3 <main+31>: cmp    DWORD PTR [rbp-0x4],0x1
위 명령어로 이동 하라는 것을 확인 할 수 있습니다.

(gdb) nexti
0x00000000004004e3 6 for(i=0; i<2; i++){
(gdb) x/i $rip
=> 0x4004e3 <main+31>: cmp    DWORD PTR [rbp-0x4],0x1

cmp는 비교 명령어 입니다. rbp-0x4에 집어넣엇던

0이라는 값과, for문에서 집어넣었던 i<2라는 값을 비교합니다. (작거나 같은지)

(gdb) nexti
0x00000000004004e7 6 for(i=0; i<2; i++){
(gdb) x/i $rip
=> 0x4004e7 <main+35>: jle    0x4004d5 <main+17>

jle는 작거나 같은경우 jump하라는 명령어로 이 경우엔 윗 줄에서 비교했던
$rbp-4가 1보다 작거나 같으면 17라인으로 jump 하라는 명령입니다.

이 경우 $rbp-4의 값이 0이었으므로 17라인으로 이동하게 될 것입니다.
그렇지 않다면
이 코드에서는 점프 없이 다음 명령어로 통과하게 되어 for문을 빠져나오게 됩니다.

우선 다음으로 넘어가 보도록 하겠습니다.

(gdb) nexti
7 printf("Hello\n");
(gdb) x/i $rip
=> 0x4004d5 <main+17>: mov    edi,0x4005e8

예상대로 17라인으로 이동하는 것을 확인 할 수 있습니다.

여기선 EDI 레지스터에 0x4005e8 이라는 값을 집어넣으라는 이야기를 하고 있는데,

EDI(Extended Destination Index) register는 복사 시에 목적지의 주소가 저장되는 index입니다.

0x4005e8의 값을 확인 해 보기 위해 아래 명령어를 확인 해보도록 하겠습니다.

(gdb) x/xw 0x4005e8
0x4005e8 <__dso_handle+8>: 0x6c6c6548

(gdb) x/6b 0x4005e8
0x4005e8 <__dso_handle+8>: 0x48 0x65 0x6c 0x6c 0x6f 0x00

(gdb) x/6db 0x4005e8
0x4005e8 <__dso_handle+8>: 72 101 108 108 111

먼저 첫 번째 실행 결과로 0x6c6c6548 이란 값을 확인 할 수 있습니다.

위를 바이트 단위로 끊어 보게 되면 두 번째 출력값을 확인 할 수 있는데, 이는 꽤나 눈에 익은 HEX값입니다. 다음으로 세번째 출력 x/6db를 통해 6개의 바이트를 10진수로 꺼내 보면

ASCII(아스키코드)값의 범위에 들어가는 것을 보실 수 있습니다.

이를 아래 명령어를 통해 출력해보면

(gdb) x/6c 0x4005e8
0x4005e8 <__dso_handle+8>:   72 'H'   101 'e'    108 'l'   108 'l'   111 'o'   0 '\000'

각각 H e l l o \0 이란 것을 확인 할 수 있습니다.

더 보기좋게 하기위해 아래와 같은 출력 방법이 가능합니다.

x/s 0x4005e8

(gdb) x/s 0x4005e8
0x4005e8 <__dso_handle+8>: "Hello"


그런데 다시 위의 출력값으로 돌아가보면,

(gdb) x/xw 0x4005e8
0x4005e8 <__dso_handle+8>: 0x6c6c6548

(gdb) x/6b 0x4005e8
0x4005e8 <__dso_handle+8>: 0x48 0x65 0x6c 0x6c 0x6f 0x00

여기서 바이트의 역순(Byte-reversal)이 일어난 것을 확인 할 수 있습니다.

이는 프로세서가 값을 little-endian 바이트 순서로 저장하기 때문이며, 꺼내올때 역순으로 꺼내오기 때문입니다. 이는 cpu가 저장공간에 IP address를 저장할 때에도 같은 현상을 보실 수 있습니다.

이후 진행되는 나머지 코드들은 위 내용의 반복입니다.