GDB & Virtual Address

suseodd·2021년 9월 15일
0

GDB & Virtual Address

가상 메모리 개념을 좀 더 명확하게 이해하기 위해 GDB 실습을 통해 전역변수, 지역변수들이 실제 가상메모리 어디에 위치하는지를 살펴보도록 하겠습니다. 32-bit 컴퓨터 기준입니다.

>> docker pull tomerbd/gcc-gdb-dockerfile
>> docker run -it -v $(pwd):/src \
   --security-opt seccomp=unconfined \
   tomerbd/gcc-gdb-dockerfile \
   /bin/bash

--security-opt seccomp=unconfined를 주어야 gdb 작업에서 address space randomization이 발생하지 않습니다.

#include <stdlib.h> 

int main() {
    int a = 0xBEEF;
    int *b = malloc(sizeof(int));
    g = 0xDDDD;
    return 0; 
}

위 코드를 컴파일해서 executable object 파일로 만들고 gdb 작업을 하도록 하겠습니다.

root@8c32719ecb7c:~# gcc main.c
root@8c32719ecb7c:~# ls
a.out  main.c
root@8c32719ecb7c:~# gdb a.out

(gdb) disas main
Dump of assembler code for function main:
   0x000000000040052d <+0>:	    push   %rbp
   0x000000000040052e <+1>:	    mov    %rsp,%rbp
   0x0000000000400531 <+4>:	    sub    $0x10,%rsp
   0x0000000000400535 <+8>:	    movl   $0xbeef,-0x10(%rbp)
   0x000000000040053c <+15>:	mov    $0x4,%edi
   0x0000000000400541 <+20>:	callq  0x400430 <malloc@plt>
   0x0000000000400546 <+25>:	mov    %rax,-0x8(%rbp)
   0x000000000040054a <+29>:	movl   $0xdddd,-0xc(%rbp)
   0x0000000000400551 <+36>:	mov    $0x0,%eax
   0x0000000000400556 <+41>:	leaveq
   0x0000000000400557 <+42>:	retq
End of assembler dump.

어셈블리를 확인할 수 있습니다. gdb명령어 disas는 해당 코드 주변 인스트럭션이 어떻게 구성되는지 확인할 수 있고 disas main은 가상메모리에서 main 함수 인스트럭션을 확인할 수 있습니다. 먼저 인스트럭션 첫 부분인 0x000000000040052d <+0>: push %rbp 를 보도록 하겠습니다. 여기를 보면 가상 메모리상 코드 영역이 시작되는 부근입니다.

위 그림에서 Read-only segment가 code가 시작되는 부분이고 해당 주소가 예시에서는 0x40052d였으며 이론상 code 위치인 0x40054s근처로 잡혔습니다. 이렇게 길제 코드가 가상 메모리상에 이론대로 위치해 있음을 확인할 수 있습니다.

여기서 0xBEEF가 들어가 있는 a의 주소가 어디인지 보겠습니다.

(gdb) break *0x000000000040053c
Breakpoint 1 at 0x40053c
(gdb) run
Starting program: /root/a.out

Breakpoint 1, 0x000000000040053c in main ()
(gdb)
(gdb) disas
Dump of assembler code for function main:
   0x000000000040052d <+0>:	    push   %rbp
   0x000000000040052e <+1>:	    mov    %rsp,%rbp
   0x0000000000400531 <+4>:	    sub    $0x10,%rsp
   0x0000000000400535 <+8>:	    movl   $0xbeef,-0x10(%rbp)
=> 0x000000000040053c <+15>:	mov    $0x4,%edi
   0x0000000000400541 <+20>:	callq  0x400430 <malloc@plt>
   0x0000000000400546 <+25>:	mov    %rax,-0x8(%rbp)
   0x000000000040054a <+29>:	movl   $0xdddd,-0xc(%rbp)
   0x0000000000400551 <+36>:	mov    $0x0,%eax
   0x0000000000400556 <+41>:	leaveq
   0x0000000000400557 <+42>:	retq
End of assembler dump.

