운영체제를 구현하다 보면, 스택은 생각보다 "정적이지 않다"는 사실을 마주하게 된다.
특히 PintOS Project 3의 Stack Growth 구현은 가상 메모리 시스템의 본질을 이해하고 있는지 테스트하는 기능 중 하나이다.
일반적으로 사용자 스택은 실행 시점에 점진적으로 확장됨.
초기부터 1MB 스택(PintOS의 최대 스택 크기)을 통째로 물리 메모리에 올려두는 방식은 너무 비효율적이기 때문이다.
대신, 사용자가 rsp 아래 주소에 접근했을 때,
그 주소에 해당하는 페이지가 아직 물리 메모리에 존재하지 않으면 운영체제는 Page Fault를 발생시키고, 그 시점에 페이지를 새로 할당해서 연결.
이런 방식을 Stack Growth, 또는 Lazy Stack Allocation이라고 부른다고 한다
스택 사용량은 프로그램마다 다르다
자원을 아끼기 위해
Page Fault를 이용한 똑똑한 메모리 할당
Page Fault가 발생했다고 해서 무조건 스택을 확장하진 않는다.
운영체제는 "이 접근이 정말 스택으로의 정당한 접근인가?"를 판단해야 한다.
따라서 addr < USER_STACK - 1MB 라면, 스택 범위를 초과한 잘못된 접근으로 간주하고 return false 해야 한다.
주소가 Stack 영역 범위에 있다고 해서 모두 확장해주지는 않는다.
다음 두 경우에만 Stack Growth를 허용한다.
→ 이 경우는 정상적인 스택 접근으로 판단하고 확장해준다.
→ 역시 Stack Growth를 통해 확장해준다.
→ 보안 및 안정성 문제 때문에 반드시 막아야 하는 접근이다.
f->rsp는 커널 스택의 포인터이기 때문에 사용할 수 없다.
시스템 콜 진입 시점에 유저 스택 포인터를 struct thread에 저장해두어야 한다.
예) thread_current()->rsp_backup = f->rsp;
이후 page fault가 발생했을 때 thread_current()->rsp_backup을 사용해서 정확한 유저 스택 포인터를 가져와야 한다.
| 조건 | Stack Growth 허용 여부 |
|---|---|
| addr < USER_STACK - 1MB | ❌ 스택 영역 초과 |
| addr == rsp - 8 | ✅ PUSH 명령 |
| rsp <= addr < USER_STACK | ✅ 정상 확장 가능 영역 |
| addr < rsp - 8 | ❌ 위험한 접근 |
void foo() {
int big[100000]; // 이 시점에 stack growth 발생 가능
}
이러한 경우에는 사용자 프로세스는 아직 매핑되지 않은 주소에 접근하게 되고,
운영체제는 이를 Page Fault로 감지 → 조건 검증 → 스택 페이지 확장으로 이어진다.
Stack Growth는 단순히 fault가 났다고 처리하는 것이 아니다.
정당한 스택 접근 조건을 만족하는 경우에만 익명 페이지를 할당해서 스택을 확장해줘야 한다.
이를 통해 운영체제는 안정성과 효율적인 메모리 확장을 모두 만족시킬 수 있다.