[pintos] Week4~5: Virtual Memory - Part.4 Stack Growth

프로젝트 2에서 스택은 USER_STACK에서 시작하는 단일 페이지였다. 프로그램은 4KB로 제한하여 실행했다. 이제 스택이 크기를 초과하면 필요에 따라 추가 페이지를 할당한다.

추가 페이지는 스택에 접근하는 경우에만 할당한다. 스택에 접근하는 경우와 아닌 경우를 구별해야 한다.

User program은 스택 포인터 아래의 스택에 쓸 경우 버그가 발생하는데, 이는 일반적인 실제 OS가 스택의 데이터를 수정하는 시그널을 전달하기 위해 프로세스를 언제든지 중단할 수 있기 때문이다. 하지만 x86-64 PUSH 명령어는 스택 포인터를 조정하기 전에 접근 권한을 검사하므로, 스택 포인터 아래 8바이트에 대해서 Page Fault를 발생시킬 수 있다.

구현 목표

User Program의 스택 포인터의 현재 값을 얻을 수 있어야 한다. System Call 또는 User Program에 의해 발생한 Page Fault 루틴에서 각각 syscall_handler()또는 page_fault()에 전달된 struct intr_frame의 rsp멤버에서 검색할 수 있다. 잘못된 메모리 접근을 감지하기 위해 Page Fault에 의존하는 경우, 커널에서 Page Fault가 발생하는 경우도 처리해야 한다. 프로세스가 스택 포인터를 저장하는 것은 예외로 인해 유저 모드에서 커널 모드로 전환될 때 뿐이므로 page_fault()로 전달된 struct intr_frame에서 rsp를 읽으면 유저 스택 포인터가 아닌 정의되지 않은 값을 얻을 수 있다. 유저 모드에서 커널 모드로 전환 시 rsp를 struct thread에 저장하는 것과 같은 다른 방법을 준비해야 한다.


Stack Growth 구현 사전 준비

Thread 구조체에 rsp 필드 선언

  • include/threads/thread.h
#ifdef VM
    struct supplemental_page_table spt; /* 가상 메모리 테이블 */
    /* %rsp 정보 저장 */
    void *rsp;
#endif

Pintos에서 스택 자동 확장(stack growth) 기능을 구현할 때 struct thread에 유저 모드의 rsp 값을 저장해야 하는 이유는,
페이지 폴트가 발생했을 때 정확히 유저 스택 포인터(User Stack Pointer)가 무엇이었는지를 커널이 정확히 알기 위함이다.

페이지 폴트 시에 전달된 struct intr_frame의 rsp는 유저 모드의 스택 포인터가 아닐 수 있기 때문에, 유저 모드 → 커널 모드로 전환될 때의 진짜 rsp 값을 thread 구조체에 저장해두어야 한다.

System Call 발생 시 rsp 저장

  • userprog/syscall.c
void
syscall_handler (struct intr_frame *f UNUSED) {
/* syscall(컨택스트 스위칭) 시 rsp 저장 */
#ifdef VM
	thread_current()->rsp = f->rsp;
#endif
	switch (f->R.rax)
	{
	case SYS_HALT:
		halt(); // 핀토스 종료
		break;
	case SYS_EXIT:
		exit(f->R.rdi);	// 프로세스 종료
		break;
	case SYS_FORK:
        f->R.rax = fork(f->R.rdi, f);
		break;
	case SYS_EXEC:
		f->R.rax = exec(f->R.rdi);
		break;
	case SYS_WAIT:
		f->R.rax = process_wait(f->R.rdi);
		break;
	case SYS_CREATE:
		f->R.rax = create(f->R.rdi, f->R.rsi);
		break;
	case SYS_REMOVE:
		f->R.rax = remove(f->R.rdi);
		break;
	case SYS_OPEN:
		f->R.rax = open(f->R.rdi);
		break;
	case SYS_FILESIZE:
		f->R.rax = filesize(f->R.rdi);
		break;
	case SYS_READ:
		f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
		break;
	case SYS_WRITE:
		f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
		break;
	case SYS_SEEK:
		seek(f->R.rdi, f->R.rsi);
		break;
	case SYS_TELL:
		f->R.rax = tell(f->R.rdi);
		break;
	case SYS_CLOSE:
		close(f->R.rdi);
		break;
	default:
		exit(-1);
	}
}
  • syscall 이 발생할 때 intr_frame에 있던 % rsp 저장 값을 현재 스레드에 저장한다.

