ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Project2 : Argument Passing
    OS/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

    1. User-level application은 %rdi, %rsi, %rdx, %rcx, %r8, %r9를 전달하기 위하여 정수 레지스터들을 사용한다.
    2. caller는 다음 인스트럭션(return address)를 stack에 넣어주고 callee의 첫 인스트럭션으로 jump한다.
      1. x86-64 인스트럭션 중에서 CALL이 이걸 해준다.
    3. callee가 실행된다.
    4. callee가 반환값을 갖는 경우에는 %rax에 넣어준다.
    5. callee는 return address를 스택에서 pop하면서 그 위치로 jump한다.
      1. x86-64 인스트럭션 중에서 RET이 이걸 해준다.

    Program Startup Details

    • 사용자 프로그램의 진입점 → /lib/user/entry.c의 _start()
      • 이 함수는 main()을 실행하고 main이 반환하면 exit()을 불러준다.
    • 커널은 유저 프로그램의 실행을 허용하기 전에 초기 함수에 대한 인수를 레지스터에 넣어줘야 한다.
      • 일반적인 calling convention과 동일한 방식으로 인자가 전달된다.
    • /bin/ls -l foo bar 가 전달될때의 예시
        1. 커맨드를 단어로 분리한다. /bin/ls, -l, foo, bar
        2. 단어들을 스택의 최상단에 넣어준다. 포인터로 참조되기 때문에 순서는 관계없다.
        3. 각 문자열의 주소와 null pointer sentinel을 스택에서 오른쪽에서 왼쪽 순서로 넣는다.
          1. 이것들이 argv의 요소들이다.
          2. null pointer를 넣음으로서 argv[argc]가 null pointer가 된다.(C표준 요구사항)
          3. word로 정렬되어 있는 경우가 더 빠르기 때문에 스택 포인터를 처음 넣기 전에 stack pointer를 8의 배수가 되도록 조정해 준다.
        4. %rsi가 argv를, %rdi가 argc를 가리키도록 한다.
        5. 마지막으로 가짜 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( ) 함수 호출을 통해 이루어진다.

    댓글

Designed by Tistory.