멈추지 않고 끈질기게
[컴퓨터공학] 스택 메모리(Stack Memory) 본문
※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.
이번 포스팅에서는 프로세스의 스택 메모리 영역에 대해 알아보겠습니다.
1. 스택 메모리
프로그램이 메모리에 적재되어 프로세스로 실행될 때, 프로세스가 차지하는 메모리 영역은 다음과 같이 구분됩니다.
정적 메모리 영역(컴파일 시점에 크기가 결정)
- 코드 영역: 프로그램의 코드(명령어)가 저장되는 영역
- 데이터 영역: 초기화까지 되어있는 전역 변수가 저장되는 영역
- BSS 영역: 초기화 없이 선언만 된 전역 변수가 저장되는 영역
동적 메모리 영역(런타임 중에 메모리 할당)
- 힙(Heap) 영역: C++의 malloc, C#의 new 등으로 직접 할당하는 영역
- 스택(Stack) 영역: 지역 변수 등 스택 프레임이 쌓이는 영역
이 중 스택 영역은 함수의 호출에 밀접하게 연관되어 있는 영역입니다. 함수 호출 시 매개변수 등 필요한 데이터들을 한데 묶어 스택에 올리는데 이를 스택 프레임이라고 하며(자세한 내용은 밑에서 다루겠습니다), 이러한 스택 프레임을 다루기 위한 특수 목적의 레지스터가 존재합니다.
SP(Stack Pointer)
스택의 최상단 주소값을 저장하는 레지스터입니다. 스택에 데이터를 push하거나 pop 할때마다 해당 레지스터 값도 갱신됩니다. 즉, 현재 스택 프레임이 어디까지인지를 나타내는 레지스터입니다. Intel CPU를 사용하는 PC라면 ESP라는 이름의 레지스터가 여기에 해당합니다.
FP(Frame Pointer)
현재 스택프레임의 베이스 주소를 저장하는 레지스터입니다. 스택 프레임을 스택 영역에 올릴 때 매개변수 다음으로 올라가며, 스택 프레임을 해제할 때까지 변하지 않습니다. 즉, EBP와 ESP를 통해 현재 스택 프레임 영역을 알 수 있습니다. Intel CPU를 사용하는 PC라면 EBP라는 이름의 레지스터가 여기에 해당합니다.
스택에는 데이터가 스택 프레임 단위로 쌓이며, SP와 FP를 통해 스택 프레임을 적재 / 해제하며 함수를 실행하게 됩니다.
2. 스택 프레임(Stack Frame)
함수를 호출할 경우 전달해야 하는 매개변수, 함수 종료 후 돌아가야 할 반환 주소, 함수 내에서 선언하는 지역 변수 등의 데이터가 저장되어야 하며 이러한 정보들을 스택 프레임이라고 합니다. 스택 프레임은 이름대로 스택 영역에 저장됩니다.
프로그램 실행 시 자동으로 메인 함수의 스택 프레임이 올라가며, 이 후 메인 함수에서 다른 함수를 호출할 경우 다음과 같은 과정으로 진행됩니다.
1) 함수에 전달할 매개변수 스택에 push(매개변수가 있는 경우)
2) 함수 call (해당 함수의 명령어 주소로 이동)
3) 함수 내용 실행
4) 함수 종료 후 해당 함수의 데이터 pop
5) 반환 주소를 통해 함수를 호출했던 라인으로 복귀(어셈블리 상에서 call [함수이름] 바로 다음 라인)
즉, 함수의 호출은 스택에 해당 함수의 스택 프레임을 쌓는 과정이며 함수의 종료는 스택 프레임을 방출하고 반환주소를 통해 호출 지점으로 돌아가는 과정입니다. 함수 안에서 함수를 호출할 경우, 예를 들어 Func1()에서 Func2() 호출, Func2()에서 Func3()를 호출한다면 Func1() -> Func2() -> Func3() 순서로 스택 프레임이 쌓이며, Func3() 종료 후 Func3() -> Func2() -> Func1() 순서대로 스택프레임을 해제하며 각 함수의 호출 지점으로 돌아가게 됩니다. 한마디로 프로그래밍 언어에서 사용하는 자료구조 stack과 동일한 LIFO(후입선출) 방식입니다.
3. 스택 오버플로우(Stack Overflow)
스택 영역이 동적 할당 영역이라고는 하나, 메모리가 유한한 만큼 한 프로세스가 사용할 수 있는 스택 영역에도 한계가 있습니다. 함수 내에서 함수를 호출하는 과정을 많이 반복하면 스택 프레임이 계속 쌓이게 되고, 스택 영역을 초과하게 되면 프로세스가 오류를 일으키게 됩니다. 따라서 그 전에 예외를 발생시키는데 이를 스택 오버플로우라 하며, 스택 오버플로우 발생 시 프로세스가 강제로 중지됩니다.
int main()
{
Factorial(10000);
}
int Factorial(int num)
{
if (num == 1)
return 1;
else
return num * Factorial(num - 1);
}
상기 코드는 팩토리얼을 계산하는 함수를 간단하게 재귀함수의 형태로 구현한 C++ 코드입니다. 사실 10000! 의 값은 int형으로 담을 수 없을 정도로 큰 값이지만, 실행해보면 그 이전에 스택 오버플로우가 발생하는 것을 확인할 수 있습니다.
메인 함수에서 Factorial(10000)을 호출
-> Factorial(10000)에서 Factorial(9999)을 호출
-> Factorial(9999)에서 Factorial(9998)을 호출
-> ...
위와 같은 흐름으로 실행되므로 Factorial(10000)은 한마디로 Factorial() 함수를 만번, 그것도 기존 호출을 종료하지도 않은 상태로 계속해서 호출하는 것입니다. 결국 스택 프레임이 계속 쌓이다 스택 오버플로우가 발생하였습니다. 이처럼 재귀함수는 특정 내용을 구현하기 편리하지만, 자칫하면 스택 오버플로우를 일으키기 쉽기 때문에 주의해야 합니다. 또한 재귀함수가 아니더라도 꼬리에서 꼬리를 무는 듯한 함수 호출은 스택 오버플로우의 위험이 있으므로 이 또한 주의해야 합니다.
'컴퓨터공학' 카테고리의 다른 글
[컴퓨터공학] 엔디안(Endian) (0) | 2023.03.04 |
---|---|
[컴퓨터공학] 교착 상태(Dead Lock) (0) | 2023.03.04 |
[컴퓨터공학] 스레드의 동기화 (0) | 2023.03.03 |
[컴퓨터 공학] 가상 메모리(virtual memory) (0) | 2023.02.10 |
[컴퓨터 공학] 0.11f * 3 == 0.33f ? (0) | 2023.02.03 |