[PintOS] Project 3 : VM - Stack Growth - 개념 정리

CorinBeom·2025년 6월 5일

PintOS

목록 보기
12/19
post-thumbnail

PintOS Project 3 - Stack Growth: 스택은 어떻게 자라나는가?

운영체제를 구현하다 보면, 스택은 생각보다 "정적이지 않다"는 사실을 마주하게 된다.
특히 PintOS Project 3의 Stack Growth 구현은 가상 메모리 시스템의 본질을 이해하고 있는지 테스트하는 기능 중 하나이다.


🔍 Stack Growth란?

일반적으로 사용자 스택은 실행 시점에 점진적으로 확장됨.
초기부터 1MB 스택(PintOS의 최대 스택 크기)을 통째로 물리 메모리에 올려두는 방식은 너무 비효율적이기 때문이다.

대신, 사용자가 rsp 아래 주소에 접근했을 때,
그 주소에 해당하는 페이지가 아직 물리 메모리에 존재하지 않으면 운영체제는 Page Fault를 발생시키고, 그 시점에 페이지를 새로 할당해서 연결.

이런 방식을 Stack Growth, 또는 Lazy Stack Allocation이라고 부른다고 한다


🤔 왜 Stack Growth가 필요한가?

  1. 스택 사용량은 프로그램마다 다르다

    • 재귀 함수, 지역 변수, 함수 호출 깊이 등은 전적으로 프로그램 구조에 따라 다름.
  2. 자원을 아끼기 위해

    • 실제로 사용하지도 않을 페이지를 미리 할당해두는 것은 메모리 낭비.
  3. Page Fault를 이용한 똑똑한 메모리 할당

    • 운영체제는 '문제'로 발생한 page fault를 오히려 '기회'로 사용.

📌 Stack Growth: 접근한 주소가 Stack 영역인지 어떻게 판단할까?

Page Fault가 발생했다고 해서 무조건 스택을 확장하진 않는다.
운영체제는 "이 접근이 정말 스택으로의 정당한 접근인가?"를 판단해야 한다.


✅ PintOS에서의 스택 크기 제한

  • 최대 스택 크기: 1MB (1,048,576 bytes)
  • 유저 스택의 최상단 주소는 USER_STACK으로 정의되어 있음
    → 즉, 유저 스택은 USER_STACK부터 아래 방향으로 자람

따라서 addr < USER_STACK - 1MB 라면, 스택 범위를 초과한 잘못된 접근으로 간주하고 return false 해야 한다.


✅ Stack Growth를 허용해야 하는 경우

주소가 Stack 영역 범위에 있다고 해서 모두 확장해주지는 않는다.
다음 두 경우에만 Stack Growth를 허용한다.

1. addr == rsp - 8 인 경우

  • 대표적인 예시: PUSH 명령 실행 시
  • x86-64에서 PUSH 명령은 스택 포인터를 조정하기 전에 접근 권한을 먼저 검사하기 때문에, rsp - 8 주소에서 Page Fault가 발생할 수 있다.
  • 함수 호출, 인자 전달, 로컬 변수 할당 등의 동작에서 자주 발생한다.

→ 이 경우는 정상적인 스택 접근으로 판단하고 확장해준다.


2. rsp <= addr < USER_STACK 인 경우

  • 스택은 위에서 아래로 자라므로, rsp보다 높은 주소에 접근한 경우는 아직 사용되지 않은 영역을 합법적으로 사용하는 것으로 볼 수 있다.

→ 역시 Stack Growth를 통해 확장해준다.


❌ Stack Growth를 허용하지 않는 경우

1. addr < USER_STACK - 1MB

  • 스택 최대 크기 제한을 넘은 접근 → 무조건 잘못된 접근

2. addr < rsp - 8

  • 이건 굉장히 위험한 접근
  • 운영체제가 시그널 처리 등을 위해 유저 스택에 컨텍스트 정보를 저장하는 경우가 있는데,
    그 아래 주소에 사용자가 데이터를 써버리면 해당 정보가 덮어써져서 복구 불가 상태가 될 수 있다.

→ 보안 및 안정성 문제 때문에 반드시 막아야 하는 접근이다.


🧠 그렇다면 rsp는 어떻게 구할까?

유저 모드에서 page fault가 발생한 경우

  • vm_try_handle_fault()에 전달된 struct intr_frame *f의 f->rsp를 그대로 사용하면 된다.

커널 모드(시스템 콜 등)에서 page fault가 발생한 경우

  • f->rsp는 커널 스택의 포인터이기 때문에 사용할 수 없다.

  • 시스템 콜 진입 시점에 유저 스택 포인터를 struct thread에 저장해두어야 한다.
    예) thread_current()->rsp_backup = f->rsp;

  • 이후 page fault가 발생했을 때 thread_current()->rsp_backup을 사용해서 정확한 유저 스택 포인터를 가져와야 한다.


✅ Stack Growth 판단 로직

조건Stack Growth 허용 여부
addr < USER_STACK - 1MB❌ 스택 영역 초과
addr == rsp - 8✅ PUSH 명령
rsp <= addr < USER_STACK✅ 정상 확장 가능 영역
addr < rsp - 8❌ 위험한 접근

🧭 Stack Growth 시나리오 예시

  • 함수에서 큰 지역 배열 사용 시
  void foo() {
    int big[100000]; // 이 시점에 stack growth 발생 가능
  }

이러한 경우에는 사용자 프로세스는 아직 매핑되지 않은 주소에 접근하게 되고,
운영체제는 이를 Page Fault로 감지 → 조건 검증 → 스택 페이지 확장으로 이어진다.


⚠️ Stack Growth 구현 시 주의점

1. 너무 관대한 확장은 위험

  • 낮은 주소를 무한 접근해도 허용된다면, 메모리 남용 및 보안 취약점으로 이어질 수 있음
    → 반드시 최대 크기 제한 필요 (보통 8MB)

2. 너무 엄격한 조건도 문제

  • 함수 호출 시 rsp - 16 정도 접근도 막으면 정당한 프로그램이 죽음
    → rsp - 32 정도까지는 허용해야 함

3. Heap과의 경계 고려

  • Heap은 아래에서 위로, Stack은 위에서 아래로 자람
    → 서로 겹치지 않도록 주소 공간 설계 필요

✅ 마무리

Stack Growth는 단순히 fault가 났다고 처리하는 것이 아니다.
정당한 스택 접근 조건을 만족하는 경우에만 익명 페이지를 할당해서 스택을 확장해줘야 한다.

이를 통해 운영체제는 안정성과 효율적인 메모리 확장을 모두 만족시킬 수 있다.

profile
Before Sunrise

0개의 댓글