🌻구현 예정 리스트🌼
1. Argument Passing
2. User Memory Access
3. System Calls
4. Process Termination Message
5. Deny Write on Executables
📚참고도서📚
운영체제 아주 쉬운 세 가지 이야기 제1편 가상화
컴퓨터 시스템 8장 예외적인 제어 흐름
이번 과제는 프로그램이 시스템 콜(system call)을 통해 OS와 상호작용할 수 있도록 만들어야한다. USERPROG 영역에서 이루어지지만, 핀토스에 존재하는 거의 모든 요소와 상호 작용하게 된다. OS(는 커널)의 최상단(user program on top of the operating system)에서 작동하는 유저 프로그램을 실행하기 시작하면 더 이상 커널에 손쉽게 접근할 수 없다. 해당 과제에서는 하나 이상의 프로그램이 실행될 수 있도록 구현할 것이며, 각각의 프로세스들은 하나의 쓰레드를 갖는다.(멀티쓰레드 프로세스는 제외)
유저프로그램을 구현하기 위해서는 동기화(synchronization)와 가상 메모리(virtual memory)에 대한 이해가 선행되어야 한다.
유저 프로그램은 자신의 유저 가상메모리에만 접근한다. 유저 프로그램이 커널 가상메모리에 접근하게 되면, page fault가 발생하고 프로세스가 종료된다. page fault는 userprog/execption.c에 있는 page_fault()라는 함수에 의해 이루어진다.
커널 프로그램은 커널 가상 메모리에 접근 가능하며, 유저 프로세스가 running 상태라면 이 유저 프로세스의 유저 가상메모리에 접근할 수 있다. 그러나, 커널에서 매핑되지 않은 유저 가상 주소 메모리에 접근한다면 page fault가 발생한다.
유저 가상메모리는 아래와 같은 레이아웃을 갖는다.
유닉스 64비트 x86-64 구현에 있는 몇 가지 중요한 호출 규약들은 다음과 같다.
%rdi
, %rsi
, %rdx
, %rcx
, %r8
, %r9
시퀀스들을 전달하기 위해 정수 레지스터를 사용합니다.CALL
이라는 x86-64 인스트럭션 하나가 이 두 가지를 모두 수행합니다.RET
를 사용해서, 스택에 받았던 리턴 어드레스를 pop하고 그 주소가 가리키는 곳으로 점프함으로써 리턴됩니다.1.인자 전달을 구현하기 위해 Tokenize 작업을 해주고, argv에 넣어주어야한다.
// Tokenize & put them to argv
char *token, *save_ptr;
char *argv[40];
int argc = 0;
if (strchr(file_name, ' '))
{
for (token = strtok_r (file_name, " ", &save_ptr); token != NULL; token = strtok_r (NULL, " ", &save_ptr)){
argv[argc] = token;
argc++;
}
}
else
{
argv[argc] = file_name;
argc++;
}
2.이 단어들을 스택의 맨 처음 부분에 놓고 순서는 상관 없다. 왜냐면 포인터에 의해 참조될 예정이기 때문이다!
size_t sum = 0;
char *argv_address[40];
for(int i = argc-1; i >= 0; i--) {
size_t len = strlen(argv[i]) + 1; // '\0' 포함
sum += len;
argv_address[i] = (if_->rsp - sum);
memcpy((if_->rsp - sum), argv[i], len);
}
3.각 문자열의 주소 + 경계조건을 위한 널포인터를 스택에 오른쪽→왼쪽 순서로 푸시한다. 이들은 argv_address의 원소가 된다. 널포인터 경계는 argv_address[argc] 가 C언어 표준의 요구사항에 맞춰서 널포인터라는 사실을 보장해준다. 그리고 이 순서는 argv_address[0]이 가장 낮은 가상 주소를 가진다는 사실을 보장해준다. 또한 word 크기에 정렬된 접근이 정렬되지 않은 접근보다 빠르므로, 최고의 성능을 위해서는 스택에 첫 푸시가 발생하기 전에 스택포인터를 8의 배수로 반올림하여야 한다.
// Word-Align
while((if_->rsp - sum) % 8 != 0){
sum++;
*(uint8_t *) (if_->rsp - sum) = 0;
}
//Push the address of each string plus a null pointer sentinel
for (int i = argc; i >=0 ; i--){
sum += 8;
if (i == argc)
memset(if_->rsp - sum, 0, sizeof(char **));
else
memcpy(if_->rsp - sum, &argv_address[i], sizeof(char **));
}
%rsi
가 argv_address
주소(argv_address[0]
의 주소)를 가리키게 하고, %rdi
를 argc
로 설정한다. if_->rsp -= sum;
if_->R.rdi = argc;
if_->R.rsi = if_->rsp;
memset(if_->rsp - 8, 0, sizeof(void *));
시스템 콜 기능을 구현하기 이전에, 만약 유저가 유저에게 할당되지 않은 메모리에 접근하려하거나 커널 영역에 직접 접근하려고 한다면 유저 프로세스를 종료시켜야 할 것이다.
먼저, 시스템 콜을 통해 받은 인자의 포인터가 유효한 지 체크해야 한다. 예를 들어, 커널 메모리를 가리키는 포인터라던가, 블록의 일부가 이런 영역들 중 하나의 블록을 가리키는 포인터라면, 유저 프로세스를 종료시켜야한다.
if (!(filename // 유효한 filenamee인가?
&& is_user_vaddr(filename) // 가상주소 공간이 유저의 가상주소 공간인가?
&& pml4_get_page(curr->pml4, filename))) // 유저 주소가 올바르게 매핑되었는가?
{
curr->my_exit_code = -1;
thread_exit();
}
이전 프로젝트를 통해서, 운영체제가 유저 프로그램에게서 제어권을 되찾아가는 방법을 배웠다. 타이머와 I/O 디바이스들로부터의 인터럽트들(외부 인터럽트)들이 바로 그 것이다. 운영체제는 프로그램 코드에서 발생하는 이벤트인 SW exception도 다룬다. 예를 들어, page fault, division by zero가 이에 해당한다. 또한 시스템콜이라는 서비스 또한 SW exception에 해당한다.
시스템 콜 핸들러(syscall_handler)가 제어권을 얻으면 시스템 콜 번호는 rax
에 있고, 시스템 콜을 호출한 콜러의 레지스터는 전달받은 struct intr_frame
(커널스택에 존재)에 접근한다.
case SYS_HALT:
sys_halt_handler();
break;
void
sys_halt_handler(){
power_off();
}
case SYS_EXIT:
sys_exit_handler(f->R.rdi);
break;
void
sys_exit_handler(int arg1){
thread_current()->my_exit_code = arg1;
thread_exit();
}
case SYS_FORK:
f->R.rax = sys_fork_handler(f->R.rdi, f);
break;
int sys_fork_handler(char *thread_name, struct intr_frame *f){
return process_fork(thread_name, f);
}
tid_t
process_fork (const char *name, struct intr_frame *if_) {
/* Clone current thread to new thread.*/
struct semaphore dup_sema;
struct thread *p_thread = thread_current();
void *arr[3] = {p_thread, if_, &dup_sema};
tid_t child_pid;
sema_init(&dup_sema, 0);
child_pid = thread_create(name, PRI_DEFAULT, __do_fork, arr);
if (child_pid != TID_ERROR)
{
sema_down(&dup_sema);
if (p_thread -> exit_check == true){
return -1;
}
return child_pid;
}else{
return child_pid;
}
}
static void
__do_fork (void **aux) {
// struct intr_frame if_;
struct thread *parent = (struct thread *)aux[0];
struct thread *current = thread_current();
struct intr_frame if_ = current->tf;
struct intr_frame *parent_if = (struct intr_frame *)aux[1];
struct semaphore *dup_sema = (struct semaphore *)aux[2];
bool succ = true;
/* 1. Read the cpu context to local stack. */
memcpy (&if_, parent_if, sizeof (struct intr_frame));
/* 2. Duplicate PT */
current->pml4 = pml4_create();
if (current->pml4 == NULL){
goto error;
}
process_activate (current);
#ifdef VM
supplemental_page_table_init (¤t->spt);
if (!supplemental_page_table_copy (¤t->spt, &parent->spt))
goto error;
#else
if (!pml4_for_each (parent->pml4, duplicate_pte, parent)){
goto error;
}
#endif
for (int fd = FDBASE; fd < FDLIMIT; fd++){
if (parent->fd_table[fd]){
current->fd_table[fd] = file_duplicate(parent->fd_table[fd]);
}else{
current->fd_table[fd] = NULL;
}
}
if_.R.rax = 0;
process_init();
/* Finally, switch to the newly created process. */
if (succ){
sema_up(dup_sema);
do_iret(&if_);
}
error:
sema_up(dup_sema);
current->my_exit_code = -1;
parent->abc = true;
thread_exit();
}
fork를 위해 thread 구조체에 멤버를 추가해준다.
struct child_info{
bool finished;
tid_t c_tid;
int c_exit_code;
struct list_elem c_elem;
struct semaphore c_sema;
};
struct thread
{
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
enum thread_status status; /* Thread state. 4가지 : ready, blocked, running, dying*/
char name[16]; /* Name (for debugging purposes). */
int my_exit_code;
int priority; /* Priority. */
int init_priority;
bool exit_check;
int64_t wakeup_tick; /* Shared between thread.c and synch.c. */
struct lock* wait_on_lock;
struct thread *my_parent;
struct file *my_file;
struct child_info *my_info;
struct file* fd_table[FDLIMIT]; /* file descriptor(fd) table */
struct list_elem donation_elem;
struct list_elem elem; /* List element. */
struct list child_list;
struct list donations;
case SYS_EXEC:
sys_exec_handler(f->R.rdi);
break;
int sys_exec_handler(char * cmd_line){
struct thread *curr = thread_current();
if (!(cmd_line
&& is_user_vaddr(cmd_line)
&& pml4_get_page(curr->pml4, cmd_line)))
{
curr->my_exit_code = -1;
thread_exit();
}
char *fn_copy = palloc_get_page (0);
strlcpy (fn_copy, cmd_line, PGSIZE);
return process_exec(fn_copy);
}
int
process_exec (void *f_name) {
char *file_name = f_name;
bool success;
/* We cannot use the intr_frame in the thread structure.
* This is because when current thread rescheduled,
* it stores the execution information to the member. */
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
/* We first kill the current context */
process_cleanup();
lock_acquire(&filesys_lock);
/* And then load the binary */
success = load (file_name, &_if);
/* If load failed, quit. */
lock_release(&filesys_lock);
palloc_free_page (file_name);
if (!success)
return -1;
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
wait
자식 프로세스(pid)를 기다리며 자식의 종료 상태(exit status)를 가져온다. 만약 자식 프로세스가 아직 살아있다면 종료될 때까지 기다린다. 종료가 되면 그 프로세스가 exit 함수로 전달해준 상태를 반환한다.
만약, 자식 프로세스가 exit() 함수를 호출하지 못 하고 커널에 의해 종료된다면, wait(pid)는 -1을 반환해야 한다.
case SYS_WAIT:
f->R.rax = sys_wait_handler(f->R.rdi);
break;
int sys_wait_handler(int pid){
return process_wait(pid);
}
int
process_wait (tid_t child_tid) {
int result;
struct thread *curr = thread_current();
struct list_elem *child_elem = list_begin(&curr->child_list);
struct child_info *c_info;
while (child_elem != list_end(&curr->child_list))
{
c_info = list_entry(child_elem, struct child_info, c_elem);
if(c_info->c_tid == child_tid){
while(!c_info->finished){
sema_down(&c_info->c_sema);
}
result = c_info->c_exit_code;
list_remove(child_elem);
free(c_info);
c_info == NULL;
return result;
}
child_elem = list_next(child_elem);
}
return -1;
}
case SYS_CREATE:
f->R.rax = sys_create_handler(f->R.rdi, f->R.rsi);
break;
int sys_open_handler(char *filename){
struct thread *curr = thread_current();
if (!(filename
&& is_user_vaddr(filename)
&& pml4_get_page(curr->pml4, filename)))
{
curr->my_exit_code = -1;
thread_exit();
}
lock_acquire(&filesys_lock);
struct file *file = filesys_open(filename);
lock_release(&filesys_lock);
if (!file)
return -1;
struct file **f_table = curr->fd_table;
int i = FDBASE;
for (i; i < FDLIMIT; i++)
{
if (f_table[i] == NULL){
f_table[i] = file;
return i;
}
}
lock_acquire(&filesys_lock);
file_close(file);
lock_release(&filesys_lock);
return -1;
}
case SYS_REMOVE:
f->R.rax = sys_remove_handler(f->R.rdi);
break;
bool sys_remove_handler(char *filename){
bool result;
lock_acquire(&filesys_lock);
result = filesys_remove(filename);
lock_release(&filesys_lock);
return result;
}
open
open 함수는 file(첫 번째 인자)이라는 이름을 가진 파일을 연다. 해당 파일이 성공적으로 열렸다면, 파일 식별자로 불리는 비음수 정수(0또는 양수)를 반환하고, 실패했다면 -1를 반환한다. 0번 파일식별자와 1번 파일식별자는 이미 역할이 지정되어 있으며, 0번은 표준 입력(STDIN_FILENO)을 의미하고 1번은 표준 출력(STDOUT_FILENO)을 의미한다.
open 시스템 콜은 아래에서 명시적으로 설명하는 것처럼 시스템 콜 인자로서만 유효한 파일 식별자들을 반환하지 않는다. 각각의 프로세스는 독립적인 파일 식별자들을 갖는다. 파일 식별자는 자식 프로세스들에게 상속(전달)된다. 하나의 프로세스에 의해서든 다른 여러개의 프로세스에 의해서든, 하나의 파일이 두 번 이상 열리면 그때마다 open 시스템콜은 새로운 식별자를 반환한다.
하나의 파일을 위한 서로 다른 파일 식별자들은 개별적인 close 호출에 의해서 독립적으로 닫히고 그 한 파일의 위치를 공유하지 않는다.
case SYS_OPEN:
f->R.rax = sys_open_handler(f->R.rdi);
break;
int sys_open_handler(char *filename){
// return -1;
struct thread *curr = thread_current();
if (!(filename
&& is_user_vaddr(filename)
&& pml4_get_page(curr->pml4, filename)))
{
curr->my_exit_code = -1;
thread_exit();
}
lock_acquire(&filesys_lock);
struct file *file = filesys_open(filename);
lock_release(&filesys_lock);
if (!file)
return -1;
struct file **f_table = curr->fd_table;
int i = FDBASE;
for (i; i < FDLIMIT; i++)
{
if (f_table[i] == NULL){
f_table[i] = file;
return i;
}
}
lock_acquire(&filesys_lock);
file_close(file);
lock_release(&filesys_lock);
return -1;
}
case SYS_FILESIZE:
f->R.rax = sys_filesize_handler(f->R.rdi);
break;
int sys_filesize_handler(int fd){
int result;
struct thread *curr = thread_current();
struct file **f_table = curr->fd_table;
struct file *f = f_table[fd];
lock_acquire(&filesys_lock);
result = file_length(f);
lock_release(&filesys_lock);
return result;
}
case SYS_READ:
f->R.rax = sys_read_handler(f->R.rdi, f->R.rsi, f->R.rdx);
break;
int sys_read_handler(int fd, void* buffer, unsigned size){
struct thread *curr = thread_current();
int result;
if (fd < FDBASE || fd >= FDLIMIT || curr->fd_table[fd] == NULL || buffer == NULL || is_kernel_vaddr(buffer) || !pml4_get_page(curr->pml4, buffer))
{
thread_current()->my_exit_code = -1;
thread_exit();
}
struct file *f = curr->fd_table[fd];
lock_acquire(&filesys_lock);
result = file_read(f, buffer, size);
lock_release(&filesys_lock);
return result;
}
write
buffer로부터 open file fd로 size 바이트를 적어준다. 실제로 적힌 바이트의 수를 반환해주고, 일부 바이트가 적히지 못했다면 size보다 더 작은 바이트 수가 반환될 수 있다.
이로 인해 파일의 끝까지 최대한 많은 바이트를 적어주고 실제 적힌 수를 반환하거나, 더 이상 바이트를 적을 수 없다면 0을 반환한다. fd 1은 콘솔에 적어준다. 콘솔에 작성한 코드가 적어도 몇 백 바이트를 넘지 않는 사이즈라면, 한 번의 호출에 있는 모든 버퍼를 putbuf()에 적어주는 것이다.(더 큰 버퍼는 분해하는 것이 합리적이다!!)
case SYS_WRITE:
f->R.rax = sys_write_handler(f->R.rdi, f->R.rsi, f->R.rdx);
break;
int sys_write_handler(int fd, void *buffer, unsigned size){
struct thread *curr = thread_current();
int result;
if (fd == 1)
{
putbuf(buffer, size);
return size;
}
if (fd < FDBASE || fd >= FDLIMIT || curr->fd_table[fd] == NULL || buffer == NULL || is_kernel_vaddr(buffer) || !pml4_get_page(curr->pml4, buffer))
{
curr->my_exit_code = -1;
thread_exit();
}
struct file *f = curr->fd_table[fd];
lock_acquire(&filesys_lock);
result = file_write(f, buffer, size);
lock_release(&filesys_lock);
return result;
}
case SYS_SEEK:
sys_seek_handler(f->R.rdi,f->R.rsi);
break;
void
sys_seek_handler(int fd, unsigned position){
struct thread *curr = thread_current ();
struct file **f_table = curr->fd_table;
if (fd < FDBASE || fd >= FDLIMIT || curr->fd_table[fd] == NULL) {
curr->my_exit_code = -1;
thread_exit();
}
struct file *f = f_table[fd];
lock_acquire(&filesys_lock);
file_seek(f, position);
lock_release(&filesys_lock);
}
case SYS_CLOSE:
f->R.rax = sys_close_handler(f->R.rdi);
break;
int sys_close_handler(int fd){
struct file **f_table = thread_current()->fd_table;
if (fd < FDBASE || fd >= FDLIMIT){
thread_current()->my_exit_code = -1;
thread_exit();
}
else if (f_table[fd]){
lock_acquire(&filesys_lock);
file_close(f_table[fd]);
lock_release(&filesys_lock);
f_table[fd] = NULL;
}
else{
thread_current()->my_exit_code = -1;
thread_exit();
}
}
참고하면 좋을 블로그.
https://velog.io/@rivolt0421/Pintos-2.-UserPrograms (고마워요!!)