Stack Growth 구현

vm_try_handle_fault 수정

이전 포스팅에서 구현했던 vm_try_handle_fault를 수정한다.

  • vm/vm.c
/* Return true on success 
 * 페이지 폴트 핸들러 - 페이지 폴트 발생시 제어권을 전달 받는다.
 * 물리 프레임이 존재하지 않아서 발생한 예외는 not_present 가 true다
 * 그 경우 물리 프레임 할당을 요청하는 vm_do_claim_page를 호출한다.
 * 반대로 not_present 가 false인 경우는 물리 프레임이 할당되어 있지만 폴트가 발생한 것이다.
 * read-only page에 write를 한경우 등 이 때에는 예외 처리를 하면 된다.
 * 그렇다고 해서 not_present가 true인 경우에서 read-only page에 요청을 할 수 있으니 이에
 * 대한 예외를 처리하라
 */
bool
vm_try_handle_fault (struct intr_frame *f UNUSED, void *addr UNUSED,
		bool user UNUSED, bool write UNUSED, bool not_present UNUSED) 
{
	struct supplemental_page_table *spt UNUSED = &thread_current ()->spt;
	struct page *page = NULL;
	/* TODO: Validate the fault */
	/* TODO: Your code goes here */

	// 1. 주소 유효성 검사
	// addr 주소 유효성 검사
	if (addr == NULL || is_kernel_vaddr(addr))
		return false;

	if (not_present) // 접근한 메모리의 physical page가 존재하지 않은 경우
    {
        /* TODO: Validate the fault */
		void *rsp = f->rsp;
		if (!user){
			rsp = thread_current()->rsp;
		}

		if (USER_STACK - (1 << 20) <= rsp - 8 && rsp - 8 <= addr && addr <= USER_STACK)
            vm_stack_growth(addr);
			
        page = spt_find_page(spt, addr);
		
        if (page == NULL)
            return false;

        if (write == 1 && page->writable == 0) // write 불가능한 페이지에 write 요청한 경우
            return false;
			
        return vm_do_claim_page(page);
    }
    return false;
}
  • 기존 페이지 폴트 핸들러에 추가를 한다.
void *rsp = user ? f->rsp : thread_current()->rsp;
if (USER_STACK - (1 << 20) <= rsp - 8 && rsp - 8 <= addr && addr <= USER_STACK)
    vm_stack_growth(addr);

 

  • 조건: 현재 rsp - 8과 addr이 USER_STACK과 1MB 사이의 유효한 스택 영역에 있을 경우
  • vm_stack_growth(addr)를 호출하여 새 페이지를 동적 생성
rsp - 8 조건은 PUSH 명령어 등으로 인해 미리 감소된 스택 포인터를 감안한 것

가상 주소가 해당 영역에 있는 경우 vm_stack_growth(addr)을 호출한다.


vm_stack_growth 구현

  • vm/vm.c
 * 스택 최하단에 익명 페이지를 추가하여 사용
 * addr은 PGSIZE로 내림(정렬)하여 사용 
 * 페이지 주소관련 코드 수정 */
/* Growing the stack. */
static void
vm_stack_growth(void *addr UNUSED)
{
	// 스택 최하단에 익명 페이지 추가
	vm_alloc_page(VM_ANON | VM_MARKER_0 , pg_round_down(addr), 1); 
}

 

