[Pintos-KAIST] Project 2 :: Argument Passing (User 프로그램 인자 설정하기)

2023. 5. 16. 16:22Computer Science

728x90
반응형

Pintos Project 2 관련 포스팅 목록

 

Argument passing

user 프로그램이 실행되기 전에 프로그램에 대한 인자를 설정해야 한다.

👇🏻 인자를 어떻게 처리해야 하는지 예시를 통해 먼저 알아보자!

예시) "/bin/ls -l foo bar"의 인자 처리하기

  1. 명령어를 단어로 분할한다: /bin/ls, -l, foo, bar
  2. 각 문자열과 널 포인터를 스택에 오른쪽에서 왼쪽으로 순서대로 푸시한다.
    • argv[0]를 가장 낮은 가상 주소에 위치시킨다.
    • 성능을 위해 첫 번째 푸시 전에 스택 포인터를 8의 배수로 내림하여 정렬한다.
  3. %rsi를 argv(즉, argv[0]의 주소)로 지정하고 %rdi를 argc로 설정한다.
    • 참고) 정수 레지스터
  4. 마지막으로, fake return address를 푸시한다.
    • 🤔 Why? 진입 함수는 반환하지 않지만, 다른 함수들과 동일한 스택 프레임 구조를 가져야 하기 때문이다.

 

사용자 프로그램이 시작되기 직전의 스택 상태 & 관련된 레지스터

  • 아래 예시처럼 스택에 인자를 push하고 rdi & rsi 레지스터 값을 설정해주는 것이 이번 과제의 목표이다.
  • 아래 예시에서 스택 포인터는 0x4747ffb8로 초기화된다.
| Address          | Name             | Data            | Type            |
|------------------|------------------|-----------------|-----------------|
| 0x4747fffc       | argv[3][...]     | 'bar\0'         | char[4]         |
| 0x4747fff8       | argv[2][...]     | 'foo\0'         | char[4]         |
| 0x4747fff5       | argv[1][...]     | '-l\0'          | char[3]         |
| 0x4747ffed       | argv[0][...]     | '/bin/ls\0'     | char[8]         |
| 0x4747ffe8       | word-align       | 0               | uint8_t[]       |
| 0x4747ffe0       | argv[4]          | 0               | char *          |
| 0x4747ffd8       | argv[3]          | 0x4747fffc      | char *          |
| 0x4747ffd0       | argv[2]          | 0x4747fff8      | char *          |
| 0x4747ffc8       | argv[1]          | 0x4747fff5      | char *          |
| 0x4747ffc0       | argv[0]          | 0x4747ffed      | char *          |
| 0x4747ffb8       | return address   | 0               | void (*) ()     |

첫번째 인자, 두번째 인자
RDI: 4 | RSI: 0x4747ffc0

 

Implement the argument passing

Todo

  • command line을 parsing해서 스레드의 이름을 식별한다.
  • 해당 파일 이름을 갖는 프로그램을 찾는다.
  • user stack에 인자를 push한다.

코드의 흐름

init.c → int main(void) → run_actions(argv) → run_task(char **argv) → process_create_initd(task) → thread_create (file_name, PRI_DEFAULT, initd, fn_copy) → initd→process_exec → load, do_iret

1. process_create_initd()

💡 목표: command line을 parsing해서 file_name을 찾는다.

  • 인자로 들어오는 file_name이 실행 시 입력된 (parsing 해야 하는) command line이다.
  • 이 command line을 parsing해서 파일 이름을 찾는다.
  • parsing해서 얻어낸 파일 이름이 thread_create 함수의 첫번째 인자로 들어가야 한다.
tid_t process_create_initd(const char *file_name)
{
    char *fn_copy;
    tid_t tid;

    /* Make a copy of FILE_NAME.
     * Otherwise there's a race between the caller and load(). */
    fn_copy = palloc_get_page(0);
    if (fn_copy == NULL)
        return TID_ERROR;
    strlcpy(fn_copy, file_name, PGSIZE);

    // Argument Passing ~
    char *save_ptr;
    strtok_r(file_name, " ", &save_ptr);
    // ~ Argument Passing

    /* Create a new thread to execute FILE_NAME. */
    tid = thread_create(file_name, PRI_DEFAULT, initd, fn_copy);
    if (tid == TID_ERROR)
        palloc_free_page(fn_copy);
    return tid;

 

2. process_exec()

💡 목표: 인자로 들어오는 f_name을 parsing하고 user stack에 매개변수들을 push한다.

  • pintos에서 command line의 길이에는 128바이트 제한이 있으므로 parsing한 인자를 담을 parse 배열의 길이는 64로 지정한다.
  • strtok_r 함수를 활용해 parsing한 결과를 count와 parse에 담아두고, 아래에서 새로 선언한 argument_stack 함수를 호출할 때 전달한다.
  • 인자의 개수와 argv 시작 주소를 각각 rdi와 rsi에 저장한다.
    👉🏻 스택에 마지막에 추가한 fake address를 담기 직전의 주소가 argv의 시작 주소이므로, rsi에는 현재 스택 포인터 rsp에서 8만큼 더한 값을 저장한다.
  • 결과를 확인하기 위해 hex_dump() 함수를 사용한다. 이 함수는 메모리의 내용을 16진수 형식으로 출력해줘서 스택에 저장된 값들을 확인할 수 있다. 👍🏻
    (아직 make check로 테스트를 할 수 없어서 이 방법으로 결과를 확인해야 한다.)
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();

    // Argument Passing ~
    char *parse[64];
    char *token, *save_ptr;
    int count = 0;
    for (token = strtok_r(file_name, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr))
        parse[count++] = token;
    // ~ Argument Passing

    /* And then load the binary */
    success = load(file_name, &_if);
    // 이진 파일을 디스크에서 메모리로 로드한다.
    // 로드된 후 실행할 메인 함수의 시작 주소 필드 초기화 (if_.rip)
    // user stack의 top 포인터 초기화 (if_.rsp)
    // 위 과정을 성공하면 실행을 계속하고, 실패하면 스레드가 종료된다.

    // Argument Passing ~
    argument_stack(parse, count, &_if.rsp); // 함수 내부에서 parse와 rsp의 값을 직접 변경하기 위해 주소 전달
    _if.R.rdi = count;
    _if.R.rsi = (char *)_if.rsp + 8;

    hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)_if.rsp, true); // user stack을 16진수로 프린트
    // ~ Argument Passing

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

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

 

