요약

공책 한 권을 두 과목이 같이 쓸 때, 한 과목은 앞장부터 쓰고 다른 과목은 뒷장부터 거꾸로 쓰면 어느 쪽이 얼마나 쓸지 미리 정하지 않아도 가운데 빈 장을 끝까지 나눠 쓴다. 프로그램이 받은 메모리도 이렇게 쓴다. 맨 아래에는 명령들과, 프로그램 내내 자리가 바뀌지 않는 값을 둔다. 함수를 부를 때 쌓는 쪽지 더미는 맨 위에서 아래로 자라고, 실행 중에 달라고 해서 받는 자리는 아래에서 위로 자란다. 둘이 가운데서 만나면 더 쓸 곳이 없다.

예시로 보기

데이터 정렬과 패딩에서 변수 하나의 모양을, 활성 레코드에서 함수 호출 하나의 모양을 정했다. 남은 물음은 “그것들을 메모리의 어디에 둘 것인가”다. 코드 생성기는 변수를 읽는 명령을 만들 때 그 변수가 어느 구역의 몇 번지에 있는지 알아야 한다. 이 배치도 컴파일러가 정한다.

C 코드 한 조각에 든 것들이 어디로 가는지 본다.

int count = 0;                         // 전역 변수

int main() {
    int a = 3;                         // 지역 변수
    int* p = (int*)malloc(sizeof(int) * 10);   // 실행 중에 받는 자리
    free(p);
}

프로그램이 이어진 메모리 한 덩어리를 쓴다고 치고, 낮은 번지부터 네 구역으로 나눈다.

 높은 번지  +--------------------------------------+
            |  스택: 활성 레코드                    |   main의 레코드 (a, p)
            |            |                         |
            |            v  아래로 자란다           |
            |                                      |
            |          (빈 곳)                     |
            |                                      |
            |            ^  위로 자란다             |
            |            |                         |
            |  힙: 실행 중에 받은 자료              |   malloc이 준 int 10칸
            +--------------------------------------+
            |  전역 데이터: 번지가 고정된 자리      |   count
            +--------------------------------------+
            |  코드                                |   main의 명령들
 낮은 번지  +--------------------------------------+

p라는 변수 자체는 main의 레코드(스택)에 있고, p에 든 주소가 가리키는 열 칸은 힙에 있다. main이 끝나면 레코드와 함께 p는 사라지지만, 힙의 열 칸은 free를 부를 때까지 남는다.

검증: 네 구역의 순서(코드 < 전역 < 힙 < 스택)를 작은 모형으로 만들어, 부를 때마다 스택 꼭대기 번지가 줄고 받을 때마다 힙 번지가 늘어남, 전역 자리는 그대로임, 무작위로 부르고 돌아가고 받아도 구역이 겹치지 않음, 마주 보고 자랄 때와 반으로 갈라 줄 때의 차이(빈 공간 12칸에서 42가지 경우) — 50_runtime-memory-layout_verify.py

네 구역

컴파일러는 코드와 자료를 메모리에 어떻게 둘지 정한다. 여기서는 프로그램이 이어진 메모리 공간 하나를 쓴다고 가정한다.

구역 든 것 크기가 정해지는 때 자리가 생기고 없어지는 때
코드 기계어 명령 컴파일할 때 프로그램 시작부터 끝까지
전역 데이터 전역 변수 컴파일할 때 프로그램 시작부터 끝까지
힙(heap) 실행 중에 달라고 해서 받은 자료 실행 중 malloc으로 받을 때 생기고 free로 돌려줄 때 없어진다
스택 활성 레코드 실행 중 함수를 부를 때 생기고 함수가 끝나면 없어진다

전역 변수. 전역 변수는 어디서 읽든 같은 하나를 가리켜야 한다. 호출마다 새로 생기는 활성 레코드에는 둘 수 없다. 그래서 바뀌지 않는 번지를 하나 정해 준다. 컴파일할 때 자리를 정해 두는 것을 정적 할당(static allocation)이라 부른다.

활성 레코드. 스택으로 관리하고, 스택은 높은 번지에서 낮은 번지 쪽으로 자란다.

실행 중에 받는 자료. C에서 int* a = (int*)malloc(sizeof(int) * 10);을 실행하면 a가 가리킬 자리가 실행 중에 생긴다. free(a)를 부르면 그 자리가 실행 중에 없어진다. 이런 자료를 동적 할당된 자료라 부르고, 힙에서 관리한다. 힙은 낮은 번지에서 높은 번지 쪽으로 자란다. 스택과 힙이 겹치지 않게 하려는 것이다.

