Pintos Project3 : stack growth

HiroPark·2023년 5월 17일
1

문제해결

목록 보기
10/11
post-thumbnail

Vitrual Memory의 3번째 과제는 이전까지 USER_STACK에서 시작한 단일 페이지로만 관리되던 스택을, 필요에 따라 추가 페이지를 할당해주는 구조로 변경해주는 과제이다.

"필요에 따라" 라고 함은, 접근이 스택에 대한 접근으로 보일때를 의미한다.

여기서 주로 구현해야 할 함수는 다음 두가지이다.
그냥 코드부터 보자..

  • vm_try_handle_fault
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 */
	/* spt_find_page로 spt에서 페이지 찾아옴 */
	if(is_kernel_vaddr(addr))
        return false;
	
    /* ----------------------------------- project3-3_Stack Growth ----------------------------------- */
    // 스택의 증가로 page fault를 해결할 수 있는지 확인
	/*  */
    // if (f->rsp - 8 <= addr && addr <= USER_STACK && USER_STACK - 0x100000 <= addr) { // 0x100000 = 1MB(스택 사이즈 제한)
	// 왜 -8? : so it may cause a page fault 8 bytes below the stack pointer..
	if (USER_STACK - 0x100000 <= thread_current()->rsp_stack - 8 && thread_current()->rsp_stack - 8 <= addr && addr <= thread_current()->stack_bottom) { // 0x100000 = 1MB(스택 사이즈 제한)
        // 스택 증가 함수 호출
        // 주소를 현재 스택의 마지막 주소에서 새롭게 할당받을 크기인 PGSIZE로 넘겨줌
        vm_stack_growth(thread_current()->stack_bottom - PGSIZE);
        return true;
    }
.
.
  • vm_stack_growth
static void
vm_stack_growth (void *addr UNUSED) {
	ASSERT(!is_kernel_vaddr(addr))

	addr = pg_round_down(addr);

	if (vm_alloc_page_with_initializer(VM_STACK, addr, true, NULL, NULL)) {
		vm_claim_page(addr);
		thread_current()->stack_bottom -= PGSIZE;
	}
}

내가 생각했을때, 이번 과제에서 관건은 vm_try_handle_fault 에서 stack_growth를 호출해줘야 하는 fault인지를 판단하는데 있는 듯 하다.

이걸 어떻게 판별하냐고? 일단 이 그림을 보자

부족하지만 이게 무슨 그림이냐면... 대충 stack_growth가 호출되는 범위를 나타낸 그림이다.

  • USER_STACK은 스택의 시작점이고, process.c의 setup_stack에서 확인해볼 수 있다.
