user stack 에서 arugments들을 parsing 한 후 순서에 맞춰 esp에 push해야한다.
PHYS_BASE영역에서 0Xc0000000를 기준으로 아래 영역은 user mode, 그리고 위 영역은 kernel mode이다. user stack이 쌓일때, arguments들이 kernel mode를 침범하지 않고 user mode에서 쌓였는지 확인해준다. 만약, 침범했을 경우에 오류에 대한 표시를 할 수 있도록 구현해준다.
user mode에서 printf(), scanf() (write, read)의 함수를 호출하면, 각 함수에 맞는 syscall number와 argument가 syscall1,2,3,4 를 호출하고, 전달받은 인자들을 esp에 push한다. 결과적으로, kernel mode에서 user mode에서 전달받은 syscall number에 따라 해당하는 syscall를 호출해준다.
kernel API를 구현한다.(exec(), exit(), write(), read(), wait())
부모 process가 child process를 처리하기 이전에 child process가 종료되는것을 막기 위하여 memory lock semaphore을 활용하는 synchronization을 사용한다.
추가적인 함수인 fibonacci와 max_of_four_int를 성공적으로 수행시키기 위하여 4개의 arguments를 처리할 수 있는 syscall4를 define하고, makefile에 additional.c 에 대한 내용을 추가해준다.
process.c에서 arguments를 parsing한 후 hex_dump()를 실행하면, ‘echo x’를 run 했을때 위와 같은 결과를 보인다. PHYS_BASE(0xc00000000)를 기준으로 밑 부분인 user영역에서 argument passing이 성공적으로 수행되어졌다고 확인할 수 있었다.
user program이 invalid pointer(open(NULL), Unmapped virtual memory, kernel address space)를 pass 시키지 않고 다른 process의 running과 kernel을 방해하지 않게 하기 위해서 is_user_vaddr, is_kernel_vaddr함수를 사용해 확인해 주었다. 위와 같은 pointer를 rejected하기 위해서 exit code -1를 호출하도록 만들어주었다.
System call 함수와 System call handler를 구현함으로써, user program에서 pintos1에 해당하는 함수들인 exec, exit, write, read 등의 syscall을 호출하도록 구현해준다.
커널 내 스택에 argument를 쌓는 과정을 step으로 나누어보면,
step1. 문자열 입력이 들어오면 입력된 문자열을 space를 기준으로 split하여 split을 수행한 개수를 argc에 저장해주고 split한 문자열을 argv[0~(argc-1)]에 담아준다.
step2. split한 arguments들을 esp에 push해준다. push 해줄 때, split한 문자열을 담을때와 다르게 역순으로 argv[(argc-1)~0] esp에 push해준다. word alignment를 활용하기 위해 문자열의 길이가 4의 배수가 아닐때 4의 배수가 될 수 있도록 만들어줘서 esp로 문자열에 대한 접근을 편리하게 접근하게 해준다.
step3. word alignment를 push 해준 후 NULL도 push 해준 다음, argv[(argc-1)~0]순서로 esp에 주소값을 push 해주는데, push 동작을 위해 esp를 4만큼 빼면서 수행해준다.
step4. 바로 위 argv[0]의 주소를 point 해주게 만들고, argc와 NULL을 push 한다.
/bin/ls -l foo bar를 예시로 들면 아래와 같다.
위 그림으로 Argument Passing에 대한 step들의 전체적인 flow를 알 수 있다.
( (5+5+4+9)+ 1 )%4 = 0 은 wrod alignment step을 뜻한다.
위 그림과 같이 pintos 상에서 virtual memory는 4GB로 kernel과 user 영역은0xc0000000을 기준으로 으로 나뉘어진다. user program이 Kernel API를 사용하기위해서 flow를 보면 다음과 같다.step1. argument 개수에 따라 syscall1,2,3,4 를 호출한다.step2. user stack에 syscall number와 arguments를 push하고 interrupt를 해준다.step3. interrupt vector table에서 syscall_handler를 호출한다.위 step들을 거친 후 syscall이 수행되는 도중에 esp stack pointer를 활용하여 argument에 접근할 때, kernel 영역(0xc0000000) 이상의 주소값에 접근하면 invalid memory access이다.
Invalid memory access를 어떻게 막을 것인지?
threads folder의 vaddr.h header file에 해당 주소값이 kernel영역에 존재하는지user영역에 존재하는지 판단할 수 있는 함수가 존재한다.
( is_user_vaddr(const void vaddr), is_kernel_vaddr(const void vaddr) ) syscall.c에서 주소값이 kernel영역에 존재하는 경우를 위 함수 중 is_user_vaddr를 사용하여 확인해 주었다. invalid한 pointer가 page_fault()를 유발할때를 대비하기 fault가 user mode에서 발생하지 않았는지 확인하고 fault_addr를 is_kernel_vaddr함수를 사용해 확인해주었다. 위 두 경우 exit mode -1를 호출함으로써 비정상적인 경우임을 보였다.
System call의 필요성은 운영 체제의 보안과 안전성, 그리고 user mode process와 상호작용에 중심을 둔다. user mode와 kernel mode 의 분리(dual mode)는 시스템 리소스와 운영 체제를 보호하며, user process가 kernel에 직접적으로 접근하는 것을 방지해준다. System call은 위와 같은 분리된 환경에서 user process가 필요한 시스템 리소스와 서비스에 안전하고 효율적으로 access할 수 있게 해준다.
이번 프로젝트에서 개발할 시스템 콜에 대한 간략한 설명
- halt
shutdown_power_off() 함수를 호출하여 현재 process 상태를 blocked한 상태로 만들어준다.
- exit
user program이 종료될 때 호출되며 현재 child thread의 이름과 status를 출력하고 부모 thread가 child thread를 정상적으로 종료시킨다.
- exec
process_execute함수를 cmd_line과 함께 호출하여 현재 child thread가 다시 child thread를 만든다.
- wait
process_wait함수를 한다. process_wait에서 parent process가 child process를 정상적으로 종료시키기 위해서 wait 한다.
- read
주어진 파일 디스크립터 에서 데이터를 읽어와 버퍼에 저장한다. standard input에 대해서만 동작하며, 파일 디스크립터 0일때 데이터를 읽어온다.
- write
버퍼의 데이터를 주어진 파일 디스크립터에 write한다. standard ouput에 대해서만 동작하며, 파일 디스크립터가 1일때 데이터를 출력한다.
- fibonacci
n에 대해 n번째 피보나치 수를 계산한다. int 범위를 넘어가면 -1을 return한다.
- max_of_four_int
입력 받은 4개의 정수 중에서 가장 큰 값을 return한다.
‘additional 10’을 입력하였을 때를 가정하여 설명하자면 다음 step과 같다.
step1. argc = 2, argv[0] = ‘additional’, argv[1] = 10의 data를 가지고 fibonacci(10)을 call 한다.step2. SYS_FIBONACCI와 10(argv[1])으로 syscall1을 call하여 push한다.step3. Pintos의 Trap instruction인 0x30으로 System call handler를 call 한다.step4. 위 과정에서 user level에서 kernel level로 바뀌며, 다음으로 syscall number(fibonacci)에 맞는 함수를 호출한다.step5. 모든 과정이 끝나면 다시 user level로 돌아와 syscall1을 call 함으로써 얻은 결과값을 return해줌으로써 user level에서 원하는 결과를 얻게된다.
9.20 ~ 9.22 : Argument Passing
9.24 ~ 9. 25 : User Memory Access9.26 : System Call Handler
9.28 ~ 9.30 System Call Implementation
10.1 : Additional Implementation 순으로 구현하였다.
- Argument Passingprocess.c 에서 arguments를 parsing하고 esp를 활용하여 stack을 쌓는 함수인make_stack 함수를 자체적으로 만들어 활용하였다. process.c 내의 load 함수에서 setup_stack(esp) 확인 후 make_stack 함수를 실행시켜 주었다. 여기서 사용한 esp 구조체는 interrupt.h내의 intr_frame 구조체이다.
- User memory accessInvalid memory access를 방지하기 위하여 point하는 각 주소값이 kernel 영역인지, user 영역인지 확인해주었다. vaddr.h에 존재하는 함수인 is_user_vaddr, is_kernel_vaddr함수를 사용하였다. userprog/syscall.c 내의 system handler에서 esp stack pointer가 point하는 값을 자체적으로 제작한 is_bad_addr함수를 통해 user 영역인지 확인해주었으며, 만약 그렇지 않다면 exit(-1) 를 return 하였다.또한, exception.c 내에서 fault_addr가 kernel영역인지 확인하고 fault가 user 모드에서 나타났는지 확인해주었다. 그렇지 않으면 exit(-1)를 return하였다.
- System Callsuserprog/syscall.c에 halt, exit, exec, wait, read, write, fibonacci, max_of_four_int 함수들을 구현하였다. 특히, max_of_four_int에서 4개의 arguments를 받기 위해 lib/user/syscall.c에 syscall4를 구현하고 syscall.h에 fibonacci와 max_of_four_int 프로토타입을 추가해주었다. 두 함수를 위해 examples내에 additional.c 파일을 만들어 주고 make file도 수정해주었다.exec함수가 process_execute 함수를 실행시킬때, input 문자열을 parsing하여 실행가능한 파일인지 확인해주었다.child process와 memory를 remove하지 못하고 죽어버리는 상황을 대비하기 위하여 synchronization 기법을 활용하였다. thread.h에 exit_status(child thread exit status), child_list(list struct), child_elem(list elem struct), 그리고 semaphore struct child_hi, child_by를 추가해줬다.thread.c 에서 init_thread 에 child_hi, child_by semaphore를 초기화해주고 child_list를 초기화 하여 child_elem에 push_back 해주었다.
Argument Passing

