-
Project2 : Argument PassingOS/Pintos 2022. 1. 10. 23:35
process_exec()함수 안에서 유저 프로그램을 위한 argument들을 설정하라
현재 Pintos는 아주 기본적인 기능만 갖추고 있다.
그래서 이번 Pintos 프로젝트의 목표는 PintOS가 user program을 적절히 실행하도록 만드는 것이다. 현재 PintOS는 명령어를 그 전체 문자열로 인지한다.
입력된 명령을 분절한 후에 명령어를 실행하는 데 이 때 User memory와 kernel Memory가 구분되어있음을 주의해야 한다. 메모리를 kernel memory와 User Memory로 구분하지 않고 사용하면 Memory를 관리하기 힘들다.. 예를 들어 각 프로세스가 서로 영역을 침범해서 오류를 발생시키거나 OS를 동작시키는 데 중요한 Kernel Code를 훼손할 수도 있다. User memory와 kernel memory를 구분한 상태에서 kernel의 함수를 User Program이 쓰려면 System Call이 필요하다.
Argument Passing
현재 PintOS는 명령어를 입력받았을 때 명령어 부분과 매개변수를 구분 못한다. 아까도 얘기했지만 'echo x'를 걍 echo x 전체로 받아들인다. 명령어를 적절히 분절해서 '명령어'와 '매개변수'로 나눠야 한다.
첫번째 단어는 프로그램의 이름이고 두 번째 단어부터는 N-1번째 매개변수이다. 단, 프로그램과 매개변수, 그리고 매개변수 끼리의 공백이 한 칸이 아니라 여러 칸일 수 있다. 이 여러 공백은 프로그램 상에서 하나의 공백과 같으므로 잉여 공백도 하나로 묶음처리해야한다.
여러 방법을 쓸 수 있겠지만 우리는 변수를 저장할 수 있는 stack을 쌓아서 접근하는 방법을 구현한다. 그러므로 변수를 stack에 쌓아서 다른 함수들이 argument를 참조할 수 있도록 한다. Userprog/process.c에 스택 포인터와 관련된 함수를 새롭게 만들어서 구현하면 된다. 이 작업은 Manual에 나와있는 것처럼
x86-64 Calling Convention
- User-level application은 %rdi, %rsi, %rdx, %rcx, %r8, %r9를 전달하기 위하여 정수 레지스터들을 사용한다.
- caller는 다음 인스트럭션(return address)를 stack에 넣어주고 callee의 첫 인스트럭션으로 jump한다.
- x86-64 인스트럭션 중에서 CALL이 이걸 해준다.
- callee가 실행된다.
- callee가 반환값을 갖는 경우에는 %rax에 넣어준다.
- callee는 return address를 스택에서 pop하면서 그 위치로 jump한다.
- x86-64 인스트럭션 중에서 RET이 이걸 해준다.
Program Startup Details
- 사용자 프로그램의 진입점 → /lib/user/entry.c의 _start()
- 이 함수는 main()을 실행하고 main이 반환하면 exit()을 불러준다.
- 커널은 유저 프로그램의 실행을 허용하기 전에 초기 함수에 대한 인수를 레지스터에 넣어줘야 한다.
- 일반적인 calling convention과 동일한 방식으로 인자가 전달된다.
- /bin/ls -l foo bar 가 전달될때의 예시
-
- 커맨드를 단어로 분리한다. /bin/ls, -l, foo, bar
- 단어들을 스택의 최상단에 넣어준다. 포인터로 참조되기 때문에 순서는 관계없다.
- 각 문자열의 주소와 null pointer sentinel을 스택에서 오른쪽에서 왼쪽 순서로 넣는다.
- 이것들이 argv의 요소들이다.
- null pointer를 넣음으로서 argv[argc]가 null pointer가 된다.(C표준 요구사항)
- word로 정렬되어 있는 경우가 더 빠르기 때문에 스택 포인터를 처음 넣기 전에 stack pointer를 8의 배수가 되도록 조정해 준다.
- %rsi가 argv를, %rdi가 argc를 가리키도록 한다.
- 마지막으로 가짜 return address를 넣어준다. 엔트리 함수는 반환되지 않지만 다른 스택 프레임과 동일한 구조를 만들어 주기 위해 넣어준다.
-
아래는 유저 프로그램이 실행되기 직전의 stack의 상태와 관련 레지스터들을 보여준다.

