언제나 느끼지만 gitbook을 보는 것이 진짜진짜진짜 가장 중요하다.
그냥 보는 게 아니라 꼼꼼하게, 그리고 자주 보는 것이 중요하다.
gitbook에서는 이렇게 설명한다.
Project 2 까지는 스택이
USER_STACK에서 시작하는 단일 페이지였고 , 프로그램 실행은 이 크기로 제한되었습니다.
이제 스택이 현재 크기를 초과하면 필요에 따라 추가 페이지를 할당합니다.
그렇다.
이전까지는 stack 영역이 그저 4KB의 하나의 페이지였던 것이다.
이번 과제에서는 이 가짜 스택을 진짜 스택으로 만들어줄 것이다.
스택은 아래로 확장되는데, 확장되기 위해서는 현재 유효한 스택 영역을 벗어난 접근을 잘 캐치해서 이를 스택 확장으로 처리해주어야 하겠지??
git book을 좀 더 따라가보자.
User programs are buggy if they write to the stack below the stack pointer,
because typical real OSes may interrupt a process at any time to deliver a "signal," which modifies data on the stack.
However, the x86-64 PUSH instruction checks access permissions before it adjusts the stack pointer,
so it may cause a page fault 8 bytes below the stack pointer.사용자 프로그램이 스택 포인터 아래의 스택에 쓰면 버그가 발생합니다,
일반적인 실제 운영 체제는 스택의 데이터를 수정하는 "signal"를 전달하기 위해 언제든지 프로세스를 중단할 수 있기 때문입니다.
그러나 x86-64 PUSH 명령어는 스택 포인터를 조정하기 전에 접근 권한을 확인합니다,
따라서 스택 포인터 아래 8바이트에서 page fault가 발생할 수 있습니다.
page fault가 발생하는 지점(위치)는 두 가지이다.
이를 좀 더 잘 이해해보자.
근본적으로 Stack 영역에는 어떤 데이터가 들어올까?
런타임 중 생성되는 지역 변수들이 차곡차곡 쌓여 연산이 종료되는 등의 쓰임이 다하면 pop 된다.
즉, 지역 변수들이 저장된다.
지역 변수는 보통 char, int, pointer 등의 타입으로 한 줄씩 write 인스트럭션이 실행된다.
만약, 배열이 선언되었을 경우에는 선언 시점에서 그 크기만큼 rsp를 낮은 주소로 *이동시키고 배열의 특정 인덱스에 값을 할당하는 시점에서 page fault가 발생된다.
(이 때 rsp를 이동시키는 것은 sub 인스트럭션으로 rsp를 감소시킨 것으로 실제 rsp에 해당하는 주소의 물리 메모리에 접근한 것은 아니다.)
그렇다는 것은,
page fault 시점의 virtual address가 스택 확장의 목적의 fault인 것이 확인된다면, 스택 포인터(rsp)만큼 스택을 확장시켜주면 된다.
예컨대,
void foo() {
int arr[1000]; // 총 4000바이트 필요
arr[999] = 42;
}
위와 같은 foo 프로그램을 로드하고 arr 배열을 선언한다.
arr[999] = 42에서 직접 arr[999]에 접근할 때, page fault가 생긴다.
이 때의 rsp, fault address를 보면
int arr[1000];
이 시점에서 rsp는 4000 바이트만큼 낮은 주소로 이동된다.
arr[999] = 42;
이 시점에서 page fault가 발생하며 fault address는 arr[999]의 VA가 된다.
따라서 fault address는 당연하게도 rsp 보다는 같거나 높은 주소가 될 것이다.
위의 지역 변수 케이스와는 다르다.
위에서는 rsp를 먼저 이동시키고 그 이후에 VA에 접근하게 되며 page fault가 발생하게 된다.
하지만 x86-64의 PUSH 인스트럭션은
스택 포인터(rsp)를 조정하기 전에 물리 메모리의 접근 권한을 확인한다.
따라서,
rsp는 여전히 더 높은 주소에 있을지라도, 그보다 8bytes만큼 낮은 주소로 직접 접근을 시도하여 page fault가 발생한다는 의미이다.
따라서 이 두 케이스에 대한 처리를 모두 고려해주어야 한다.
그리고 git book에서는 page fault가 발생한 시점을 언급한다.
즉, kernel context의 시점인지 user context의 시점인지를 구분한다.
구분해야하는 이유는 어느 시점에서 page fault가 발생했는지에 따라,
intr_frame이 달라지기 때문이다.
user stack의 확장을 기대한 page fault임은 동일하다고 가정하자.
- user context에서 page fault가 발생했다면 user context의 intr_frame을 가지고 page fault 처리를 하게된다.
- 반면, kernel context에서 page fault가 발생했다면 kernel의 intr_frame을 가지고 page fault 처리를 하게된다.
kernel의 intr_frame을 참조하여 rsp를 확장시켜주는 것은 무의미하다. 우리가 확장하고자 하는 것은 user stack임을 명심하자.
그렇다면 kernel context에서 user stack을 확장시키기 위해 page fault가 발생되는 경우가 어느 때일까?
user mode에서 접근할 수 없는 어떠한 처리를 위해 kernel mode로 진입하려는 시점이 되겠다.
이는 딱 하나다. system call을 호출한 경우이다.
따라서,
system call이 호출되었을 때 user context의 rsp를 어딘가에 저장해두었다가 page fault를 처리할 때는 이것을 꺼내쓰면 될 것이다.
git book에는 이렇게 안내한다.
Most Oses impose some absolute limit on stack size.
Some OSes make the limit user-adjustable, e.g. with the ulimit command on many Unix systems.
On many GNU/Linux systems, the default limit is 8 MB.
For this project, you should limit the stack size to be 1MB at maximum.대부분의 OS는 스택 크기에 절대적인 제한을 부과합니다. 예를 들어, 많은 유닉스 시스템에서 ulimit 명령어를 사용하여 제한을 사용자가 조정할 수 있도록 하는 OS도 있습니다.
많은 GNU/Linux 시스템에서 기본 제한은 8MB입니다.
이 프로젝트의 경우 스택 크기를 최대 1MB로 제한해야 합니다.
오호 그렇군
스택의 시작 주소는 알다시피, USER_STACK 매크로로 정의되어있다.
USER_STACK에서 1MB 낮은 주소까지 스택 영역으로 쓸 수 있다. 더 낮아지면 안 된다!!
여기까지 이해했다면 구현을 시도해도 된다.
struct thread우선 위에서 말했듯, "system call이 호출되었을 때 user context의 rsp를 어딘가에 저장" 해야 하는데
이는 당연하게도 thread 구조체에서 관리하는 게 좋겠다.
우리 팀은 아래와 같이 stack_ptr 을 추가해주었다.
struct thread {
.
.
.
#ifdef VM
/* Table for whole virtual memory owned by thread. */
struct supplemental_page_table spt;
void* stack_ptr;
void* stack_bottom;
#endi`
.
.
.
syscall_handler()system call을 호출하며 kernel mode로 변환된 상태로 page fault가 발생할 경우를 대비해야한다.
이 함수에서 위에서 추가해준 stack_ptr 변수에 user context의 rsp를 저장해둔다.
void
syscall_handler(struct intr_frame* f UNUSED) {
/* %rdi, %rsi, %rdx, %r10, %r8, %r9 */
int syscall_number = (int)f->R.rax;
thread_current()->stack_ptr = f->rsp`
.
.
.
vm_try_handle_fault()page fault가 발생하면 vm_try_handle_fault()로 진입한다.
static void
page_fault(struct intr_frame* f) {
bool not_present; /* True: not-present page, false: writing r/o page. */
bool write; /* True: access was write, false: access was read. */
bool user; /* True: access by user, false: access by kernel. */
void* fault_addr; /* Fault address. */
/* Obtain faulting address, the virtual address that was
accessed to cause the fault. It may point to code or to
data. It is not necessarily the address of the instruction
that caused the fault (that's f->rip). */
fault_addr = (void*)rcr2();
/* Turn interrupts back on (they were only off so that we could
be assured of reading CR2 before it changed). */
intr_enable();
/* Determine cause. */
not_present = (f->error_code & PF_P) == 0;
write = (f->error_code & PF_W) != 0;
user = (f->error_code & PF_U) != 0;
#ifdef VM
/* For project 3 and later. */
if ((!not_present && write) || (fault_addr < 0x400000 || fault_addr >= USER_STACK))
{
exit(-1);
}
if (vm_try_handle_fault(f, fault_addr, user, write, not_present))
return;
#endif
.
.
.
우리는
kernel context일 경우, user의 rsp를 참조하고
user context일 경우, 인자로 받은 intr_frame* f를 참조하면 된다.
void* rsp = (void*)f->rsp; // user mode
if (!user)
rsp = thread_current()->stack_ptr; // kernel mode - system call
좋다.
그러면 이제 이 rsp와 fault가 발생한 addr을 보고, 이게 과연 stack growth에 적합한 address인지 확인해주어야 한다.
또 하나 고려해야할 점은 스택 최대 크기이다.
스택은 1MB를 넘어서는 안 된다.
그러면 정리해보자.
rsp-8 주소는 허용한다.USER_STACK 보다 낮은 주소여야 한다.USER_STACK - 1MB 보다 높은 주소여야 한다.
if (addr >= rsp - 8 && // push, call의 stack 직접 접근을 고려한 8bytes의 마진을 허용
addr >= USER_STACK - STACK_MAX &&
addr <= USER_STACK) { // stack의 최대 size를 1MB로 제한
...
}
이렇게 해주면 되겠다.
stack growth에 적합한지 확인했다면, vm_stack_growth를 호출한다.
.
.
전체 코드는 아래와 같다.
bool
vm_try_handle_fault(struct intr_frame* f UNUSED, void* addr UNUSED,
bool user UNUSED, bool write UNUSED, bool not_present UNUSED) {
/* TODO: Validate the fault */
/* TODO: Your code goes here */
if (addr == NULL || is_kernel_vaddr(addr)) return 0;
if (!not_present) return 0;
/* stack growth */
void* rsp = (void*)f->rsp; // user mode
if (!user)
rsp = thread_current()->stack_ptr; // kernel mode - system call
if (addr >= rsp - 8 && // push, call의 stack 직접 접근을 고려한 8bytes의 마진을 허용
addr >= USER_STACK - STACK_MAX && addr <= USER_STACK) { // stack의 최대 size를 1MB로 제한
vm_stack_growth(addr);
return 1;
}
/* validate and do claim */
struct supplemental_page_table* spt UNUSED = &thread_current()->spt;
struct page* page = spt_find_page(spt, addr);
if (page == NULL) {
return 0;
}
if (write && !page->rw_w) return 0;
return vm_do_claim_page(page);
}
vm_stack_growth()마지막이다! gitbook을 보자.
- 주소가 더 이상 결함이 있는 주소가 되지 않도록 하나 이상의 익명 페이지를 할당하여 스택 크기를 늘립니다.
- 할당을 처리할 때 주소를 PGSIZE로 반올림해야 합니다.
이 함수의 역할은 심플하다.
static void
vm_stack_growth(void* addr UNUSED) {
void* pg_addr = pg_round_down(addr);
if (!vm_alloc_page(VM_ANON | VM_MARKER_0, pg_addr, 1)
|| !vm_claim_page(pg_addr)) {
PANIC("Stack growth failed");
}
}

이번 과제에서는 gitbook이 너무너무 안 읽혀서
함수 설명 부분만 읽고 코드 구현을 했더니 이해 안 되는 것들 투성이에 fail의 연속이었다.
돌고돌아 gitbook에서 해답을 찾는다.
아 그리고,
system call에서 유효한 주소인지 검증하는 함수로 check_address()를 두었는데 이 함수는 VM 단계에서 수정해야한다.
이 때문에도 오랜 시간 애먹었다.
#ifndef VM
void check_address(void* addr) {
if (addr == NULL || !is_user_vaddr(addr) || pml4_get_page(thread_current()->pml4, addr) == NULL)
{
exit(-1);
}
}
#else
struct page* check_address(void* addr) {
struct thread* curr = thread_current();
if (!is_user_vaddr(addr) || addr == NULL)
exit(-1);
return spt_find_page(&curr->spt, addr);
}
#endif
실은 vm에서는 vm_try_handle_fault() 함수를 거쳐 page fault를 해결하기 때문에
exit(-1) 은 사실 필요없다.
우리 팀은 addr이 NULL이거나, kernel address일 경우의 처리만 해주었다.
이외의 검증은 vm_try_handle_fault() 함수에서 이루어진다.