movl 인스트럭션 바로 다음 인스트럭션인 mov에 break를 걸고 프로그램을 동작시켰습니다. movl src dest 인스트럭션은 4바이트 만큼 src에서 dest로 복사한다는 의미입니다. $0xbeef는 그 값을 뜻하므로 movl $0xbeef,-0x10(%rbp)-0x10(%rbp)$0xbeef를 복사한다는 뜻입니다. 따라서 -0x10(%rbp)가 변수 a의 주소가 됨을 알 수 있습니다.

(gdb) x/2xb $rbp-0x10
0x7fffffffe700:	0xef	0xbe

x는 gdb에서 "examine"을 의미하는 것으로 해당 메모리에 어떤 값이 들어가 있는지를 보겠다는 명령어입니다. x 명령어의 활용법을 확인하고 싶은 분들은 아래 링크를 확인하시기 바랍니다.

gdb에서 x 사용해서 Virtual Address엿보기

$rbp-0x10은 rbp에 rbp 레지스터에 들어있는 값 -0x10을 의미하고 가상 메모리 주소를 의미합니다. 실제 그런지 알아보기 위해 rbp 레지스터에 어떤 값이 들어가있는지 봅시다.

(gdb) p $rbp
$1 = (void *) 0x7fffffffe710

rbp에 주소 0x7fffffffe710이 들어가 있습니다. 이 얘기는 해당 주소에서 0x10만큼 뺀 주소에 0xbeef 값이 들어 있다는 의미겠죠? 메모리 주소 값을 직접 입력해서 확인해 보겠습니다.

(gdb) x/2x 0x7fffffffe700
0x7fffffffe700:	0xef	0xbe

0xef와 0xbe값을 확인할 수 있었습니다. 그런데 재미있는 것은 0xbe 0xef 이렇게 순서대로 값이 들어있는 것이 아닌 반대로 바이트 값이 들어가 있는 것을 확인할 수 있습니다.

이유는 little endian 개념에 기초합니다. 즉 주소 0x7fffffffe700에는 0xef 바이트 값이 들어가고 주소 0x7fffffffe701에는 0xbe 바이트 값이 들어가 있습니다. little endian은 어떤 데이터를 메모리에 저장할 때 less signigicant 바이트 값을 먼저 저장하는 방식입니다. 이 부분은 다른 글에서 더 자세히 다루도록 하겠습니다.

(gdb) x/xw 0x7fffffffe700
0x7fffffffe700:	0x0000beef

워드 단위로 읽어오면 0x0000beef를 읽어옴을 볼 수 있습니다. 이 다음에는 malloc 함수를 호출한 후 힙 메모리 주소를 받아오는 정수형 포인터 변수 b에 들어있는 값을 확인해 힙 메모리 주소 위치를 확인하겠습니다.

(gdb) break *0x000000000040054a
Breakpoint 2 at 0x40054a
(gdb) cont
Continuing.

Breakpoint 2, 0x000000000040054a in main ()
(gdb)
(gdb) disas
Dump of assembler code for function main:
   0x000000000040052d <+0>:	push   %rbp
   0x000000000040052e <+1>:	mov    %rsp,%rbp
   0x0000000000400531 <+4>:	sub    $0x10,%rsp
   0x0000000000400535 <+8>:	movl   $0xbeef,-0x10(%rbp)
   0x000000000040053c <+15>:	mov    $0x4,%edi
   0x0000000000400541 <+20>:	callq  0x400430 <malloc@plt>
   0x0000000000400546 <+25>:	mov    %rax,-0x8(%rbp)
=> 0x000000000040054a <+29>:	movl   $0xdddd,-0xc(%rbp)
   0x0000000000400551 <+36>:	mov    $0x0,%eax
   0x0000000000400556 <+41>:	leaveq
   0x0000000000400557 <+42>:	retq
End of assembler dump.

%rax 레지스터는 반환하는 값을 저장하는 레지스터이고 그 위에 malloc 함수를 호출했다는 인스트럭션을 확인할 수 있으므로 현재 %rax 레지스터에 힙 메모리 주소 값이 들어가 있을거라 예상할 수 있습니다.

(gdb) p/x $rax
$4 = 0x602010

%rax 레지스터에 있는 값을 16진법으로 확인해보면 0x602010임을 확인할 수 있습니다. 이 값이 힙 메모리 시작 주소입니다. 그렇다면 이것을 저장하고 있는 포인터 변수 b의 위치는 어디 일까요?

