2019년 12월 1일 일요일

스택과 힙 그리고 Stack overflow(메모리 관련)

스택과 힙 (Stack and Heap)

프로그램이 사용하는 메모리는 일반적으로 세그먼트(segment)라고 하는 몇 가지 다른 영역으로 나뉜다.
  • 코드 세그먼트 : 컴파일된 프로그램이 저장되는 영역, 일반적으로 read-only 속성이다.
  • 데이터 세그먼트 : 전역 변수 및 정적 변수가 저장되는 영역
  • 힙 세그먼트 : 동적으로 할당된 변수가 할당되는 영역
  • 스택 세그먼트 : 함수 매개 변수, 지역 변수 및 기타 함수 관련 정보가 저장되는 영역

힙 세그먼트 (Heap segment)

힙 세그먼트는 동적 메모리 할당에 사용되는 메모리를 추적한다. C++에서 new 연산자를 사용해서 메모리를 할당하면 이 메모리는 응용 프로그램의 힙 세그먼트에 할당된다.
int* ptr   = new int; //   ptr은 힙에서 4바이트로 할당된다.
int* array = new int[10]; // array는 힙에서 40바이트로 할당된다.
이 메모리의 주소는 연산자 new에 의해 포인터에 저장될 수 있다. 주의할 점은 이 메모리 요청이 순차적 메모리 주소를 할당하는 결과가 아닐 수 있다는 것이다.
int* ptr1 = new int;
int* ptr2 = new int;
// ptr1과 ptr2은 순차적인 주소가 아닐 수 있다.
동적으로 할당된 변수가 삭제되면 메모리는 힙으로 "반환"되고, 이후 다시 할당될 수 있다. 포인터를 삭제하면 변수가 삭제되는 것이 아니라 관련 주소의 메모리를 운영 체제에 반환하는 것이다.
힙에는 장단점이 있다.
  • 힙에 메모리를 할당하는 것은 비교적 느리다.
  • 할당된 메모리는 명시적으로 할당 해제하거나 응용 프로그램이 종료될 때까지 유지된다. (메모리 릭 주의)
  • 동적으로 할당된 메모리는 포인터를 통해 접근한다: 포인터를 역참조하는 것은 변수에 직접 접근하는 것보다 느리다.
  • 힙은 큰 메모리 풀이므로 큰 배열, 구조체 또는 클래스를 할당할 수 있다.

스택 세그먼트 (Stack segment)

스택 세그먼트(=콜 스택)는 메인() 함수부터 현재 실행 지점까지의 모든 활성 함수를 추적하고 모든 함수 매개 변수와 지역 변수의 할당을 처리한다.
스택은 후입선출(LIFO) 자료구조다. 즉, 가장 늦게 들어간 자료를 가장 먼저 꺼내게 된다. 함수 호출이 끝나고, 이전 함수로 돌아갈 때 이 함수의 바로 이전 함수로 돌아가야 한다. 그래서 컴퓨터는 내부적으로 스택 세그먼트를 스택 자료구조로 구현한다.
콜 스택(call stack) 이란 컴퓨터 프로그램에서 현재 실행 중인 서브루틴(함수)에 관한 정보를 저장하는 스택 자료구조이다. -위키백과-
응용프로그램이 시작되면 메인() 함수가 운영체제에 의해 호출 스택에 푸시된다. 그 후 프로그램이 실행되기 시작한다. 함수호출이 발생하면 함수가 콜 스택에 푸시된다. 현재 함수가 끝나면 해당 함수는 콜 스택에서 해제된다. 따라서 콜 스택에 푸시된 함수를 살펴보면 현재 실행 지점으로 이동하기 위해 호출된 모든 함수를 볼 수 있다.
콜 스택 자체는 고정된 크기의 메모리 영역이다. 여기서 콜 스택에 넣고 빼는 데이터 자체를 스택 프레임(stack frame)이라고 한다. 스택 프레임은 하나의 함수 호출과 관련된 모든 데이터를 추적한다. 또한 스택 포인터(Stack Pointer)라고 하는 CPU의 작은 조각인 레지스터는 현재 호출 스택의 최상위 위치를 가리킨다.

