Jungle

[TIL] pintos : stack growth 지원하기

손가든 2023. 12. 23. 19:04

유저프로그램이나 커널에서 함수를 호출할 때는

 

런타임 스택에 인자들을 집어넣고 함수가 끝났을 때 복귀할 주소인 return address를 넣은 뒤

 

call function 어셈블리어를 통해 함수를 호출한다.

 

이때 런타임 스택도 페이지로 할당받아 사용하는 메모리 공간이다.

 

 

근데 만약 런타임 스택이 1개의 페이지 이상의 값을 채워넣어야 하는 상황이 발생하면 그때는 페이지를 추가로 요청해야 한다.

 

이 상황을 stack growth라고 한다.


 

그럼 왜 동적으로 stack을 할당해줄까?

 

미리 넓찍하게 스택을 할당해두면 다른 메모리 페이지를 할당할 용량이 줄어들어서 효율적이지 못하기 때문이다.

 

 

나는 stack growth를 지원해주기 위해서

 

사용자 혹은 커널이 stack을 추가로 할당하기를 원하는 정당한 page_fault인지,

 

아니면 불법적인 참조에 의한 page_fault인지를 검사해야 할 필요가 있기에 그 상황을 나누어 생각해야 했다.

 

 

그것을 판단하는데에는 rsp값을 사용할 수 있다.

 

사용자든 커널이든 런타임 스택에 인자를 채우기 위해서 rsp를 내리고 런타임 스택에 값을 차곡차곡 넣게 되어있다.

 

만약 page_fault를 발생시켰다면 rsp를 내린 후 발생시켰을 것이기에

 

fault_addr >= rsp-8 범위 안에서 발생한 page_Fault인지 확인하면 된다.

 

 

이때 유의해야 할 점이 있다.

 

사용자가 page_fault로 전환될 때는 사용자 프로세스의 인터럽트 프레임을 전달하여 rsp 값을 참조할 수 있지만,

 

만약 커널에서 page_fault가 발생한다면 같은 커널 단인 page_fault로 커널의 인터럽트 프레임을 전달하지 않기 때문에

if(인터럽트 프레임)을 통해 rsp 값을 전달받을 수 없다.

 

따라서 커널에서 런타임 스택에 참조하는 page_fault가 발생가능한 곳인 argument_parsing 함수와

 

system call 진입 시 thread 멤버에 현재의 rsp를 기억할 수 있게 저장하였다.

 

그리고 현재 스택의 주소 위치에 스택을 이어 붙이기 위해 현재 스택의 bottom address를 알 필요가 있다.

 

따라서 현재 stack page의 bottom 주소 값을 기억하도록 thread에 추가했다.

 

thread.h 의 struct thread에 현재 스택 bottom va 와 현재 rsp 기억 변수 멤버 선언

 

 

그리고 위에서 말했던 것 처럼 커널에서 발생하는 page_fault 시 인터럽트 프레임(if)으로 rsp 를 전달하지 못하므로 

 

system call과 argument_parsing시 curr_rsp를 저장하도록 설정했다.

 

 

 

이제 본격적으로 vm_try_handle_Fault()함수에서의 stack growth 처리 단을 보자

 

 

유저일 경우 f->rsp를 참조할 수 있지만 / 커널일 경우 curr_rsp를 참조해야 한다.

 

따라서 이렇게 두가지 방식으로 나눈 뒤 그 안에서 if문으로 USER_STACK의 제한크기를 확인하고

 

vm_stack_growth()함수로 stack에 page를 하나 추가해준다.

 

 

vm_stack_growth는 setup_stack에서 해준것과 동일하게 해주되,

 

이전 스택과 붙여놓기 위해서 현재 stack_bottom에서 pgsize만큼 내려 페이지를 할당한다.

 

 

그렇게 되면 이전 스택 페이지는 stack_bottom의 VA를 가지고

 

새로 할당한 페이지는 stack_bottom - PGSIZE의 VA를 가지게 되어 붙어있는 stack이 된다.

 

 