- hex_dump()함수는 stdio.h에 정의되어 있으며 argument passing code를 디버깅 하는데 유용할지도 모름.
- USER_STACK의 시작점이 0x47480000이므로 데이터 크기만큼 아래로 쌓인다.
Implement the argument passing.
현재 구현된 process_exec() 는 새로운 프로세스에게 인자를 전달하지 않는다. 이 기능을 구현하자.
- process_exec()가 단순히 파일 이름을 받는 것이 아니라 공백을 기준으로 나누어서 앞쪽은 프로그램 이름, 뒤쪽은 인자가 되도록 파싱해준다.
현재 Pintos의 process_exec( ) 함수는 새로운 프로세스에 인자를 전달하지 못하는 구조이다.
따라서 입력받은 명령어를 공백을 기준으로 나누어야 한다.
요구 사항에 따르면 명령어의 첫 번째 단어가프로그램명, 두 번째 단어부터가 해당 프로그램에 전달할 인자가 될 수 있도록 수정해야 한다.
예를 들어, process_exec("grep foo bar")에서 grep이 실행할 프로그램명이고 foo와 bar가 전달할 인자인 것이다.
구현!
STEP1. process_exec()함수 안에서 유저 프로그램을 위한 argument들을 설정하라니까, process_exec()함수 구조를 봐보자!
자세히 보면 인자로 받는 명령어 f_name을 문자열 file_name으로 받고 있으며, 이는 특별한 변환 없이 인터럽트 프레임 구조체인 _if와 함께 load( ) 함수의 인자로 사용된다.
/* Switch the current execution context to the f_name. * Returns -1 on fail. */ int process_exec (void *f_name) { char *file_name = f_name; //💡인자로받는 명령어 f_name을 문자열 file_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 (); /* And then load the binary */ success = load (file_name, &_if); //file_name은 특별한 변환 없이 인터럽트 프레임 구조체인 _if와 함께 load( ) 함수의 인자로 사용된다. /* If load failed, quit. */ palloc_free_page (file_name); if (!success) return -1; /* Start switched process. */ do_iret (&_if); NOT_REACHED (); }STEP2. load()함수보기 (process_exec()함수에서 불려지니까!)
이제 load( ) 함수를 살펴보자. 언급하였듯이 file_name은 사용자가 입력한 명령어이다.
해당 명령어를 parsing하여 프로그램을 정상적으로 로드 및 스택에 저장해보도록 하겠다.
static bool load (const char *file_name, struct intr_frame *if_) { struct thread *t = thread_current (); struct ELF ehdr; struct file *file = NULL; off_t file_ofs; bool success = false; int i; //중략 /* Set up stack. */ if (!setup_stack (if_)) goto done; /* Start address. */ if_->rip = ehdr.e_entry; /* TODO: Your code goes here. * TODO: Implement argument passing (see project2/argument_passing.html). */ success = true; done: /* We arrive here whether the load is successful or not. */ file_close (file); return success; }STEP3. load()함수수정
우선, file_name을 parsing하기 위해서 strtok_r() 함수를 이용한다.
함수의 첫 번째 인자 : 분할하고자 하는 문자열, 두 번째 인자 : 분할 기준이 되는 구분자, 세 번째 인자 : 포인터
strtok_r( ) 함수는 미리 구현되어 있으며, 자세한 내용은 구현 코드를 직접 살펴보는 것이 이해에 도움이 된다.
/* lib/string.c */ char * strtok_r (char *s, const char *delimiters, char **save_ptr) { char *token; ASSERT (delimiters != NULL); ASSERT (save_ptr != NULL); /* If S is nonnull, start from it. If S is null, start from saved position. */ if (s == NULL) s = *save_ptr; ASSERT (s != NULL); /* Skip any DELIMITERS at our current position. */ while (strchr (delimiters, *s) != NULL) { /* strchr() will always return nonnull if we're searching for a null byte, because every string contains a null byte (at the end). */ if (*s == '\0') { *save_ptr = s; return NULL; } s++; } /* Skip any non-DELIMITERS up to the end of the string. */ token = s; while (strchr (delimiters, *s) == NULL) s++; if (*s != '\0') { *s = '\0'; *save_ptr = s + 1; } else *save_ptr = s; return token; }아래와 같이 strtok_r( ) 함수와 while 문을 이용하여 file_name의 모든 내용을 parsing하여 argv에 넣어주었다.
앞의 예시를 적용하여 설명하자면 argv에는 [grep\0, foo\0, bar\0]가 담겨있는 것이다.
이렇게argv를 만든 이유는 스택에 프로그램명과 각종 인자들을 넘겨주기 위함이다.
이는 load( ) 함수 마지막 부분 argument_stack( ) 함수 호출을 통해 이루어진다.
'OS > Pintos' 카테고리의 다른 글
Pintos 프로젝트 시 도움받은 블로그들! (0) 2022.01.11 Project2 : SystemCall (0) 2022.01.11 Project2: User Programs(introduction) (0) 2022.01.10 Project1 : Thread - Priority Scheduling(3) (0) 2021.12.30 Project1 : Thread - Priority Scheduling(2) (0) 2021.12.30