포스트

[CS] 가상 메모리

[CS] 가상 메모리

스택과 힙의 메모리 주소 관계 및 실제 배치

1. 가상 메모리(Virtual Memory)란?

가상 메모리는 운영체제가 제공하는 메모리 관리 기법으로, 각 프로세스마다 독립적인 메모리 공간을 제공하여 실제 물리 메모리와 무관하게 연속된 주소 공간을 사용하는 것처럼 보이게 한다. 이를 통해 다음과 같은 이점이 있다.

  • 프로세스 간 메모리 보호: 한 프로세스가 다른 프로세스의 메모리에 접근하지 못하도록 보호한다.
  • 주소 공간 독립성: 각 프로세스는 자신의 가상 메모리 주소 공간을 가지며, 다른 프로세스와 충돌 없이 독립적으로 실행된다.
  • 메모리 확장: 실제 물리 메모리가 부족하더라도 디스크의 일부를 사용하여 부족한 메모리를 보완(스왑)할 수 있다.
  • 효율적인 메모리 사용: 필요한 부분만 물리 메모리에 로드하고 나머지는 디스크에 둠으로써 효율적인 메모리 사용이 가능하다.

운영체제는 가상 메모리를 페이지(page) 단위로 나누고, 페이지 테이블(Page Table)을 통해 가상 주소를 물리 주소로 변환하여 실제 메모리에 접근하도록 한다.

2. “스택은 높은 주소, 힙은 낮은 주소”라는 말의 의미

일반적으로 스택은 높은 주소부터, 힙은 낮은 주소부터 부여된다고 알려져 있다. 이는 가상 메모리 주소 공간(Logical/Virtual Address Space) 기준의 설명이다.

가상 메모리 기준으로 흔히 그리는 구조

1
2
3
4
5
6
7
8
9
높은 주소
+-------------------+
|      스택(Stack)  |  ↓ 밑으로 성장
|-------------------|
|      ...          |
|-------------------|
|      힙(Heap)     |  ↑ 위로 성장
+-------------------+
낮은 주소
  • 스택(Stack): 함수 호출이나 지역 변수 생성 시, 높은 주소에서 낮은 주소 방향으로 확장(감소)한다.
  • 힙(Heap): new, malloc() 등으로 동적 메모리를 할당하면, 낮은 주소에서 높은 주소 방향으로 확장(증가)한다.

이는 가상 주소 공간에서의 배치 모습일 뿐이며, 물리 메모리 주소와 직접적인 관계는 없다.

3. 가상 주소와 물리 주소는 다르다

실제 메모리(RAM)에는 스택이 위, 힙이 아래로 정렬되어 들어가는 것이 아니다. 운영체제가 페이지 테이블(Page Table)을 통해 가상 주소를 물리 주소에 매핑하기 때문에, 가상 주소 공간에서 스택이 힙보다 높다고 해서, 물리 주소에서도 스택이 힙보다 높은 주소에 있다는 보장은 없다.

예시

가상 주소 (논리 주소)물리 주소 (실제 RAM)
스택의 0x7FFFxxxx0x20000000
힙의 0x600000000x30000000

위와 같이 가상 주소는 스택이 힙보다 높지만, 물리 주소에서는 힙이 더 높은 경우도 충분히 있을 수 있다.

4. 스택과 힙의 물리 주소 관계는 OS와 상황에 따라 다르다

같은 프로그램을 여러 번 실행해도 물리 메모리 배치는 달라질 수 있다. 페이지 테이블에 의해 가상 주소와 물리 주소가 동적으로 매핑되므로, 스택이 항상 물리적으로 더 높은 주소에 있다고는 단정할 수 없다.

5. 결론

기준스택이 힙보다 항상 높은가?
가상 주소보통 높음 (높은 주소부터 내려감)
물리 주소항상 높다고 할 수 없음 (OS가 임의 배치)
  • 가상 주소 공간에서는 스택이 힙보다 항상 높다.
  • 하지만 물리 주소에서는 스택이 힙보다 항상 높다고 보장할 수 없다.
  • 페이지 테이블이 가상 주소와 물리 주소를 동적으로 매핑하기 때문이다.


