프로세스와 메모리
프로그램은 프로세스라는 형태로 OS에 의해 관리되며, 자원 (CPU, 메모리, 보조기억장치 등) 들을 할당 받습니다.
Microsoft Windows 32bit 의 경우에는 가상 메모리의 형태로 4GB의 메모리를 프로세스에게 할당해 줍니다.
가상 메모리는 OS에 의해 실제 물리적 메모리 주소로 변환됩니다.
- 만일 실제 가용 물리메모리가 부족하면 페이징 파일 등을 통해 관리합니다.
만약 프로세스에서 1번의 메모리 위치값을 사용하고 있는 데이터가 있다면, 실제로 이 메모리가 물리적으로 위치하는 곳은 2 혹은 3이 될 수도 있습니다. 모든 변환은 OS가 처리하므로, 프로세스는 1번의 메모리에 있다고 생각하고 사용할 수 있습니다.
프로그램의 실행 후, OS는 대부분 위와 같은 형태의 메모리 구조를 구성합니다.
세그먼트 (Segment)
OS에 따라서 다를 수 있으나, 대부분의 프로세스는 4개의 세그먼트를 가집니다.
세그먼트 - 코드 영역
프로그램 코드로 생성한 기계어들이 위치합니다. 실제 프로그램 파일을 실행 시켰을때, 파일에 들어있던 목적코드들이 이곳에 위치합니다.
폰 노이만 머신의 stored program concept 의 핵심 파트이기도 합니다.
이 영역은 일반적으로 OS에 의해 보호되어 조작할 수 없습니다.
- 접근하려고 하면 에러(segment fault)가 발생합니다.
세그먼트 - 데이터 영역
프로그램 내에 존재하는 정적 변수, 전역 변수 등이 이곳에 위치합니다. 현재 실행 상태와는 관계없이 항상 존재해야 하는 변수들이 존재하는 영역입니다.
세그먼트 - 힙 영역
이 곳에는 프로그램이 동작하는 중에 시스템콜인 malloc(), free() 함수나
new, delete 연산자에 의해 만들어진 힙 메모리가 위치합니다.
프로그램 실행중에 메모리 영역을 잡는 것을 동적 할당이라고 하며 그 때 사용하는 메모리가 바로 이 힙 영역입니다.
변수는 사용할 메모리를 미리 계획하여 잡아둬야 한다는 단점이 있습니다.
예)
- 메모리를 500KB 밖에 예약해 두지 않았으나, 사용자가 1MB의 그림 파일을 읽으려고 합니다.
- 이 파일을 읽기에는 너무 부족한 공간을 가지고 있게 됩니다.
그렇다고 해서 1MB를 예약해 두었다가, 사용자가 1KB 짜리 그림을 사용하려면 메모리 낭비가 됩니다.
PC의 자원은 한정되어 있으며, PC 에서 동작하는 프로그램은 여러분의 프로그램만이 아닙니다.
위와 같이 메모리를 많이 할당해 놓는다면, 다른 프로세스들이 충분한 메모리를 확보하지 못 할 수도 있습니다.
- 사용자는 아마도 여러분의 프로그램이 좋지 않은 프로그램이라 생각하고 삭제할 지도 모릅니다.
위와 같은 이유들이 동적할당이 필요하다는 사실을 알려줍니다.
세그먼트 - 스택 영역
프로그램의 실행 상황을 다루는 영역입니다.
처음 프로그램의 진입점으로써 main 함수가 호출되고, 프로그램의 진행에 따라서 다른 함수들을 호출합니다. 각 함수들은 각각 자신만의 지역변수를 가집니다.
지역변수와 함수 자신의 상태에 대한 정보가 위치하는 공간이 스택 영역입니다.
예 )
- main 함수에서 A라는 함수를 호출한다면, A 함수가 사용하는 만큼의 데이터가 스택에 push 됩니다.
- A 함수가 끝나게 되면 데이터는 다시 pop 이 됩니다.
즉 스택의 가장 최상위에 있는 데이터가 현재 실행하고 있는 함수의 데이터가 해당합니다.
자료구조 - 스택
기본적으로 메모리는 주소에 의해 자유롭게 접근하고, 사용할 수 있습니다.
자료구조는 이러한 메모리를 어떻게 사용할지에 대해 특정한 약속을 정한 형태입니다.
즉 특정한 접근방식 혹은 해석방식을 규정하여 메모리에 의미를 부여힙니다.
스택은 LIFO(Last In First Out - 다른말로 FILO, First in Last Out)의 자료구조입니다.
나중에 들어온 데이터가 가장 마지막에 나간다는 의미를 가집니다.
스택에 데이터를 넣는 작업을 push, 꺼내는 작업을 pop 이라고 합니다.
예 )
데이터 1 |
데이터 1을 push 합니다.
데이터 2 |
데이터 1 |
데이터 2를 push 합니다.
데이터 1 |
데이터 2를 pop 합니다.
데이터 3 |
데이터 1 |
데이터 3을 push 합니다.
데이터 4 |
데이터 3 |
데이터 1 |
데이터 4를 push 합니다.
프로그램의 처리와 레지스터
CPU 에는 여러가지 종류의 레지스터가 존재합니다.
여기서는 우리가 쉽게 접할 수 있는 X86을 예시로 하겠습니다.
X86의 레지스터는 EAX, EBX, ECX, EDX, EBP, EIP, ESP 등이 존재합니다.
물론 여기서 모든 레지스터를 설명하려는 것은 아니며, EIP 와 ESP 만을 다룹니다.
EIP 는 Instruction Pointer 이며, 다음에 실행할 명령어의 번지를 가르킵니다.
ESP 는 Stack Pointer 이며, 스택의 마지막 자료를 가르킵니다.
코드는 코드영역에 존재하며, 명령어들이 코드 영역내에 나열되어 있습니다. 명령은 다음과 같다고 해 보겠습니다.
- 좌측의 숫자는 해당 명령의 번지입니다. @로 표현된 부분은 명령으로는 번역되지 않는 부분입니다.
main 함수 :
@. A 변수를 만듬.
1. A에 0을 대입
2. A에 1을 더하기
3. print 를 호출하여 A를 C로 넘김
4. 함수 종료
print 함수 :
@. B 변수를 만듬.
5. B에 C를 대입
6. B 출력
7. 함수 종료
처음 main 함수가 호출됩니다.
- 이 함수의 지역변수에 A 변수가 있기 때문에, main 함수에 해당하는 스택 메모리에는 변수 A 가 함께 저장됩니다.
- EIP 는 1을 가르키고 있습니다.
EIP 가르키고 있는 명령을 가져오며, 스택에 있는 A 변수에 0을 대입합니다.
EIP 가 1 증가 (1 -> 2) 하며, 다시 명령을 가져와서 스택에 있는 A 변수에 1을 더합니다.
다시 EIP 가 1 증가 (2 -> 3) 하고, printf 함수의 호출루틴에 들어갑니다.
스택에 새로운 메모리를 추가합니다.
print 함수에는 B 변수가 지역변수로 존재하고, C 변수는 인수로 들어옵니다.
따라서 print 함수에 해당하는 스택 메모리에는 변수 B와 변수 C, 그리고 main 함수의 다음 명령 주소를 함께 저장합니다.
- 다음 명령주소를 저장하는 이유는 이 함수가 종료되면 원의 함수로 복귀해야 하기 때문입니다.
이와 동시에 ESP 는 print 함수에 해당하는 스택 메모리를 가르키게 됩니다.
이제 print 함수의 루틴에 들어왔습니다.
- EIP 는 5를 가르키고 있으며 (3 -> 5), B 변수에 C를 대입합니다.
EIP 가 1 증가 (5 -> 6) 하고, B 변수를 출력합니다.
다시 EIP 가 1 증가 (6 -> 7) 하면 함수를 종료하는 위치입니다.
printf 함수에 해당하는 스택 메모리에 기록해 두었던 주소를 가져오며, EIP 는 4가 (7 -> 4) 됩니다.
또한 이와 동시에 print 함수에 해당하는 스택 메모리는 반환되며, ESP 는 다시 main 함수의 스택 메모리를 가르키게됩니다.
이어서 4번 번지의 명령을 처리하면 main 함수가 종료됩니다.
실행 루틴 관리에 스택을 사용하는 이유
C/C++ 언어에서는 함수를 몇 번이고 다시 호출할 수 있으며, 심지어는 자신이 자신을 다시 호출할 수도 있습니다.
- 이를 재귀호출이라고 합니다.
이 과정에서 몇 가지 문제가 발생합니다.
- 함수 내에 포함된 지역변수들은 어떻게 관리를 해야할까요?
- 함수의 호출 과정에서 처리해야할 명령어의 순서가 뒤섞여버리는데 어떻게 관리를 해야할까요?
이러한 부분을 처리하는 방법으로써 스택영역은 매우 효과적입니다.
힙 영역과 스택영역의 관계
힙 영역과 스택영역은 메모리의 효율을 고려하여 설계되었습니다.
이 둘은 코드영역과 데이터 영역의 나머지를 차지하게 되는데, 메모리의 사용 방향이 다릅니다.
heap 은 낮은 주소에서 높은 주소로, stack 은 높은 주소에서 낮은 주소 방향으로 향합니다.
- 이 둘의 영역의 크기는 프로그램의 실행 과정에 따라서 얼마든지 변할 수 있기 때문입니다.
스택과 힙을 같은 방향으로 진행시키려면, 정확히 특정 메모리 경계선을 선정해서 둘을 나누어야 합니다.
그러나 마주보는 방향으로 진행시키면, 그럴 필요 없이 서로를 신경쓰지 않고 남은 메모리 공간에 확장해 나갈 수 있습니다.
물론 충분한 메모리 공간이 없다면 어느 경계점에서 둘은 서로 부딪히게 될 지도 모릅니다.
- 컴파일러에 따라서 다르지만, 경계선을 정해두고 거기까지만 확장하도록 하기도 합니다.