귀찬으니 정리만 한다.
#include <stdio.h> int main(){ int i; for(i = 0; i < 10; i++){ printf("Hello, World!\n"); } return 0; }
간단히 hello world를 10번 출력하는 예제이다.
gcc helloword.c
와같이 컴파일 하게되면 아시다 시피 바이너리로 변해 컴퓨터가 해석을 위해 변환 되게 된다.
이를 기계어로 변하게 하는데 이를 볼 수 있게 해주는 objdump
명령어를 통해서 자세히 봐보자.
Command : objdump -D a.out | grep -A20 main.:
위와 같이 입력하면, main 함수의 부분에서 20줄을 보여주게 된다. 실제론 이보다 훠~얼~씬 길지만..
0000000000400526 <main>: 400526: 55 push %rbp 400527: 48 89 e5 mov %rsp,%rbp 40052a: 48 83 ec 10 sub $0x10,%rsp 40052e: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 400535: eb 0e jmp 400545 <main+0x1f> 400537: bf e4 05 40 00 mov $0x4005e4,%edi 40053c: e8 bf fe ff ff callq 400400 <[email protected]> 400541: 83 45 fc 01 addl $0x1,-0x4(%rbp) 400545: 83 7d fc 09 cmpl $0x9,-0x4(%rbp) 400549: 7e ec jle 400537 <main+0x11> 40054b: b8 00 00 00 00 mov $0x0,%eax 400550: c9 leaveq 400551: c3 retq 400552: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 400559: 00 00 00 40055c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000400560 <__libc_csu_init>: 400560: 41 57 push %r15 400562: 41 56 push %r14
각각 열이 의미하는 바에 대해 설명하자면,
제일 처음의 400526:
은 메모리 주소를 의미하고 2번째 열은 3~4번째의 어셈블리 코드를 의미한다.
++ rbp ebp?? rex ebx??
위 두개는 같은 것을 의미하며, 현재 실행중인 컴퓨터의 환경(x64 or x86)의 따라서 r(x64) 또는 e(x86)이 붙게 된다.
위에 코드는 AT&T 문법 인데, 이를 Intel 문법으로 바꾸기 위해선 objdump
에서 -M intel
옵션을 추가해주면 아래와 같은 코드를 볼 수 있다.
0000000000400526 <main>: 400526: 55 push rbp 400527: 48 89 e5 mov rbp,rsp 40052a: 48 83 ec 10 sub rsp,0x10 40052e: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 400535: eb 0e jmp 400545 <main+0x1f> 400537: bf e4 05 40 00 mov edi,0x4005e4 40053c: e8 bf fe ff ff call 400400 <[email protected]> 400541: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 400545: 83 7d fc 09 cmp DWORD PTR [rbp-0x4],0x9 400549: 7e ec jle 400537 <main+0x11> 40054b: b8 00 00 00 00 mov eax,0x0 400550: c9 leave 400551: c3 ret 400552: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 400559: 00 00 00 40055c: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 0000000000400560 <__libc_csu_init>: 400560: 41 57 push r15 400562: 41 56 push r14
x86 Debugging
GDB라는 디버깅 도구를 이용해 프로세서 레지스터의 상태를 볼 수 있다.
Command : gdb -q ./a.out
Reading symbols from ./a.out...(no debugging symbols found)...done. (gdb) break main Breakpoint 1 at 0x40052a (gdb) run Starting program: /home/silnex/hack/a.out Breakpoint 1, 0x000000000040052a in main () (gdb) info registers rax 0x400526 4195622 rbx 0x0 0 rcx 0x0 0 rdx 0x7ffffffde448 140737488217160 rsi 0x7ffffffde438 140737488217144 rdi 0x1 1 rbp 0x7ffffffde350 0x7ffffffde350 rsp 0x7ffffffde350 0x7ffffffde350 r8 0x4005d0 4195792 r9 0x7fffff410ab0 140737475840688 r10 0x846 2118 r11 0x7fffff050740 140737471907648 r12 0x400430 4195376 r13 0x7ffffffde430 140737488217136 r14 0x0 0 r15 0x0 0 rip 0x40052a 0x40052a <main+4> eflags 0x246 [ PF ZF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 (gdb) quit A debugging session is active. Inferior 1 [process 565] will be killed. Quit anyway? (y or n) y
중지점(breakpoint)이 main()
함수에 설정 되어 실행 되기전에 멈추었고, info registers
를 통해 실행중에 사용중인 레지스터의 내용들을 볼 수 있다.
첫번째 부터 4개의 레지스터 (eax, ecx, edx, ebx)는 범용 레지스터이고 각각 누산기, 카운터, 데이터, 베이스 레스터라고 부른다.
[위에서는 rax, rcx, rdx, rbx 라고 나왔지만 이는 실행 중인 환경이 x64여서 r이 붙은것일 뿐 의미하는 바는 같다.]
그 다음부터 4개의 (esp, ebp, esi, edi)도 범용 레지스터 이다. 이 레지스터는 가끔 포인터(pointer)나 인덱스(index)라고 부르기도 한다.
각기 스택 포인터, 베이스 포인터, 근원지(source) 인덱스, 목적지(destination) 인덱스라고 불리운다.
위의 레지스터들은 프로그램을 실행하고 메모리를 관리하는데 쓰이는 포인터 이며 매우 중요하게 사용된다.
eip 레지스터는 현재 프로세서가 읽고 있는 명령 포인터 레지스터 이고, 당현히 디버깅시 매우 많이 사용되게 된다.
마지막으로 eflags레지스터는 비교와 메모리 분할을 위한 몇 비트의 플래그로 이루어져있다. 이에 대해선 나중에 알아보자.
어셈블리 언어
대게 인텔의 문법 어셈블리어를 많이 사용하므로 (앞으로도 많이쓰게될진 모르겟지만..) 디버깅시 사용하는 툴도 인텔 문법에 맞춰 사용하기 위해서
set disassembly-flavor intel
이라고 입력해 역어셈블 표기를 인텔로 정할 수 도 있으며,
명령을 넣은.gdbinit
파일을 생성해 GDB를 실행할 때 마다 자동으로 환경설정 되도록 할 수 도 있다.
인텔 어셈블리어는 다음과 같은 형식으로 이루어져 있다.
명령 <목적지>, <근원지>
목적지
와 근원지
는 레스터나 메모리 주소 값이 될 수 있고 명령
은 보통 직관적인 연산기호 이다.
mov명령은 근원지에서 목적지로 값을 이동,
sub는 빼고, inc는 증가시킨다.
예를 들어 다음 명령은 esp에서 ebp로 이동시킨 후 8을 뺀 결과를 esp에 저장하라는 명령이다.
8048375: 89 e5 mov ebp,esp
8048377: 83 ec 08 sub esp,0x8
또한 실행의 흐름을 제어하는 명령도 있다.
cmp 명령은 값을 비교하는데 사용되고,
j로 시작하는 명령은 코드를 다음부분으로 점프하는데 사용된다.
만약 gdb에서 소스코드를 볼 수 있는 추가 정보를 포함시키려면, 컴파일 시 gcc -g 플레그를 넣으면 된다.
Reading symbols from a.out...done. (gdb) list 1 #include <stdio.h> 2 3 int main(){ 4 int i; 5 for(i = 0; i < 10; i++){ 6 printf("Hello, World!\n"); 7 } 8 return 0; 9 } (gdb) disassemble main Dump of assembler code for function main: 0x0000000000400526 <+0>: push rbp 0x0000000000400527 <+1>: mov rbp,rsp 0x000000000040052a <+4>: sub rsp,0x10 0x000000000040052e <+8>: mov DWORD PTR [rbp-0x4],0x0 0x0000000000400535 <+15>: jmp 0x400545 <main+31> 0x0000000000400537 <+17>: mov edi,0x4005e4 0x000000000040053c <+22>: call 0x400400 <[email protected]> 0x0000000000400541 <+27>: add DWORD PTR [rbp-0x4],0x1 0x0000000000400545 <+31>: cmp DWORD PTR [rbp-0x4],0x9 0x0000000000400549 <+35>: jle 0x400537 <main+17> 0x000000000040054b <+37>: mov eax,0x0 0x0000000000400550 <+42>: leave 0x0000000000400551 <+43>: ret End of assembler dump. (gdb) break main Breakpoint 1 at 0x40052e: file firstprog.c, line 5. (gdb) run Starting program: /home/silnex/hack/a.out Breakpoint 1, main () at firstprog.c:5 5 for(i = 0; i < 10; i++){ (gdb) info registers rip rip 0x40052e 0x40052e <main+8> (gdb)
중지점이 main() 함수의 시작 점있다. 이때 17번째 줄에 0x000000000040052e <+8>: mov DWORD PTR [rbp-0x4],0x0
의 주소값과,
info registers rip
를 통해 얻은 주소값이 동일함을 볼 수 있다.
17번째 줄까지의 명령들을 함수 프롤로그(function prologue)라고 불린다.