인위적 연속성(artificial continuity)이란 가상기억장치의 개념에서 가상공간의 연속적 주소가 실제 물리적인 공간상에서 연속일 필요가 없다는 것을 뜻하는 말이다. 사상 표를 통해서 가상기억장치의 주소로부터 실기억장치의 주소를 알 수 있다.


메모리 관리와 힙, 스택의 동작 방식 정리

1. 힙 메모리는 실제 메모리(RAM) 이상으로 할당이 가능한가?

힙 메모리는 이론적으로 계속 늘어날 수 있다고 표현하기도 하지만, 실제로는 컴퓨터에 장착된 물리 메모리(RAM)와 스왑 공간(디스크)이 합쳐진 만큼만 사용할 수 있다.

  • 힙 메모리는 필요에 따라 동적으로 증가하지만, 운영체제가 실제 메모리를 할당해줘야만 가능하다.
  • 물리 메모리(RAM)가 부족하면 스왑 메모리를 활용해 가상 메모리로 확장할 수 있다.
  • 스왑까지 모두 사용해도 부족하면 메모리 부족(Out of Memory, OOM) 상태가 되어 프로그램이 비정상 종료되거나 운영체제가 강제로 프로세스를 종료시킬 수 있다.

즉, 힙 메모리는 무한정 사용할 수 있는 것이 아니라, 시스템 자원(RAM + 스왑)의 한계 내에서만 사용 가능하다.

2. 프로그램 간 스택 메모리 사용 충돌 여부

프로그램마다 스택 메모리는 독립적으로 할당되며, 한 프로그램이 스택을 많이 쓴다고 해서 다른 프로그램의 스택 공간이 줄어드는 것은 아니다.

  • 각 프로그램(프로세스)마다 독립적인 가상 메모리 공간을 가지고 있고, 스택 크기도 각자 설정된다.
  • 하지만, 시스템 전체 메모리가 부족해지면 간접적으로 영향을 받을 수 있다. 예를 들어, 어떤 프로그램이 너무 많은 메모리를 사용해 시스템 자원이 부족해지면, 다른 프로그램도 메모리 할당에 실패할 수 있다.

3. 힙 메모리가 높은 주소에서 시작하는 시스템 여부

과거 일부 시스템에서는 힙 메모리가 높은 주소에서 시작해 아래쪽으로 자라는 구조를 사용한 경우도 있었다.

  • 초기 매킨토시(Mac OS Classic), 일부 임베디드 시스템에서는 힙이 높은 주소에서 시작하는 경우가 있었다.
  • 현대적인 운영체제(Windows, Linux 등)에서는 힙이 일반적으로 낮은 주소에서 시작하여 높은 주소로 자란다.

4. 스택이 높은 주소에서 시작하는 이유와 커널 보호와의 관계

스택이 높은 주소에서 시작해 아래로 내려오는 구조는 커널 보호가 1차적인 이유라기보다는, 다음과 같은 실용적인 이유가 크다.

  • 스택은 함수 호출 시마다 새로운 스택 프레임(지역 변수, 리턴 주소 등)이 추가되므로, 유동적으로 증가하는 특성상 높은 주소에서 시작해 내려오는 방식이 효율적이다.
  • 다만, 메모리 보호 측면에서도 이런 설계가 도움이 되는 측면이 있다. 과거에는 스택 오버플로우가 발생해 커널 영역까지 침범하는 문제가 있었는데, 현대 운영체제는 사용자 공간과 커널 공간을 명확하게 분리하여 이런 위험성을 줄였다.

즉, 스택이 높은 주소에서 시작하는 설계는 유동적으로 자라나는 스택의 특성과 맞물려 효율적이기 때문이고, 결과적으로는 메모리 보호 측면에서도 유리한 구조가 된 것이다.