static bool setup_stack (struct intr_frame *if_) { 
	bool success = false;
	void *stack_bottom = (void *) (((uint8_t *) USER_STACK) - PGSIZE);

	/* TODO: Map the stack on stack_bottom and claim the page immediately.
	 * TODO: If success, set the rsp accordingly.
	 * TODO: You should mark the page is stack. */
	/* TODO: Your code goes here */
	// VM_MARKER_0 은 스택을 의미
	if (vm_alloc_page_with_initializer(VM_STACK, stack_bottom, true, NULL, NULL))
    {   
        // va에 페이지를 할당하고, 해당 페이지에 프레임 할당하고 mmu 설정
        if (vm_claim_page(stack_bottom))
        {   
            // rsp 설정
            if_->rsp = USER_STACK;
			/* 궁금: USER_STACK은 유저 스택의 시작점인데, 인터럽트 프레임의 rsp는 유저 스택의 끝을 가리켜야 하는거 아님??
			그리고 지금 코드면, stack_bottom은 계속 밑으로 내려가는게 아니라, 그냥 유저 스택의 시작점에서 한단계 밑의 페이지자나..
			ㄴ 이게 맞다. 왜? setup_stack은 맨 처음에 프로세스 만들때 load에서 딱 한번만 호출되기 때문에, 한 페이지 크기만큼만 밑으로 내려가는게 맞음
			ㄴ 마찬가지로 rsp도 USER_STACK을 가리켜서 initial rsp를 USER_STACK과 동일하게 두는게 맞음
			*/

			/* ----------------------------------- project3-2_Stack Growth ----------------------------------- */ 
            // 스택의 끝부분 저장
            thread_current()->stack_bottom = stack_bottom;
			/* ----------------------------------- project3-2_Stack Growth ----------------------------------- */ 

            // success를 true로 값 변경
            success = true;
        }
    }

    return success;
}
  1. 처음에 intrframe(편의상 이제 if라 부르겠다)의 rsp는 USER_STACK, 즉 유저 스택의 시작점을 가리킨다. 이후 rsp는 유저 스택 안을 마음대로 돌아다닌다.
  2. 특정한 함수가 호출됐을때, rsp를 이동시키는 arithmetic instruction(sub라고 하자)을 통해서 if_의 rsp는 이동하게 된다. 그림에서는 new_rsp이다. 이때 new_rsp가 기존 유저스택의 범위를 벗어낫다고 치자.
  • 현재는 범위를 벗어났지만, 나중에 스택을 늘려줄건데, 이 공간은 현재 함수의 지역변수를 저장하기 위해 쓰인다.
  1. 여기까지는 단순히 레지스터의 값을 다루는 과정이기에 문제가 없다.
  2. 문제는 이 new_rsp에다가 push를 하려고 할 때이다.
  3. push를 하려고 -8만큼 내려가서 값을 적으려고 보니, 페이지가 UNINIT이라 페이지 폴트가 난다.
  4. 이 페이지 폴트가 나는 경위는 메모리 접근을 해서인데, 이는 대충 다음과 같은 경우의 수가 있다.
    push/pop을 하거나, 간접 메모리 참조(자세히는 모르겠는데, 레지스터를 통해서 메모리 주소를 참조하는 등 여러번의 역참조를 거치는 듯 하다)를 하거나, call/ret을 하는 경우들이 있다.
  5. 암튼 이렇게 폴트가 나면, 폴트가 난 가상주소의 위치를 확인해서, 이것이 스택을 늘려줘야 하는 폴트인지를 확인한 이후, 맞으면 스택을 늘려주면 된다.
  6. 이 범위는 (내가 알기로는)
  • 유저 스택의 끝을 가리키는 stack_bottom 보다는 작고,
  • pintos에서 유저 스택의 최대 크기인 1MB보다는 크고(USER_STACK - 0x100000),
  • 폴트가 난 곳(rsp - 8) 보다는 커야 한다.

그래서 이 범위가 맞는지 확인하고, 스택의 크기를 늘려주기만 하면 된다.

USER_STACK - 1MB <= rsp-8 <= addr <= stack_bottom

문제는 위의 if문에서 rsp를 활용하자고 하니, gitbook의 다음 문장이 마음에 걸린다

If you depend on page faults to detect invalid memory access, you will need to handle another case, where a page fault occurs in the kernel. Since the processor only saves the stack pointer when an exception causes a switch from user to kernel mode, reading rsp out of the struct intr_frame passed to page_fault() would yield an undefined value, not the user stack pointer.

이 문장을 나는 이렇게 해석했다.
"exception이 발생할시, 인터럽트 프레임의 rsp값이 유저 스택을 가리키다가, 커널 스택을 가리키게 바뀜"(틀린 해석이다, 밑에서 설명함)

그래서, exception을 통해 호출된 vmtry_handle_fault의 인자로 들어온 if 안의 rsp는 이미 커널 스택을 가리키고 있을 가능성이 있다. 이렇게 되면 stack_growth를 호출해줘야 하는지에 대한 올바른 범위 판단이 안된다.

그래서 쓰레드 구조체 안에 if_의 rsp가 유저 스택에서 커널 스택을 가리키게 변화하기 전에 미리 유저스택에서의 rsp값을 저장하는 rsp_stack을 만들어 줬다.

struct thread {
.
.
#ifdef VM
	/* Table for whole virtual memory owned by thread. -> vm_entry set역할을 하는 supplemental page table */
	struct supplemental_page_table spt;
   void* stack_bottom; 
   void* rsp_stack; // exception(page fault), syscall 호출시 유저 -> 커널로 이관되는데, 이때 intr_frame의 rsp가 커널 스택을 가리키게 되니까, 
   // 유저 스택의 값을 저장하고 있기 위해서 해당 값을 둠
#endif
.
.
}

그리고 얘의 값을 if_의 rsp가 바뀌기 전인syscall_handler랑 exception.c에서 업데이트를 해주었다.

void syscall_handler (struct intr_frame *f UNUSED) {

    thread_current()->rsp_stack = f->rsp;
.
.
}