왜 마주 보고 자라게 하는가. 스택과 힙은 얼마나 커질지 실행해 봐야 안다. 빈 곳을 반으로 갈라 한쪽씩 주면, 한쪽이 자기 몫을 넘는 순간 다른 쪽이 텅 비어 있어도 못 쓴다. 양 끝에서 마주 보고 자라게 하면 둘의 합이 빈 곳의 크기를 넘지 않는 한 어떤 비율로든 쓸 수 있다. 빈 곳이 12칸일 때 힙 10칸과 스택 2칸은 마주 보는 배치에서는 들어가고, 6칸씩 갈라 준 배치에서는 들어가지 않는다1.

지켜 주는 것과 지켜 주지 않는 것

지켜 주는 것.

  • 코드와 전역 변수의 번지는 실행 내내 바뀌지 않는다. 컴파일러는 그 번지를 명령에 바로 적어 넣을 수 있다.
  • 스택과 힙이 만나기 전까지는 네 구역이 서로 겹치지 않는다.
  • 컴파일러는 컴파일할 때 코드와 자료의 배치를 정하고, 목표 자료가 있는 곳을 맞게 찾아가는 코드를 만든다.

지켜 주지 않는 것.

  • 가운데 빈 곳이 바닥나지 않는다는 보장은 없다. 재귀가 너무 깊거나 힙에서 너무 많이 받으면 둘이 만난다1.
  • 힙에서 받은 자리는 저절로 없어지지 않는다. C에서는 free를 불러야 한다. 부르지 않으면 그 자리는 가리키는 변수가 사라진 뒤에도 차지된 채로 남는다1.
  • 지역 변수의 번지는 고정되어 있지 않다. 같은 함수라도 스택이 얼마나 쌓인 뒤에 불렸느냐에 따라 레코드가 놓이는 번지가 다르다. 그래서 지역 변수는 번지가 아니라 “스택 꼭대기에서 몇 칸”으로 찾는다(단순 연산과 분기의 코드 생성).

활용

  • 변수가 어느 구역에 가는지는 선언된 자리와 만드는 방법이 정한다. 함수 밖에 선언하면 전역 데이터, 함수 안에 선언하면 스택, malloc으로 받으면 힙이다.
  • 흔한 실수: int* p = malloc(...)의 p도 힙에 있다고 답한다. p는 주소를 담은 지역 변수라 스택에 있고, p가 가리키는 곳이 힙이다.
  • 흔한 실수: 스택이 “위로 쌓인다”고 번지가 커진다고 생각한다. 그림에서 스택은 높은 번지에서 시작해 번지가 줄어드는 쪽으로 자란다. 스택에 한 칸을 올리는 명령이 스택 꼭대기 번지에서 4를 빼는 까닭이다(함수 호출의 코드 생성).

연결

확인 문제

C1 메모리의 네 구역을 낮은 번지부터 쓰고, 스택과 힙이 자라는 방향을 쓰라.

답: 코드, 전역 데이터(정적으로 할당한 자리), 힙, 스택. 힙은 낮은 번지에서 높은 번지로, 스택은 높은 번지에서 낮은 번지로 자란다.
이유: 크기가 컴파일할 때 정해지는 둘을 아래에 고정하고, 실행 중에 크기가 바뀌는 둘을 남은 공간의 양 끝에 둔다.

C2 스택과 힙을 같은 방향으로 나란히 자라게 하지 않고 마주 보게 하는 까닭은?

답: 둘 다 얼마나 커질지 미리 모른다. 마주 보게 하면 빈 곳을 미리 가르지 않아도 되고, 둘의 합이 빈 곳을 넘지 않는 한 한쪽이 크게 자라도 된다.
이유: 같은 방향으로 자라게 하려면 둘 사이의 경계를 미리 정해야 한다. 그러면 한쪽이 경계에 닿았을 때 다른 쪽에 빈자리가 남아 있어도 쓰지 못한다. 흔한 오답은 “스택이 더 중요해서 위에 둔다”인데, 위아래가 아니라 서로 반대쪽 끝에서 출발한다는 것이 요점이다.

C3 다음이 각각 어느 구역에 있는지 말하라. ① 함수 밖에 선언한 int total; ② fib 안의 임시 변수 t0 ③ malloc(40)이 돌려준 주소가 가리키는 40바이트 ④ 그 주소를 받아 둔 지역 변수 p ⑤ fib의 명령들 ⑥ fib가 끝난 뒤 돌아갈 주소

답: ① 전역 데이터 ② 스택(그 호출의 활성 레코드) ③ 힙 ④ 스택 ⑤ 코드 ⑥ 스택(활성 레코드의 저장한 기계 상태 칸).
이유: ③과 ④를 가르는 것이 요점이다. 주소가 가리키는 곳과 주소를 담은 변수는 다른 구역에 있다. ⑥의 돌아갈 주소는 코드 구역의 한 곳을 가리키는 값이지만, 그 값을 적어 둔 칸은 스택에 있다.

  1. 수치는 검증 코드의 모형으로 확인했다. ↩ ↩2 ↩3