실행중인 프로세스의 전체 메모리 구조 / 스택에서 사용되는 레지스터 / 어셈블리에서 함수에 대해서 정리한다.
스택 메모리
- 함수
- 지역변수
1. 실행중인 프로세스의 전체 메모리 구조
- /proc/pid/maps
( 실행했던 프로세스 id에 메모리 구조를 나타내는 파일 )
* 리눅스에서 .so 확장자는 동적 라이브러리를 나타내고 .a는 정적 라이브러리를 나타낸다.
( 리눅스 메모리 구조가 그림으로 표현된 것이다..
env, etc, argc, argv도 스택의 일부분이다... )
메모리 구조( maps )
08048000-08049000 r-xp 00000000 03:01 425344 /root/a.out
08049000-0804a000 rw-p 00000000 03:01 425344 /root/a.out
40000000-40013000 r-xp 00000000 03:01 310116 /lib/ld-2.1.3.so
40013000-40014000 rw-p 00012000 03:01 310116 /lib/ld-2.1.3.so
40014000-40016000 rw-p 00000000 00:00 0
4001c000-40109000 r-xp 00000000 03:01 310123 /lib/libc-2.1.3.so
40109000-4010d000 rw-p 000ec000 03:01 310123 /lib/libc-2.1.3.so
4010d000-40111000 rw-p 00000000 00:00 0
bfffe000-c00000000 rwxp fffff000 00:00 0
* 0x00000000 ~ 0x08047fff은 사용하지 않는 영역이다.
* 0x08048000 ~ 0x3fffffff는 text, data, bss 세그먼트가 사용하는 코드 세그먼트 영역이고 그 이후 남는 영역은 heap 메모리 확장을 위한 예약된 영역이다.
( heap 메모리를 계속 할당하게 되면 확장하면서 해당 확장 영역을 사용한다 )
* 0x40000000 ~ 0xbffdffff는 공유 라이브러리 영역이다.
* 0xbfffe000 ~ 0xc0000000 영역은 stack 메모리 영역이다.
( 스택은 kernel의 메모리 영역을 침범하는 위험을 없애기 위해서 높은쪽에서 낮은쪽으로 주소가 자라난다 )
* 0xc0000000 ~ 0xFFFFFFFF는 kernel이 사용하는 영역으로 접근이 불가능하다.
* stack 메모리는 여러 곳( 함수, 프로그램 )에서 같이 사용하는 공용 공간이다.
( 필요할때만 가져왔다가 사용후에 다시 메모리를 회수하는 식으로 동작한다 )
!! stack 메모리에서 메모리의 할당은 높은쪽에서 낮은쪽으로 되지만 데이터의 할당은 낮은쪽에서 높은쪽으로 이루어진다.
!! stack 메모리에는 선언한 순서대로 차곡차곡 쌓인다.
( 메모리 구조를 파악하기 위한 프로그램을 하나 간단하게 만들어서 sleep 명령을 이용해서
프로그램이 계속 동작 될 수 있도록 했다.. )
( 프로그램을 실행한다음 백그라운드에서 정지 상태로 만들어 두면 아직 프로세스가 끝난게 아니기 때문에
/proc/ 디렉터리에서 해당 프로세스에 대한 pid로 된 디렉터리가 없어지지 않을것이다..
그러므로 /proc/pid/maps 파일 확인이 가능하다 )
( pid가 767이기 때문에 /proc/767/ 디렉터리에 들어가면 해당 프로세스에 대한
메모리 구조가 나와있는 maps 파일을 볼 수 있다.. )
( /proc/pid/maps 파일을 이용해서 해당 메모리의 구조를 한눈에 파악 할 수 있다... )
( main() 함수는 entry point이므로 &main과 같이 main() 함수의 메모리 주소를 가져올 수 있다.. )
( 스택 메모리의 출력 결과를 자세히 보면 메모리가 순서대로 줄어드는걸 볼 수있는데
스택 메모리의 경우에는 push 된 내용이 차곡차곡 쌓이기 때문이다.. )
2. 스택 메모리에서 사용되는 레지스터
- ESP: Stack Pointer ( stack 메모리에서 데이터를 어디까지 사용했는지를 알 수 있는 레지스터 )
- EBP: Base Pointer ( 기준점 )
* stack pointer의 경우 계속 값이 변하기 때문에 그에 대한 기준점으로 base pointer를 사용해서
base pointer를 기준으로 스택 메모리 영역에 접근할 수 있다.
( stack 형태 )
* esp 레지스터는 stack pointer로써 top의 위치를 가르키기 때문에
그 값이 push나 pop을 할 때마다 계속 변하게 된다.
* ebp 레지스터는 base pointer, 즉 기준점 역할을 하기 때문에 stack pointer를 base pointer에
넣어두고 해당 기준점으로부터 스택 메모리에 주소를 접근할 수도 있다.
( printf로 출력이 될 때는 push esp를 했을때가 아닌 push prompt_hex에 대한 주소가 출력되는 것이다.. )
( 스택 자료구조에서 볼수 있듯이 push를 이용해서 내용을 넣고
pop을 이용해서 top에 있는 내용을 꺼낸다.. )
[ a와 b에 값을 할당해서 a와 b를 출력하는 C코드 ]
int main()
{
int a;
int b;
a = 10;
b = 20;
printf("%d \n", a );
printf("%d \n", b);
}
* 지역변수는 함수가 실행되기 전까지는 메모리에 잡혀있지 않고 함수가 실행되서야 스택 메모리에
할당 된다.
( 컴파일러가 실행이 될때 지역변수를 다 가져와서 필요한 메모리를 미리 계산해둔다 )
!! 함수가 실행되는 동안에는 스택 메모리에 대한 크기나 값을 변경할 수 없다. ( static memory )
( sub 명령을 이용해서 스택 메모리를 8byte만큼 사용한다음에 그 stack pointer의 주소 값을
base pointer 역할을 하는 레지스터인 ebp에 할당 해두면 stack pointer가 바뀌어도
ebp를 이용해서 미리 확보해둔 메모리에 접근해서 값을 할당 시킬 수 있다.. )
* 위 그림에 있는 어셈블리 코드는 위에 있는 a와 b에 값을 할당해서 a와 b를 출력하는 C코드를
어셈블리 형태로 작성한 것이다.
3. 함수
- 어셈블리어에서는 함수라는 개념이 없다.
- 함수의 기계어 표현이 아주 정교하게 작성이 되어 있다.
- 하지만 취약점도 이 과정에서 발생한다.
[ sum 함수가 포함된 C 코드 ]
int sum( int a, int b )
{
int sum = 0;
sum = a + b;
return sum;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = sum( a, b );
printf("sum is: %d\n", ret );
return 0;
}
[ 위 코드를 어셈블리 코드 표현한 경우 ( 어셈블리에서 함수 표현 ) ]
extern printf
section .data
prompt_sum db 'sum is: %d', 10, 00
section .text
global main
sum:
push ebp
mov ebp, esp ; function prologue
sub esp, 4
mov dword [ebp-4], eax
add dword [ebp-4], ebx ; sum = a + b;
mov eax, dword [ebp-4] ; return sum;
mov esp, ebp
pop ebp ; function epilogue
jmp return
main:
sub esp, 12
mov ebp, esp
mov dword [ebp], 0
mov dword [ebp+4], 20
mov dword [ebp+8], 10
mov eax, [ebp+8]
mov ebx, [ebp+4]
jmp sum
return:
mov [ebp], eax
push dword [ebp]
push prompt_sum
call printf
* calling component 표준에 따르면 return 값을 전달할때도 eax를 사용한다.
댓글