User Memory Access

System Calls


file_name을 parsing하여 stack에 쌓는다. parsing할 때 처음에는 생각하지 못한 double space와 같은 경우를 처리하기 위하여 flag를 선언하여 space를 만나면 flag가 다시 0으로 바뀌도록 만들어주었다.
argv에 쌓을때는 strtok_r함수를 사용하여 하나씩 argv[0], argv[1], ~ argv[argc -1]에 쌓아주었으며 역순으로(argc[argc-1] ~ argc[0]) esp에 push해주었다.위 과정을 마친 후 4의 배수인지 계산 후 데이터 접근속도 향상을 위한 word-align을 push해준 후 NULL을 push해주었다.
마지막으로, 앞에서 저장했던 주소값들을 역순으로 저장해주고 argc value와 return address를 저장해주었다. pintos에 적용하기 위해 stack에 쌓는 순서가 어색했지만 hex_dump함수를 통해 내가 저장하려는 문자열이 어떻게 저장되는지 하나씩 확인해보면서 극복할 수 있었다.

is_bad_address함수를 따로 만들어주어 각 arguments들의 주소가 user 영역에 존재하는지 체크하였다. 만약 그렇지 않다면 kernel영역에 존재하는 것이기 때문에 exit mode -1를 호출했다. 처음에는, 첫 argument만 검사하면 될거라고 생각했지만, 모든 arguments들의 주소를 확인해야한다고 생각했고 for문을 통해 모두를 검사해줬다.

