GDB & Virtual Address
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 섹션이 가상메모리에 이론대로 위치하고 있음을 확인해 보았습니다.
Author And Source
이 문제에 관하여(GDB & Virtual Address), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@suseodd/GDB-Virtual-Address저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)