. [PintOS, Project2] 명령어 실행 기능 구현
본문 바로가기
Pintos Project/Project 2

[PintOS, Project2] 명령어 실행 기능 구현

by 불냥이_ 2021. 2. 17.

1. Process_Exec() 수정 

 유저 프로그램이 실행하기 전에, 커널은 레지스터에 맨 처음 function의 argument를 저장해야한다. 

 

 Process_Exec()은 유저가 입력한 명령어를 수행할 수 있도록, 프로그램을 메모리에 적재하고 실행하는 함수이다. 해당 프로그램은 f_name에 문자열로 이름이 저장되어있으나, 문제는 파일 이름 뿐만 아니라 옵션 (예를 들어서 rm -rf *을 명령어로 입력한다면, 실행 프로그램 파일은 rm이 되지만, 현재 f_name은 rm 뿐 아니라 -rf, * 도 함께 들어가있다.) 이 있기 때문에 이를 분리해줘야한다.

 

 우선 원본은 아래와 같다. 

 

@/userprog/process.c

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
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 ();

	/* And then load the binary */
	success = load (file_name, &_if);

	/* If load failed, quit. */
	palloc_free_page (file_name);
	if (!success)
		return -1;

	/* Start switched process. */
	do_iret (&_if);
	NOT_REACHED ();
}

 이 함수에서는 입력받은 명령어의 문자열을 인자로 받는다. (void *f_name)

7줄 : f_name은 문자열이지만, void *로 넘겨받았다. 이를 문자열로 인식하기 위해서 char*으로 변환해준다. 

13~16줄 : intr_frame에 실행할 때 필요한 정보들을 담아준다.

20줄 : 그리고 새로운 실행 파일을 현재 스레드에 담기 전에 먼저 현재 process에 담긴 context를 지워준다. 지워준다는 것은 현재 process에 할당된 page directory를 지운다는 것이다. 

 

23줄 : 그리고 _if (위에서 저장한 intr_frame을 말한다.) 와 file_name을 현재 프로세스에 load한다. 만약 load에 성공하면 1을 반환하고 아니면 0을 반환할 것이다. (물론 file_name은 f_name의 첫번째 문자열을 parsing하여 넘겨줘야한다.)

 

26줄 : file_name은 우리가 프로그램 파일 이름을 받기 위해 만든 임시 변수이다. 그렇기에 load가 끝났다면 해당 메모리를 반환해야한다. 

27~28줄 : 만약 load가 실패했다면 -1을 반환한다.

 

31~32줄 : 만약 load가 실행했다면 context switching 을 실시한다. 

 

 

 