- The call stack in action

콜 스택이 어떻게 작동하는지 좀 더 자세히 살펴보자. 함수를 호출할 때 발생하는 단계는 다음과 같다.
  1. 프로그램에 함수 호출이 발생한다.
  2. 스택 프레임이 생성되고 콜 스택에 푸시된다. 스택 프레임은 다음과 같이 구성된다:
    • 함수가 종료되면 복귀할 주소
    • 함수의 모든 매개 변수
    • 지역 변수
    • 함수가 반환할 때 복원해야 하는 수정된 레지스터의 복사본
  3. CPU가 함수의 시작점으로 점프한다.
  4. 함수 내부의 명령어를 실행한다.
함수가 종료되면 다음 단계가 수행된다:
  1. 레지스터가 콜 스택에서 복원된다.
  2. 스택 프레임이 콜 스택에서 튀어나온다. 이렇게 하면 모든 지역 변수와 매개 변수에 대한 메모리가 해제된다.
  3. 반환 값이 처리된다.
  4. CPU는 반환 주소에서 실행을 재개한다.
반환 값은 시스템 아키텍처에 따라 여러 가지 방법으로 처리한다. 반환 값이 스택 프레임의 일부로 포함되기도 하며, CPU 레지스터를 사용하기도 한다.
일반적으로 콜 스택이 어떻게 동작하는지에 대한 모든 세부 사항이 중요하지는 않다. 그러나 함수가 호출될 때와 종료될 때 함수가 콜 스택에서 효과적으로 작동한다는 것을 이해하면 재귀를 이해할 때와 디버깅할 때 유용하다.

스택 오버플로 (Stack overflow)

스택 세그먼트는 크기가 제한되어 있으므로 제한된 양의 데이터만 저장할 수 있다. window 운영체제에서 기본 스택 세그먼트의 크기는 1MB다. 응용 프로그램이 스택 세그먼트에 너무 많은 정보를 넣으려고 하면 스택 오버플로(stack overflow)가 발생한다. 스택 오버플로(Stack overflow)는 스택 세그먼트의 모든 메모리가 할당되어 꽉 찼을 때 발생하며, 이 경우 추가 할당이 메모리의 다른 섹션으로 넘치기 시작한다.
스택 오버플로는 일반적으로 스택 세그먼트에 너무 많은 변수를 할당하거나 중첩된 함수 호출(A calls function B calls function C calls funct D...)을 너무 많이 한 결과다. 보통 스택 오버플로가 발생하면 프로그램이 다운된다.
다음은 스택 오버플로를 일으킬 수 있는 예제 프로그램이다.
int main()
{
    int stack[100000000];
    return 0;
}
위 프로그램은 스택에 거대한 크기의 배열을 할당하려고 시도한다. 그러나 스택 세그먼트 크기가 배열을 처리할 만큼 충분히 크지 않으므로 배열 할당은 응용 프로그램에서 사용할 수 없는 메모리 부분까지 오버플로 된다. 따라서 프로그램이 다운된다.
다음은 다른 이유로 스택 오버플로를 발생시키는 또 다른 예제 프로그램이다.
void foo()
{
    foo();
}

int main()
{
    foo();

    return 0;
}
위 프로그램은 함수 foo()가 호출 될 때마다 스택 프레임이 스택 세그먼트에 푸시된다. foo() 함수는 무한히 호출되기 때문에 결국에는 스택 메모리가 꽉 차서 스택 오버플로가 발생한다.
  • 스택에 메모리를 할당하는 것은 비교적 빠르다.
  • 스택에 할당된 메모리는 스택 범위에 있을 때만 접근할 수 있다.
  • 스택에 할당된 모든 메모리는 컴파일 타임에 알려진다. 메모리는 변수를 통해 직접 접근할 수 있다.
  • 스택은 비교적 크기가 작으므로 스택 공간을 많이 차지하는 지역 변수를 만드는 것은 좋지 않다.


출처: https://boycoding.tistory.com/235 [소년코딩]