프로젝트 2에서는 스택이 USER_STACK
위치(고정)를 시작 주소로 하는 단일 페이지였음. 그리고 프로그램의 실행은 이 사이즈 안에서만으로 한정지어졌다. 이제, 만약 스택이 지금의 사이즈를 넘어서 grow하면 우리는 필요한 만큼 추가 페이지를 할당해준다.
오직 스택 접근으로 나타나는 경우에만 추가 페이지를 할당해라. 스택 접근과 다른 access를 구분하기 위한 시도를 하는 휴리스틱한 방법을 고안해라.
유저 프로그램은 이 프로그램이 스택 포인터 밑으로 스택에 write을 할 때 버그가 생긴다. 왜냐면 전형적인 real OS는 시그널을 보내는 순간에 프로세스에 인터럽트를 거는데, 이는 스택에 데이터를 수정하는 시그널이다. 하지만, x86-64에서 PUSH 인스트럭션은 access permission을 체크한다. 그러고 나면 스택 포인터를 조정하는데, 이는 스택 포인터 아래 8바이트 위치에서 page fault를 일으킨다.
우리는 유저 프로그램의 스택 포인터의 현재 값을 확보할 필요가 있다. 시스템 콜 혹은 유저 프로그램에 의해 발생한 page fault 안에서, 우리는 스택 포인터를 syscall_handler(
)나 page_fault()
를 통해 넘겨받은 struct intr_frame
의 rsp
멤버로부터 다시 회수할 수 있다. 만약 우리가 유효하지 않은 메모리 접근을 탐지하기 위해 page fault에 의존한다면, 우리는 또다른 케이스를 다룰 필요가 있는데, page fault가 커널에서 일어났을 때이다.(시발) 프로세서는 exception이 발생해 유저 모드에서 커널 모드로 스위치를 일으켰을 때만 스택 포인터를 저장하기 때문에, page_fault()
로 전달받은 struct intr_frame
에서 rsp를 읽으면 유저 스택 포인터가 아니라 정의되지 않은 값이 생성될 것이다. 우리는 다른 방식으로 접근할 필요가 있는데, 예를 들어 유저 모드에서 커널 모드로 최초의 transition이 일어났을 때 rsp를 struct thread
구조체에 저장한다던지 등.
stack growth functionalities를 구현해라. 이를 구현하기 위해, 우리는 첫번째로 stack growth를 구분하기 위해 vm_try_handle_fault
를 수정해야 한다. stack growth를 구분한 뒤, 우리는 vm/vm.c
에 있는 vm_stack_growth
를 호출해서 stack을 키운다. vm_stack_growth
를 구현해라.
bool vm_try_handle_fault (struct intr_frame *f, void *addr,
bool user, bool write, bool not_present);
이 함수는 page fault exception을 handling하는 동안
userprog/exception.c
에 있는page_fault()
에 의해 호출된다. 이 함수에서, 우리는 이 page fault가 stack growth에 합당한 case인지 아닌지를 체크할 필요가 있다. 만약 우리가 stack growth로 이 fault를 해결할 수 있음을 확인했다면, fault가 뜬 해당 가상 주소에 대해 vm_stack_growth()를 호출한다.
void vm_stack_growth (void *addr);
하나 이상의 anonymous page를 할당함으로써 스택 크기를 증가시킨다. 이로 인해
addr
은 더이상 fault가 뜬 address가 아니게 된다. allocation을 다룰 때 반드시addr
을 PGSIZE로 round down시켜라.
대부분 OS는 스택 크기에 대해 절대 상한을 두고 있다. 몇몇 OS는 유저에 맞게 limit을 정한다. 예를 들어, UNIX 시스템에서는 ulimit 커맨드가 있다. 많은 GNU/Linux 시스템에서, 디폴트 제한은 8MB이다. 이 프로젝트에서, 우리는 반드시 스택 사이즈를 최대 1MB로 제한해야 한다.