exception.c에서 fault_addr가 kernel 영역에서 일어났는지 확인이 필요하다. 또한, user 영역에서 일어났는지(access by user)도 확인이 필요하다.

kernel 영역의 Syscall_handler에서 전달받은 syscall number에 맞는 위 함수를 call 해준다. f->esp로 user영역에 쌓인 arguments들에 접근하여 함수를 실행시켜준다.read함수는 fd(f->esp+4), buffer(f->esp+8), size(f->esp+12)를 받아오서 data를 읽어주고 write함수는 위와 같이 받아와서 putbuf(buffer, size)로 출력을 해준다.

wait함수를 호출할때 사용한 synchronization기법에 대한 코드이다. child process가 wait하는 1개의 semaphore만 필요하다고 생각했지만, parent process가 list_remove를 할 때 까지 child가 exit하면 안돼므로 다음과 같은 코드 구성으로 바꿨다.바꿔준 이후에 parent가 child process의 메모리까지 처리한 후 종료되는 것을 확인할 수 있었다.

위와 같은 additional.c를 examples 파일에 만들어준 후 /userprog/syscall.c에 fibonacci와 max_of_four_int 함수를 만들어주었다. (makefile에 additional_SRC = additional.c를 추가해주었다.) 47번째 fibonacci수가 int 범위를 넘어서기 때문에 46번째 까지만 구하고, 넘어가거나 0 미만이면 -1를 출력하게 했다.


4개의 매개변수를 처리하기위해 syscall3을 참고하여 syscall4 wrapper를 만들어 주었으며 /lib/user/syscall.h에 fibonacci를 0번 max_of_four_int를 1번으로 추가해주었다.또한, /lib/user/syscall.c에도 fibonacci와 max_of_four_int를 정의해 줌으로써, System handler에서의 결과값을 return하여 user영역에서 받을 수 있게 만들어주었다.

fibonacci 및 max_of_four_int 시스템 콜 수행 결과
부.fibonacci(10)의 결과인 55와 10, 20, 62, 40중 가장 큰 62가 출력됨을 볼 수 있다.
성공적으로 출력했으므로 exit(0)을 통해 exit한 모습도 확인할 수 있다.

pintos project1 에서 제시한 21가지 test에 대한 성공도 확인할 수 있었다.