Project 3 VM 시리즈 3편
SPT에 없는 주소를 실패로 볼지, 새 stack page로 만들지 판단하는 기준을 정리한다.
이번 글은 SPT에 없는 주소를 어떻게 판단할 것인가를 다룬다. VM에서는 SPT에 없다고 바로 죽이면 안 되는 경우가 있다. 대표적으로 stack이 아래 방향으로 자라야 하는 상황이다.
- stack growth 후보를 판단하는 기준을 본다.
- user mode와 kernel mode에서 사용할 rsp가 달라지는 이유를 정리한다.
- syscall의 user pointer 검증이 lazy page와 stack growth를 함께 고려해야 하는 이유를 확인한다.
글의 기준은 week11-team-03-pintos-vm의 pair-a 브랜치다. COW는 이번 구현 범위에서 제외한다. 아래 코드는 전체 파일을 통째로 복붙하기 위한 목록이 아니라, diff에서 의미가 바뀐 핵심 블록을 따라 읽기 위한 기준점이다.
1. SPT에 없으면 무조건 잘못된 주소인가
아니다. 유저 스택은 아래 방향으로 자란다. 현재 stack pointer 근처의 주소에 접근했는데 아직 SPT에 등록되지 않았다면, 커널은 “정말 잘못된 주소인지”와 “새 stack page를 만들어야 하는지”를 구분해야 한다.
rsp현재 함수 호출 흐름에서 stack의 위치를 가리키는 레지스터다.
fault_addrpage fault를 일으킨 가상 주소다.
Stack Growth유저 스택이 필요에 따라 낮은 주소 방향으로 확장되는 동작이다.
User Pointer 검증syscall 인자로 들어온 주소가 유저가 접근 가능한 주소인지 커널이 확인하는 과정이다.
USER_STACK스택의 최상단 기준 주소다.
1MB 제한무한정 자라지 않도록 하한선을 둔다.
rsp 근처fault 주소가 stack pointer 근처여야 한다.
새 page 등록조건을 통과하면 VM_ANON stack page를 만든다.
2. stack growth 후보를 판단하는 함수
pair-a 구현은 stack growth 후보를 별도 함수로 분리했다. 이 분리는 중요하다. page fault handler와 syscall pointer 검증이 같은 기준을 공유할 수 있기 때문이다.
#define USER_STACK_MAX_SIZE (1 << 20)
static bool
is_stack_growth_candidate (void *addr, void *rsp) {
if (addr == NULL || rsp == NULL)
return false;
if (!is_user_vaddr (addr) || !is_user_vaddr (rsp))
return false;
uint8_t *fault_addr = addr;
uint8_t *stack_ptr = rsp;
uint8_t *stack_top = (uint8_t *) USER_STACK;
uint8_t *stack_bottom = stack_top - USER_STACK_MAX_SIZE;
if (fault_addr < stack_bottom || fault_addr >= stack_top)
return false;
if (stack_ptr < stack_bottom || stack_ptr > stack_top)
return false;
if (fault_addr < stack_ptr - sizeof (char *))
return false;
return true;
}
USER_STACK_MAX_SIZE는 stack이 끝없이 커지는 것을 막는다.is_user_vaddr는 커널 주소를 user stack growth로 오해하지 않게 한다.fault_addr >= stack_ptr - sizeof(char *)는 rsp 바로 아래 접근까지 허용하는 기준이다.- 이 함수가 너무 느슨하면 잘못된 주소를 stack으로 만들어 버리고, 너무 엄격하면 정상 stack 접근도 죽는다.
3. page fault에서 stack을 새로 만든다
SPT에서 page를 찾지 못했을 때, 바로 실패하지 않고 stack growth 후보인지 검사한다. 통과하면 새 anonymous page를 만들고 claim한다.
static bool
vm_stack_growth (void *addr) {
void *upage = pg_round_down (addr);
if (!vm_alloc_page (VM_ANON | VM_MARKER_0, upage, true))
return false;
struct page *page = spt_find_page (&thread_current ()->spt, upage);
if (page == NULL || !vm_do_claim_page (page)) {
spt_remove_page (&thread_current ()->spt, page);
return false;
}
return true;
}
bool
vm_try_handle_fault (struct intr_frame *f, void *addr,
bool user, bool write, bool not_present) {
struct page *page = spt_find_page (&thread_current ()->spt, addr);
if (page == NULL) {
void *rsp = user ? f->rsp : (void *) thread_current ()->saved_user_rsp;
if (is_stack_growth_candidate (addr, rsp)) {
vm_stack_growth (addr);
return true;
}
return false;
}
if (write && !page->writable)
return false;
return vm_do_claim_page (page);
}
SPT 조회 실패page가 없다고 바로 종료하지 않는다.
rsp 선택user fault면 intr_frame의 rsp, kernel fault면 저장해 둔 user rsp를 쓴다.
후보 검사stack 범위와 rsp 근처 조건을 확인한다.
page 생성조건을 통과하면 새 stack page를 claim한다.
4. syscall에서 user pointer를 볼 때도 같은 문제가 생긴다
syscall handler는 커널 모드에서 실행된다. 이때 page fault가 나면 CPU의 현재 rsp는 커널 스택을 가리킬 수 있다. 그래서 syscall 진입 시점의 유저 rsp를 따로 저장해 두고, 포인터 검증에서 그 값을 사용한다.
void
syscall_handler (struct intr_frame *f) {
#ifdef VM
thread_current ()->saved_user_rsp = f->rsp;
#endif
/* syscall number와 인자 처리 */
}
static bool
user_addr_accessible_for_syscall (const void *uaddr, bool write) {
if (uaddr == NULL || !is_user_vaddr (uaddr))
return false;
struct page *page = spt_find_page (&thread_current ()->spt, (void *) uaddr);
if (page != NULL) {
if (write && !page->writable)
return false;
return page->frame != NULL || vm_do_claim_page (page);
}
return vm_try_handle_fault (NULL, (void *) uaddr, false, write, true);
}
saved_user_rsp는 syscall 진입 순간의 유저 스택 위치를 보존한다.page != NULL인 경우에는 lazy page를 claim해서 접근 가능하게 만들 수 있다.page == NULL인 경우에도 stack growth 후보라면 새 stack page가 만들어질 수 있다.- syscall pointer 검증은 단순히 주소 범위만 보는 함수가 아니라 VM 상태를 실제로 진행시키는 함수가 된다.
5. 이 단계까지 오면 의미 있는 테스트
이번 글에서 기억할 것
- SPT에 없다고 바로 잘못된 주소라고 판단하면 stack growth가 막힌다.
- stack growth는 주소 범위와 rsp 근처 조건을 함께 봐야 한다.
- syscall에서 pointer를 검사할 때는 현재 커널 rsp가 아니라 저장해 둔 유저 rsp가 필요할 수 있다.
스스로 점검
- 왜 kernel mode page fault에서는
f->rsp대신saved_user_rsp가 필요할 수 있는가? - stack growth 조건이 너무 널널하면 어떤 보안 또는 안정성 문제가 생길 수 있는가?
- user pointer 검증이 lazy page를 claim하는 이유를 설명할 수 있는가?
다음 글 예고
다음 글에서는 실제 물리 메모리인 frame이 부족해졌을 때 어떤 page를 내보낼지, swap disk에 어떻게 저장할지 본다.
Stack growth는 “없는 주소”를 바로 실패로 보지 않고, 유저 스택의 합리적인 확장인지 판단하는 장치다.