vm_stack_growth는 스택 자동 확장 기능을 구현하는 핵심 함수로 유저 프로그램이 아직 할당되지 않은 스택 주소에 접근했을 때 해당 주소를 포함하는 새로운 페이지를 가상 주소 공간에 할당하는 역할을 한다.

 

  • VM_ANON: 파일에 backing 되지 않은 익명 페이지
  • VM_MARKER_0: 이 페이지가 스택용임을 표시하는 마커 (추후 grow stack 가능 여부 판단에 사용됨)
  • true: 쓰기 가능해야 함 (스택은 데이터를 저장하므로)
이 함수는 vm_alloc_page_with_initializer()의 wrapper로, 실제로는 UNINIT 타입이 아닌 즉시 사용할 수 있는 ANON 페이지로 등록된다.

추가 구현

check_address 수정

기존에 userprog에서 사용했던 check_address의 경우 plm4 즉 하드웨어 페이지 테이블을 검사했다. 이제 우리는 vm을 구현하기 때문에 이를 vm용으로 수정해야 한다.

  • userprog/syscall.c
void check_address(void *addr)
{
    // kernel VM 못가게, 할당된 page가 존재하도록(빈공간접근 못하게)
    // if (is_kernel_vaddr(addr) || addr == NULL || pml4_get_page(thread_current()->pml4, addr) == NULL)
    //     exit(-1);
    if (addr == NULL)
		exit(-1);
	if (!is_user_vaddr(addr))
		exit(-1);

}

 

if (addr == NULL)
    exit(-1);

 

  • 사용자 코드에서 NULL 포인터를 전달한 경우
  • 즉시 프로세스 종료 → exit(-1) 호출
if (!is_user_vaddr(addr))
    exit(-1);

 

 

  • addr이 사용자 영역에 속하는 가상 주소인지 확인
  • 커널 영역 주소(0xC0000000 이상) 일 경우 → 보안상 위험 → 즉시 종료

FAIL tests/vm/cow/cow-simple
run: two phys addrs should be the same.: FAILED
pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
pass tests/userprog/rox-child
pass tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/vm/pt-grow-stack
pass tests/vm/pt-grow-bad
pass tests/vm/pt-big-stk-obj
pass tests/vm/pt-bad-addr
pass tests/vm/pt-bad-read
pass tests/vm/pt-write-code
pass tests/vm/pt-write-code2
pass tests/vm/pt-grow-stk-sc
pass tests/vm/page-linear
pass tests/vm/page-parallel
pass tests/vm/page-merge-seq
FAIL tests/vm/page-merge-par
FAIL tests/vm/page-merge-stk
FAIL tests/vm/page-merge-mm
pass tests/vm/page-shuffle
FAIL tests/vm/mmap-read
FAIL tests/vm/mmap-close
FAIL tests/vm/mmap-unmap
FAIL tests/vm/mmap-overlap
FAIL tests/vm/mmap-twice
FAIL tests/vm/mmap-write
FAIL tests/vm/mmap-ro
FAIL tests/vm/mmap-exit
FAIL tests/vm/mmap-shuffle
pass tests/vm/mmap-bad-fd
FAIL tests/vm/mmap-clean
FAIL tests/vm/mmap-inherit
FAIL tests/vm/mmap-misalign
FAIL tests/vm/mmap-null
FAIL tests/vm/mmap-over-code
FAIL tests/vm/mmap-over-data
FAIL tests/vm/mmap-over-stk
FAIL tests/vm/mmap-remove
pass tests/vm/mmap-zero
pass tests/vm/mmap-bad-fd2
pass tests/vm/mmap-bad-fd3
pass tests/vm/mmap-zero-len
FAIL tests/vm/mmap-off
FAIL tests/vm/mmap-bad-off
FAIL tests/vm/mmap-kernel
FAIL tests/vm/lazy-file
pass tests/vm/lazy-anon
FAIL tests/vm/swap-file
FAIL tests/vm/swap-anon
FAIL tests/vm/swap-iter
pass tests/vm/swap-fork
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
pass tests/filesys/base/syn-read
pass tests/filesys/base/syn-remove
pass tests/filesys/base/syn-write
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
FAIL tests/vm/cow/cow-simple
28 of 141 tests failed.