3. argument_stack()

💡 목표: process_exec() 함수에서 parsing한 프로그램 이름과 인자를 스택에 저장하기 위해 사용할 함수를 새로 선언한다.

1) 프로그램 이름, 인자 문자열 push

스택은 아래 방향으로 성장하므로 스택에 인자를 추가할 때 string을 오른쪽에서 왼쪽 방향으로 (역방향으로) push해야 한다.

 

2) 정렬 패딩 push

각 문자열을 push 하고나서 8byte 단위로 정렬하기 위해 필요한만큼 padding을 추가한다.

 

3) 인자 문자열 종료를 나타내는 0 push

정렬 하고 나서 0을 push한다. (인자 문자열들의 종료를 의미함)

 

4) 각 인자 문자열의 주소 push

인자 문자열 push하면서 parse에 담아둔 각 문자열의 주소를 push한다.

 

5) return address push

다음 인스트럭션의 주소를 push해야 하는데, 지금은 프로세스를 생성하는 거라서 반환 주소가 없다! 따라서 fake return address로 0을 추가한다.

/* process.h */

// parse: 프로그램 이름과 인자가 담긴 배열
// count: 인자의 개수
// rsp: 스택 포인터를 가리키는 주소 값
void argument_stack(char **parse, int count, void **rsp);
/* process.c */

void argument_stack(char **parse, int count, void **rsp) // 주소를 전달받았으므로 이중 포인터 사용
{
    // 프로그램 이름, 인자 문자열 push
    for (int i = count - 1; i > -1; i--)
    {
        for (int j = strlen(parse[i]); j > -1; j--)
        {
            (*rsp)--;                      // 스택 주소 감소
            **(char **)rsp = parse[i][j]; // 주소에 문자 저장
        }
        parse[i] = *(char **)rsp; // parse[i]에 현재 rsp의 값 저장해둠(지금 저장한 인자가 시작하는 주소값)
    }

    // 정렬 패딩 push
    int padding = (int)*rsp % 8;
    for (int i = 0; i < padding; i++)
    {
        (*rsp)--;
        **(uint8_t **)rsp = 0; // rsp 직전까지 값 채움
    }

    // 인자 문자열 종료를 나타내는 0 push
    (*rsp) -= 8;
    **(char ***)rsp = 0; // char* 타입의 0 추가

    // 각 인자 문자열의 주소 push
    for (int i = count - 1; i > -1; i--)
    {
        (*rsp) -= 8; // 다음 주소로 이동
        **(char ***)rsp = parse[i]; // char* 타입의 주소 추가
    }

    // return address push
    (*rsp) -= 8;
    **(void ***)rsp = 0; // void* 타입의 0 추가
}

 

4. process_wait

  • 다음 과제에서 구현하는 함수인데, argument passing 테스트를 하려면 일단 wait를 흉내라도 내야한다. 따라서, 임시방편으로 반복문을 추가한다.
  • infinite loop를 추가하도록 제시되어 있지만, 그렇게 하면 테스트할 때 프로세스를 수동으로 종료시켜야 하는 것이 번거로워서 반복문으로 대체했다.
    int process_wait(tid_t child_tid UNUSED)
    {
      /* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
       * XXX:       to add infinite loop here before
       * XXX:       implementing the process_wait. */
      for (int i = 0; i < 100000000; i++)
      {
      }
      return -1;
    }

 

Test 방법 & 결과

이 명령어를 입력했을 때 아래 사진과 같은 결과가 나와야 한다.
GG의 개수와 command_line이 끝까지 잘 출력되었는지 확인하는 것이 중요하다!

pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'

728x90
반응형