++ pt-write-code2 test 해결하기

 

 

위의 stack growth를 다 구현하고 난 뒤에 stack 과 관련한 테스트는 모두 통과하였는데 이 테스트가 통과되지 않았다.

 

매우 간단한 테스트 소스 파일인데

 

테스트 코드를 잘 보면 read(int fd, void * buffer , size_t size) 함수인 read 시스템콜을 요청하는데

 

읽은 내용을 옮겨담는 buffer에 test_main인 테스트 이름의 주소값이 들어가 있다.

 

이 테스트 이름은 실행되는 ELF 파일이므로 코드 세그먼트 단의 주소이다.

 

하지만 코드 세그먼트 주소는 read_only이므로 write가 불가능하다(Read-only-execution을 구현했으므로).

 

 

하지만 내 코드에서는 read_only 주소에 write하는 것을 수행하고 종료한다.

 

왜 그런걸까?

 

코치님의 답변은 이러했다.

 

그 이유는 시스템콜을 호출받은 커널은 pintos에서 슈퍼바이저 모드로 writable과 상관없이 읽고 쓰기가 가능해진다.

 

따라서 유저모드의 사용자는 시스템콜을 이용해서 read-only의 file에 쓰는것이 가능해지는 것이다.

 

우리는 이 권력을 제한할 줄 알아야 하고, 또 이 테스트는 그것을 구현했는지 검사하고 있다.

 

코치님께서 kernel mode에서는 쓰기전에 주소 위치를 미리 검사할 수 있다는 것에 대한 힌트를 보고

 

코드 세그먼트의 주소 위치인지를 확인하라 = writable인지 확인하라. 고 해석했다.

 

 

따라서 다음과 같이 read와 write시 buffer가 writable인지 체크하도록 확인했다.

 

 

일단 첫번째 빨간색으로 체크한 단에서 런타임 스택 상의 요청인지 확인했다.

 

이는 코드 세그먼트나 데이터 세그먼트처럼 writable의 영역이 아니므로 이 경우 그냥 다시 되돌아 가도록 했다.

(이 코드를 수행하지 않으면 많은 경우에서 call stack이 길어져서 간헐적으로 터지게 된다..)

 

그 경우가 아니라면 코드 세그먼트와 데이터 세그먼트에 대한 접근일 수 있으므로

 

그 경우를 예외처리하여 exit하였다.

 

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
pass tests/vm/page-merge-par
pass 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
pass tests/vm/mmap-ro
FAIL tests/vm/mmap-exit
FAIL tests/vm/mmap-shuffle
FAIL 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
FAIL tests/vm/mmap-bad-fd2
FAIL tests/vm/mmap-bad-fd3
FAIL 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
FAIL 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
FAIL 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
31 of 141 tests failed.

 


 

* kernel 단에 syscall로 write를 요청했을 시 writable == false면 종료

 

 

#define is_thread(t) ((t) != NULL && (t)->magic == THREAD_MAGIC)

 

이 assert문이 test fail의 원인이라면

 

thread.h 에 선언된 thread 구조체가 너무 많은 크기의 데이터를 가지고 있는건 아닌지 의심해보아야 한다.

 

 

커널 page의 모양은 이러한데, 커널의 스택 공간을 위해 thread 구조체의 크기는 1kb 미만으로 유지하는 것이 좋다.

 

만약 kernel stack이 진행중에 thread 구조체 선언 단(바닥부분)과 만나게 되면 magic의 값을 침범하여 오버플로우가 발생하고

 

그 때문에 위와 같은 is_thread(t) assert문에서 터지게 된다.

 

현재 내 코드에서는 프로세스의 쓰레드가 기억하고 있는 curr_rsp 값을

 

uintptr_t 형태나 uint64_t * 와 같이 저장하면 매우 큰 값으로 저장했었다.

 

따라서 이 값을 void * 형태로 변환하였더니 오버플로우가 발생하지 않아 테스트를 통과할 수 있었다. (사실 아직 간헐적으로 터짐..)