5. 스택과 힙의 동작 차이

스택과 힙은 모두 메모리 할당 영역이지만 동작 방식에 차이가 있다.

구분스택(Stack)힙(Heap)
할당 방식함수 호출 시 자동 할당프로그래머가 명시적으로 new, malloc 등으로 할당
성장 방향높은 주소 → 낮은 주소낮은 주소 → 높은 주소 (현대 시스템 기준)
사용 용도지역 변수, 함수 호출 정보 저장동적 메모리 할당(런타임 크기 결정)
자동 해제 여부함수 종료 시 자동 해제프로그래머가 delete, free 등으로 해제 필요
크기 설정프로그램 실행 시 고정 크기로 설정됨필요에 따라 동적으로 증가(그러나 시스템 한도 존재)

6. 유동적이라는 표현의 의미

  • 스택이 유동적이라는 의미는 함수 호출과 지역 변수 생성에 따라 자동으로 증가 및 감소하기 때문에 나온 표현이다.
  • 힙도 동적으로 증가할 수 있지만, 스택처럼 자동으로 늘어나는 것이 아니라 프로그래머가 명시적으로 요청해야 한다는 점에서 차이가 있다.

7. 결론

  • 힙 메모리는 무한정 늘어나는 것이 아니라 시스템 자원(RAM + 스왑)의 한도 내에서만 사용 가능하다.
  • 프로그램마다 스택 메모리는 독립적으로 설정되지만, 시스템 메모리 부족 시 간접적인 영향은 받을 수 있다.
  • 과거에는 힙이 높은 주소에서 시작하는 시스템도 있었지만, 현대 시스템에서는 낮은 주소에서 시작하는 것이 일반적이다.
  • 스택이 높은 주소에서 시작하는 것은 유동적으로 증가하는 스택의 특성과 효율적으로 맞아떨어지는 구조이며, 결과적으로 커널 보호에도 유리한 설계가 되었다.

실제 주소 비교해보기 (리눅스에서)

리눅스에서는 /proc/self/pagemap을 활용해 가상 주소와 물리 주소를 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <fstream>

#define PAGE_SIZE 4096

uintptr_t get_physical_address(uintptr_t virtual_address) {
    std::ifstream pagemap("/proc/self/pagemap", std::ios::binary);
    if (!pagemap) {
        std::cerr << "Failed to open /proc/self/pagemap" << std::endl;
        return 0;
    }

    uintptr_t page_index = virtual_address / PAGE_SIZE;
    uintptr_t offset = page_index * sizeof(uint64_t);

    pagemap.seekg(offset, std::ios::beg);
    uint64_t entry;
    pagemap.read(reinterpret_cast<char*>(&entry), sizeof(entry));

    if (!(entry & (1ULL << 63))) {
        std::cerr << "Page not present in memory" << std::endl;
        return 0;
    }

    uintptr_t physical_address = (entry & ((1ULL << 55) - 1)) * PAGE_SIZE + (virtual_address % PAGE_SIZE);
    return physical_address;
}

int main() {
    int stackVar = 42;
    int* heapVar = new int(100);

    uintptr_t stack_virtual = reinterpret_cast<uintptr_t>(&stackVar);
    uintptr_t heap_virtual = reinterpret_cast<uintptr_t>(heapVar);

    uintptr_t stack_physical = get_physical_address(stack_virtual);
    uintptr_t heap_physical = get_physical_address(heap_virtual);

    std::cout << "Stack Virtual Address: " << std::hex << stack_virtual << std::endl;
    std::cout << "Stack Physical Address: " << std::hex << stack_physical << std::endl;

    std::cout << "Heap Virtual Address: " << std::hex << heap_virtual << std::endl;
    std::cout << "Heap Physical Address: " << std::hex << heap_physical << std::endl;

    delete heapVar;

    return 0;
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.