2. Argument Parsing

 우리는 thread의 이름을 실행 파일 명으로 저장할 것이다. 그러나 위에서 언급한대로 현재 f_name은 유저가 입력한 커맨드 라인(cmd line)이며, cmd line에는 실행파일 이름 뿐 아니라 다른 인자들도 한꺼번에 담겨져있다. 그래서 실행파일명만 분리하여 넘겨줘야한다. 실행파일명은 cmd line 안에서 첫번째 공백 전에 해당한다. 

 

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
int process_exec (void *f_name) {
    /*-------------------------- project.2-Parsing -----------------------------*/
    char *file_name = f_name;
    char *file_name_copy[48];
    memcpy(file_name_copy, file_name, strlen(file_name) + 1);
    /*-------------------------- project.2-Parsing -----------------------------*/
	bool success;

 위에서 file_name은 f_name을 문자열로 인식하기 위해서 char *로 선언했다고 했다. 그 다음에 우리는 이 f_name을 공백을 기준으로 쪼개야한다. 그렇지만 원본을 쪼개는 것은 좋지않으므로 (딴 곳에서 f_name을 사용할 수도 있으니깐) file_name_copy를 새로 만들어줘서 깊은복사(memcpy)로 넘겨준다.

 

 memcpy(a, b, size) 는 b에서 size만큼 읽어서 a에 복사하는 함수이다. size는 strlen()로 읽어올 수 있다. 즉, strlen(file_name)을 넣으면, file_name에 있는 문자열을 읽어서 size를 반환한다. 

 

 그런데 왜 여기서 size에 strlen(file_name)이 아니라 strlen(file_name)+1을 넣을까?

 

이는 strlen()함수가 어떻게 문자열의 크기를 읽어오는 지를 보면 알 수 있다.

@/lib/string.c

/* Returns the length of STRING. */
size_t
strlen (const char *string) {
	const char *p;

	ASSERT (string);

	for (p = string; *p != '\0'; p++)
		continue;
	return p - string;
}

 char*에는 해당 문자열의 시작 주소만 있지, 사이즈에 대한 정보는 전혀 없다는 것을 생각해보자.

strlen()은 해당 문자열로부터 \n이 나올 때 까지 1byte씩 읽고, \n이 나오면 종료한다. 

우리가 콘솔창에서 명령어를 입력하고, 엔터를 치면 자동으로 명령어 끝에 \n가 들어간다. (이를 sentinel이라고 한다.)

 

 즉, cml line에는 \n도 포함되어 있는데, 여기서 memcpy에서 strlen(file_name)만 넣어주면 \n이 들어가지 않게 된다. 그런데 \n는 차후에도 계속 쓸 것이기 때문에 \n도 같이 file_name_copy에 들어갈 수 있도록 사이즈에 +1해준다. (우리는 memcpy에 char *를 넣기 때문에, size에 +1해주면, 1byte가 늘어나는 것이 아니라 char*(8byte)만큼 늘어난다. 즉, 한글자 더 읽을 수 있게 된다.)

 

 

 이제 우리는 파일명을 뽑아내야하지만, 다른 인자들 역시 프로세스를 실행하는 데 필요하므로 함께 user stack에 담아줘야한다. 그 코드는 다음과 같다. (process_exec() 안의 내용이다.) 

    /* We first kill the current context */
    process_cleanup();

    /*-------------------------- project.2-Parsing -----------------------------*/
    char *token, *last;
    int token_count = 0;
    char *arg_list[64];
    token = strtok_r(file_name_copy, " ", &last);
    char *tmp_save = token;
    arg_list[token_count] = token;
    while (token != NULL)
    {
        token = strtok_r(NULL, " ", &last);
        token_count++;
        arg_list[token_count] = token;
    }
    /* And then load the binary */
    success = load(tmp_save, &_if);
    /* If load failed, quit. */
    if (!success)
    {
        return -1;
    }
    argument_stack(arg_list, token_count, &_if);
    /*-------------------------- project.2-Parsing -----------------------------*/

 큰 줄기는 다음과 같다. arg_list라는 배열을 만들어서, 각 인자의 char*를 담아줄 것이다. 프로그램 명은 arg_list[0]에 들어갈 것이며, 2번째 인자는 arg_list[1]에 들어갈 것이다. 

 

 다행히도 이는 strtok_r()함수로 간단히 작업할 수 있다. 만약 위의 방법으로 한다면 cmd line이 rm -rf * 일 때, 이들을 ' ' 을 기준으로 쪼갠 뒤, 각각의 인자에 \0 (sentinel) 을 붙여서 저장한다. arg_list에는 [rm\0, -rf\0, *\0]가 들어가게 된다. 그리고 token_count에는 파일명을 제외한 인자의 갯수가 들어가게 된다. (rm -rf *의 경우에는 2개)

 

 그리고 load가 성공적으로 이뤄졌을 때, argument_stack함수를 이용하여, user stack에 인자들을 저장한다. 

 

 

@/userprog/process.c

void argument_stack(char **argv, int argc, struct intr_frame *if_)
{
    /* insert arguments' address */
    char *argu_address[128];
    for (int i = argc - 1; i >= 0; i--)
    {
        int argv_len = strlen(argv[i]);
        if_->rsp = if_->rsp - (argv_len + 1);
        memcpy(if_->rsp, argv[i], argv_len + 1);
        argu_address[i] = if_->rsp;
    }
    
    /* insert padding for word-align */
    while (if_->rsp % 8 != 0)
    {
        if_->rsp--;
        *(uint8_t *)(if_->rsp) = 0;
    }
    
    /* insert address of strings including sentinel */
    for (int i = argc; i >= 0; i--)
    {
        if_->rsp = if_->rsp - 8;
        if (i == argc)
            memset(if_->rsp, 0, sizeof(char **));
        else
            memcpy(if_->rsp, &argu_address[i], sizeof(char **));
    }
    
    /* fake return address */
    if_->rsp = if_->rsp - 8;
    memset(if_->rsp, 0, sizeof(void *));

    if_->R.rdi = argc;
    if_->R.rsi = if_->rsp + 8;
}

  **argv는 위에서 저장한 인자의 배열(arg_list)을 나타내고, argc는 인자의 갯수를 나타낸다.

좀 더 자세한 설명을 위해서 인자를 '/bin/ls -l foo bar'로 받았다고 하자. arg_list에 [/bin/ls\0, -l\0, foo\0, bar\0]가 담겨있다고 해보자. 그렇다면 argv는 [/bin/ls\0, -l\0, foo\0, bar\0]를 가리키는 포인터가 되고, argc는 4가 된다. 그리고 이 인자들은 user stack에 다음과 같이 담길 것이다. 

 

 

 userstack의 맨 상단은 0x47480000이다.

    /* insert arguments' address */
    char *argu_address[128];
    for (int i = argc - 1; i >= 0; i--)
    {
        int argv_len = strlen(argv[i]);
        if_->rsp = if_->rsp - (argv_len + 1);
        /* store adress seperately */
        memcpy(if_->rsp, argv[i], argv_len + 1);
        argu_address[i] = if_->rsp;
    }

 

 우선, user stack의 제일 상단부터 각 배열의 문자열 크기만큼 담아준다. (\0은 1byte이다.)

if_->rsp는 현재 user stack에서 현재 위치를 가리키는 스택 포인터(Stack Pointer)이다.

각 인자에서 인자의 크기를 읽고 ( 각 인자에는 sentinel이 포함되어 있기 때문에 strlen+1해준다.),  그 크기만큼 rsp를 내려준다. 그리고 rsp에 인자를 복사시킨다 (memcpy). 

 

 위 표에 나와있듯이 user stack인 인자인 문자열(bar\0) 그 자체 뿐 아니라, 그 문자를 새긴 곳을 가리키는 주소도 저장할 것이다(0x4747ffc0 ~ 0x4747ffd8). 그렇기 때문에 현재 문자열의 시작 위치(0x4747fffc)를 argu_address에 저장한다.

 

    /* insert padding for word-align */
    while (if_->rsp % 8 != 0)
    {
        if_->rsp--;
        *(uint8_t *)(if_->rsp) = 0;
    }

 그리고 word-align을 실행한다. 64bit이기 때문에 8byte단위로 끊어준다. rsp가 8의 배수가 될 때까지 rsp의 위치를 내려준다.

 

 

    /* insert address of strings including sentinel */
    for (int i = argc; i >= 0; i--)
    {
        if_->rsp = if_->rsp - 8;
        if (i == argc)
            memset(if_->rsp, 0, sizeof(char **));
        else
            memcpy(if_->rsp, &argu_address[i], sizeof(char **));
    }

  C 표준에서, 인자를 3개 받았으면, 밑에서부터 인자 3개의 주소를 새기고 마지막에는 0을 넣어준다 (0x4747ffe0). 그것이 if문 안의 memset()이다. 

 그리고 나머지에는 위에서 만든 argu_address에 저장한 주소를 넣어준다. memcpy()의 인자로 포인터가 들어가야하므로 argu_address[i]의 주소를 넣어준다.

 

 

    /* fake return address */
    if_->rsp = if_->rsp - 8;
    memset(if_->rsp, 0, sizeof(void *));

 인자의 주소값까지 새기고 그 밑에는 Return Address(함수를 호출하는 부분의 다음 수행 명령어 주소)를 새긴다. 원래 return address는 프로세스가 어떤 함수를 호출한다면, 그 함수는 독자적인 stack을 가지고 함수가 종료되면 다시 프로세스로 돌아가기 위한 코드 영역 주소를 새겨놓지만, 지금은 유저 프로그램을 실행하기 위한 준비 단계이므로 돌아올 곳을 표기하지 않아도 된다. 그래서 0만 존재하는 fake address를 입력한다. (C 표준 stack의 맨 아래에는 return address가 반드시 들어가야 하기 때문이다.)

 

 

    if_->R.rdi = argc;
    if_->R.rsi = if_->rsp + 8;

  그리고 intr frame의 rdi에는 인자의 갯수, rsi에는 첫 인자를 가리키는 주소의 주소(0x4747ffc0)를 적는다. (현재 rsp는 return address를 가리키고 있고, 이 바로 위에 첫 인자의 주소가 있다.) 위의 표를 기준으로하면 RDI는 4가 되고, RSI는 0x4747ffc0가 된다. 

 

 

 이로서 process_exec()을 통해 thread가 해당 file을 실행할 준비를 완료했다. 

 

 

'Pintos Project > Project 2' 카테고리의 다른 글

[PintOS, Project2] System Call  (0) 2021.02.17
[PintOS, Project 2] User Programs Introduction  (0) 2021.02.05

댓글