CS/Operating System
[Operating System] 시스템 메모리(프로세스 주소 공간)
hanseongbugi
2024. 1. 26. 16:13
시스템 메모리 구조
- 프로그램을 동작시키면 프로세스가 생성되고 메모리에 프로세스 주소 공간이 할당됨
- 프로세스 주소 공간은 코드(Code) 영역, 데이터(Data) 영역, 스택(Stack) 영역, 힙(Heap) 영역으로 이루어져 있다.
코드(Code) 영역
- 실행될 프로그램 코드가 적재되는 영역
- 사용자가 작성한 모든 함수의 코드와 사용자가 호출한 라이브러리의 함수들의 코드가 적재
- 컴파일 시간에 결정되고 중간에 코드를 바꿀 수 없게 Read-Only로 지정되어 있다.
데이터(Data) 영역
- 프로그램에서 고정적으로 만든 변수 공간
- 전역 변수와 정적 데이터가 적재된다.
- 전역(Static) 값을 참조한 코드는 컴파일을 하고 나면 Data 영역의 주소 값을 가르키도록 바뀐다.
- 실행 중 전역 변수의 값이 변경될 수 있으므로 Read-Write로 지정되어 있다.
- 프로세스 적재시 할당되며, 종료 시 소멸된다.
스택(Stack) 영역
- 함수가 실행될 때 사용될 데이터를 위해 할당된 공간
- 매개변수, 지역변수, 함수가 종료된 후 돌아갈 주소 값이 적재된다.
- 함수는 호출될 때, 스택 영역에서 위쪽으로 공간을 할당한다.
- 함수가 return하면 할당된 공간을 반환한다.
- 함수 호출 외에 프로세스에서 필요 시 사용이 가능하다.
- 스택은 메모리의 상위 주소에서 하위 주소 방향으로 사용한다.
- 후입 선출(Last In First Out, LIFO) 원칙에 따라 나중에 저장된 값을 먼저 사용한다.
힙(Heap) 영역
- 프로세스의 실행 도중 동적으로 사용할 수 있도록 할당된 공간
- 프로그램이 동작할 때 알 수 없는 가변적인 양의 데이터를 저장하기 위해 미리 예약된 메모리 영역
- 프로그래머가 필요 시 사용하곤 한다.
- malloc, new 등으로 할당 받은 공간은 힙 역역에 할당
- 힙 영역에서 아래 번지로 내려가며 할당
- 힙 영역은 프로그래머에 의해 할당되었다가 회수되는 작업을 되풀이한다.
- 기억 공간은 포인터 변수를 통해 동적으로 할당 받고 돌려준다.
- 프로그램 실행 중 힙 영역이 없어지면, 메모리 부족으로 비정상 종료된다.
스택 영역 동작 방식
void func1()
{
func2(); // func2() 호출
}
void func2()
{
}
int main(void)
{
func1(); // func1() 호출
return 0;
}
- 아래 그림은 예제 코드에서 함수 호출에 의한 스택 영역의 변화를 보여준다.
- 스택 공간에는 함수의 정보가 적재된다.
- 이때 함수의 정보는 지역 변수, 매개 변수, 반환 주소 값을 의미한다.
Step1. 프로그램이 실행되면, 가장 먼저 main 함수가 호출되어 main 함수의 정보가 스택 영역에 적재된다.
Step2. func1 함수를 호출하면, 해당 함수의 정보가 스택 영역에 적재된다.
Step3. func2 함수를 호출하면, 해당 함수의 정보가 스택 영역에 적재된다.
Step4. func2 함수의 모든 작업이 완료되어 반환되면, func2 함수의 정보가 스택에서 제거된다.
Step5. func1 함수의 호출이 종료되면, func1 함수의 정보가 스택에서 제거된다.
Step6. main 함수의 모든 작업이 완료되면, main 함수의 정보가 스택에서 제거되면서 프로그램이 종료된다.
- 스택은 가장 나중에 저장된 데이터가 가장 먼저 제거되는 방식으로 동작한다.
- 이러한 방식을 후입선출(LIFO, Last-In First-Out) 방식이라고 한다.
- 이때 스택은 푸시(push) 동작으로 데이터를 저장하고, 팝(pop) 동작으로 데이터를 제거한다.
스택 오버플로우 (Stack Overflow)
- 제귀 함수의 호출이 무한히 반복된다면, 스택 오버플로우에 의해 프로그램이 종료
- 재귀 호출이 무한히 반복되면, 위 그림에서 Step 3 이후로 재귀 호출에 의한 함수의 정보가 계속해서 쌓여 간다.
- 스택의 모든 공간을 다 차지하고 난 후 더 이상의 여유 공간이 없을 때 또 다시 스택 영역에 저장하게 되면, 스택 영역을 넘어가서 저장되게 돤다.
- 해당 스택 영역을 넘어가도 데이터가 저장될 수 있으면, 해당 프로그램은 오동작을 하게 되거나 보안상의 크나큰 취약점을 가지게 된다.
- C 언어에서는 실행 중인 프로그램에서 스택 오버플로우가 발생하면, 에러를 발생하고 곧바로 강제 종료시킨다.
동적 할당 (Dynamic Allocation)
- C++에서는 동적 할당 시 new와 delete 연산자를 사용한다.
- new 연산자는 malloc, calloc, realloc과 같은 역할을 한다.
- malloc은 인자로 할당받을 메모리의 총 크기(바이트)를 받는다. 이때 각 칸은 NULL로 이루어진다.
- calloc은 인자로 할당받을 칸의 크기와 각 칸의 크기를 받는다. 이때 초기화도 이루어진다.
- realloc은 할당받은 메모리의 크기를 늘려준다.
- malloc과 calloc의 차이점은 메모리가 초기화에 있다.
- new를 하였을 때 메모리에 공간이 없다면 NULL을 반환한다.
- 따라서 동적할당 후 NULL 체크를 항상 해줘야
- delete 연산자는 동적으로 할당된 메모리를 해제하여 재사용 할 수 있도록한다.
int main(){
int* num1 = new int;
int* num2 = new int(39);
Bugi* Bae = new Bugi();
Bugi* Han = new Bugi(25, "배한성", "171");
if(!num1){
cout << "NULL이 반환되었습니다." << '\n';
return -1;
}
if(!num2){
cout << "NULL이 반환되었습니다." << '\n';
return -1;
}
if(!Bae){
cout << "NULL이 반환되었습니다." << '\n';
return -1;
}
if(!Han){
cout << "NULL이 반환되었습니다." << '\n';
return -1;
}
cout << *num1 << '\n';
cout << *num2 << '\n';
cout << Bae->getAge() << '\n';
cout << Han->getAge() << '\n';
delete num1;
delete num2;
delete Bae;
delete Han;
return 0;
}
// 출력
// 배 생성되었다.
// 배한성 생성되었다.
// 0
// 39
// 0
// 25
// 배 소멸되었다.
// 배한성 소멸되었다.
- new 키워드로 동적할당 시 포인터 변수를 사용해 주소 값을 저장해야한다.
- new "객체의 타입"(초기화할 값); 형태로 동적할당 한다.
- 초기화할 값을 넣지 않으면 기본 생성자가 호출된다.
- delete 연산자를 사용하여 소멸 시 delete 선언의 순서에 따라 소멸자가 호출된다.
- 정적으로 객체 생성 시 생성자 호출의 역순으로 소멸자가 호출된다.
- 배열 동적할당 시 배열이라는 것을 명시해주면 된다.
- int* arr = new int[100]
- 배열의 크기를 정해주고 동적할당을 해준다.
- 배열의 동적할당을 해제할 때 delete 연산자를 사용하고, 대괄호를 통해 배열임을 명시해주면 된다.
- delete[] arr;
- 배열을 동적할당 할 때 선언과 동시에 초기화할 수 있다.
int main(){
int* arr = new int[10]{0, 100, 200, 300, 400, 500, 600, 700, 800, 900};
for(int i = 0; i < 10; i++){
cout << arr[i] << '\n';
}
delete[] arr;
return 0;
}
// 출력
// 0
// 100
// 200
// 300
// 400
// 500
// 600
// 700
// 800
// 900
구역을 나눈 이유
- 최대한 데이터를 공유하여 메모리 사용량을 줄여야 하기 때문
- Code는 같은 프로그램 자체에서는 모두 같은 내용이기 때문에 따로 관리하여 공유함
- Stack과 데이터를 나눈 이유는, 스택 구조의 특성과 전역 변수의 활용성을 위한 것
- 프로그램의 함수와 지역 변수는, LIFO(가장 나중에 들어간게 먼저 나옴)특성을 가진 스택에서 실행된다.
- 함수들 안에서 공통으로 사용하는 전역 변수는 따로 지정해주면 메모리를 아낄 수 있다.
출처
https://tcpschool.com/c/c_memory_structure
https://blog.naver.com/vgb910526/220661295961
https://jimmy-ai.tistory.com/376
https://github.com/gyoogle/tech-interview-for-developer