static void
page_fault (struct intr_frame *f) {
.
.
#ifdef VM
	/* For project 3 and later. */
	// 유저이면, rsp값을 thread구조체의 rsp_stack에 저장해야함(인터럽트 프레임의 rsp값을 커널 스택을 가리키는 값으로 바뀔테니까)
	if (user) {
		thread_current()->rsp_stack = f->rsp;
	}

	if (vm_try_handle_fault (f, fault_addr, user, write, not_present)) {
		return;
	}
#endif
.
.
}

요러면 if_의 rsp값이 커널 스택을 가리키게 바뀌기 전에, 유저 스택의 값을 저장할 수 있겠지?? 라고 생각을 했는데, 뭔가 이상하다.

일단 정확히 어느 순간에 rsp가 유저스택->커널 스택 으로 넘어가는지 모르겠다.

실제로 해당 if문 안에 rsp_stack을 넣건, f->rsp를 넣건 통과하는 테스트의 개수도 똑같다. 뭔가 쓸데없는 일을 하고 있다는생각이 들었다.

if (f->rsp - 8 <= addr && addr <= USER_STACK && USER_STACK - 0x100000 <= addr)

ㄴ 이렇게 해도 아무 차이가 없다는 말...

그리고, 상식적으로 생각을 해보면, 이미 exception이 불리고 난 이후면 rsp가 커널 스택으로 넘어간 이후일텐데, exception 안에서 이미 값이 바뀐 이후에 rsp_stack의 값을 저장하는게 별 의미도 없는 듯하다.

그래서 조교님께 "어디서 rsp값이 유저스택 -> 커널 스택으로 바뀌나요??" 하고 다소 뻔뻔한 질문을 했다.

이에 대한 조교님의 답변은 다음과 같다

그러니까,

  • exception 때문에 user->kernel로 옮겨질때 : intr_frame의 rsp에 유저레벨의 %rsp가 저장됨
  • exception 때문에 kernel->kernel로 옮겨질때 :
    intr_frame의 rsp 항목에 %rsp가 백업되지 않음.

이고, 따라서 "intr_frame의 rsp값이 유저 스택을 가리키다가 커널 스택을 가리킨다" 는 나의 잘못된 뇌피셜이었다.

thread 구조체의 rsp_stack에 값을 백업하는 메커니즘과, 백업시기는 옳았지만 나의 이해가 잘못된 부분이 있었다.

우선, exception.c에서 rsp_stack에다가 값을 백업하는 부분의 경우, 이미 exception이 일어난 직후에 intr_frame의 rsp에 유저의 %rsp가 저장돼있기 때문에, 저 부분에서 값을 저장한다고 해서 추후에 intr_frame의 rsp 값이 커널 레벨로 바뀔것을 대비하는 효과를 가질 일은 없다.

static void
page_fault (struct intr_frame *f) {
.
.
#ifdef VM
	/* For project 3 and later. */
	// 유저이면, rsp값을 thread구조체의 rsp_stack에 저장해야함(인터럽트 프레임의 rsp값을 커널 스택을 가리키는 값으로 바뀔테니까) 
    // ㄴ 위는 잘못된 주석
	if (user) {
		thread_current()->rsp_stack = f->rsp;
	}

읽어와서 저장하는 부분은 맞다만, 나의 이해가 잘못된 것이다.

또한, syscall.c의 syscall_handler()에서 rsp_stack에 intr_frame의 값을 백업하는 부분의 경우, syscall을 위해 커널 모드로 넘어갔다고 해서, intr_frame의 rsp가 백업되지는 않는다.
따라서, 해당부분에서 미리 유저레벨에 해당하는 rsp를 쓰레드 구조체의 rsp_stack에 저장해두어야 추후 sysall에서 exception을 처리하다가 fault가 났을 경우 올바른 유저 스택 값을 얻어올 수 있다.

만약 여기서 저장하지 않는다면, syscall 처리 중에 호출된page_fault에서 해당 부분

	if (user) {
		thread_current()->rsp_stack = f->rsp;
	}

에서 if(user)의 조건문을 충족하지 못하므로, 유저레벨의 rsp값을 제대로 백업할 수 없으니, vm_try_handle_fault에서 rsp_stack을 가지고 올바른 stack_growth인지를 판별할 수 없게 된다.

profile
https://de-vlog.tistory.com/ 이사중입니다

0개의 댓글