0x0000000000400546 <+25>: mov %rax,-0x8(%rbp) 를 보면 %rax레지스터 값을 주소 -0x8(%rbp) 에 저장하는 것을 볼 수 있습니다. 그렇다면 해당 주소가 b의 위치이고 여기에 앞서 살펴본 0x602010이 저장되어 있을 것입니다. 확인해보죠.

(gdb) x/xw $rbp-0x8
0x7fffffffe708:	0x00602010

한 바이트씩 끊어서 읽어보겠습니다.

(gdb) x/4xb $rbp-0x8
0x7fffffffe708:	0x10	0x20	0x60	0x00

값이 들어가 있음을 확인됩니다. heap 메모리도 가상 메모리상 낮은 부분에 위치해 있음을 확인할 수 있습니다. 또한 stack에 들어갈 지역변수인 b는 주소 값이 굉장히 크다는(0x7fffffffe708)것을 볼 수 있습니다. 이렇게 가상 메모리 그림대로 code, heap, stack이 위치해 있음을 확인할 수 있었습니다.

이번에는 전역변수가 data에 위치했는지를 확인하기 위해 코드를 조금 변경하겠습니다.

#include <stdlib.h> 

int g = 0xDEAD;

int main() {
    int a = 0xBEEF;
    g = 0xDDDD;
    return 0; 
}

위 코드를 컴파일하고 gdb를 실행하도록 하겠습니다.

(gdb) disas main
Dump of assembler code for function main:
   0x00000000004004ed <+0>:	push   %rbp
   0x00000000004004ee <+1>:	mov    %rsp,%rbp
   0x00000000004004f1 <+4>:	movl   $0xbeef,-0x4(%rbp)
   0x00000000004004f8 <+11>:	movl   $0xdddd,0x200b36(%rip)        # 0x601038 <g>
   0x0000000000400502 <+21>:	mov    $0x0,%eax
   0x0000000000400507 <+26>:	pop    %rbp
   0x0000000000400508 <+27>:	retq
End of assembler dump.
(gdb) break *0x0000000000400502
Breakpoint 1 at 0x400502
(gdb) run
Starting program: /root/a.out

Breakpoint 1, 0x0000000000400502 in main ()
(gdb)
(gdb) disas
Dump of assembler code for function main:
   0x00000000004004ed <+0>:	push   %rbp
   0x00000000004004ee <+1>:	mov    %rsp,%rbp
   0x00000000004004f1 <+4>:	movl   $0xbeef,-0x4(%rbp)
   0x00000000004004f8 <+11>:	movl   $0xdddd,0x200b36(%rip)        # 0x601038 <g>
=> 0x0000000000400502 <+21>:	mov    $0x0,%eax
   0x0000000000400507 <+26>:	pop    %rbp
   0x0000000000400508 <+27>:	retq
End of assembler dump.

위 어셈블리 코드를 보면 $0xdddd를 특정 주소에다 복사하고 있습니다. 0x200b36(%rip) 저 위치가 전역변수 g 같습니다.

(gdb) x/2xb $rip+0x200b36
0x601038 <g>:	0xdd	0xdd

해당 주소 값을 읽으니 0xdd와 0xdd를 확인할 수 있습니다. 이번에는 워드 단위로 읽어봅시다.

(gdb) x/xw $rip+0x200b36
0x601038 <g>:	0x0000dddd

워드 단위로 읽어도 제대로 읽힙니다. 위 어셈블리 코드를 자세히 보면

0x00000000004004f8 <+11>: movl $0xdddd,0x200b36(%rip) # 0x601038 <g>

이런 인스트럭션이 있음을 확인할 수 있고 # 0x601038 <g>를 볼 수 있습니다. g라는 전역변수 위치가 0x601038임을 어셈블리코드에서 확인 가능합니다. 위에서 $rip+0x200b36도 같은 주소를 가리키고 있습니다. 또한, 0x601038은 코드 시작 지점인 0x4004ed보다 뒤에 있고 이는 가상메모리상 data section이 code section보다 주소상 뒤에 있음을 알 수 있는 대목입니다. 이렇게 code, data, heap, stack 섹션이 가상메모리에 이론대로 위치하고 있음을 확인해 보았습니다.

profile
백엔드 개발자 디디라고합니다.

0개의 댓글