[PintOS] Project2: Argument Passing

김상호·2022년 6월 21일
1

Development Log

목록 보기
33/45

Argument Passing

과제목표

process_exec() 내에 사용자 프로그램을 위한 인자를 셋업해라.

유저 프로그램을 실행하기 전에, 커널은 레지스터에다가 맨 처음 function의 argument를 저장해야 한다. process_exec()은 유저가 입력한 명령어를 수행할 수 있도록 프로그램(=process)을 메모리에 적재하고 실행하는 함수이다. 해당 프로그램은 f_name에 문자열로 저장되어 있으나 현재 상태에서 process_exec() 은 새로운 프로세스에 대한 인자 passing을 제공하지 않는다. 이 기능을 구현하는 것이 이번 과제이다. process_exec() 에 코드를 추가해서 간단히 프로그램 파일 이름을 인자로 넣는것 대신에, space가 올 때마다 단어를 parsing하도록 만들어야 한다. 이때,  첫 번째 단어는 프로그램 이름이고 두세 번째 단어는 각각 첫 번째, 두 번째 인자이다.

명령어 라인과 함께, 여러 개 space는 하나의 space와 동일하게 취급해야 한다. 이때 명령어 인자의 길이에 제한을 둘 수 있다.

구현

process_create_initd() 수정
원본 코드

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);

	/* 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;
}

현재 process_create_initd() 함수는 커맨드 라인의 첫 번째 토큰을 thread_create() 함수의 첫 인자로 전달 되도록 프로그램을 수정해야 한다. 현재는 커맨드 라인 전체가 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);

	/* Create a new thread to execute FILE_NAME. */
	char *save_ptr;
	strtok_r(file_name, " ", &save_ptr);

	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
	if (tid == TID_ERROR)
		palloc_free_page (fn_copy);
	return tid;
}

strtok_r() : strtok_r 함수는 지정된 문자(이를 delimiters라고 한다)를 기준으로 문자열을 자른다.

예를 들어 위 코드에서 token = strtok_r(file_name, " ", &save_ptr);이라고 하면 file_name의 가장 첫번째 문자열이 나온다. 긴 말 할 것 없이 아래 예시를 보자. "The little prince"를 공백을 기준으로 잘라서 출력한다고 하자. 그러면 출력값은 "The", "Little", "Prince"일 것이다. strtok_r은 한글자씩 이동하다가(T, h, e, ...) 공백을 만나면 그곳에 NULL 값을 넣은 다음, 그 앞까지의 문자열(=The)을 반환한다. 여기서 &the_last는 그 뒤의 문자열("\nLittie Prince\n")에서 가장 첫번째 문자의 주소값을 나타낸다. 이렇게 한 글자씩 잘라서 반환하는 개념이기에 while문을 돌리면서 한글자씩 뽑아내는 것으로 이해하면 된다.

load() 수정
원본 코드

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;

	/* Allocate and activate page directory. */
	t->pml4 = pml4_create ();
	if (t->pml4 == NULL)
		goto done;
	process_activate (thread_current ());

	/* Open executable file. */
	file = filesys_open (file_name);
	if (file == NULL) {
		printf ("load: %s: open failed\n", file_name);
		goto done;
	}
    
    	•••

원래 코드 이후에는 load()를 실행하는 코드가 나온다. load()함수는 실행파일의 file_name을 적재해 실행하는 함수이다. load()를 부른 caller인 process_exec()에서 입력한 커맨드 전체가 file_name인자로 넘어온다.

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;

	/* Allocate and activate page directory. */
	t->pml4 = pml4_create ();
	if (t->pml4 == NULL)
		goto done;
	process_activate (thread_current ());

	char *token, *save_ptr;
	char *argv[64];
	uint64_t cnt = 0;

	for (token = strtok_r(file_name, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr)) {
		argv[cnt++] = token;
	}

	/* Open executable file. */
  	file = filesys_open (argv[0]);
	if (file == NULL) {
		printf ("load: %s: open failed\n", file_name);
		goto done;
	}
		
        •••

for 문을 보면 매개변수로 받은 인자 file_name을 NULL을 기준으로 문자열을 잘라서 argv 배열에 하나씩 넣어준다.

		•••
        
	/* 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). */
  
	argument_stack(argv, cnt, &if_->rsp);
	if_->R.rdi = cnt;
	if_->R.rsi = if_->rsp + 8;

	success = true;

 	 // * 추가
	t->running_file = file;
	file_deny_write(file);

done:
	/* We arrive here whether the load is successful or not. */
	return success;
}

다음은 인자값을 스택에 올리는 함수 argument_stack()이 필요하다. 위에서 parsing한 다음 한 문자씩 넣어준 배열 arg_list와 count값인 token_count, 그리고 인터럽트 프레임도 인자로 넣는다. 이 함수 자체에서 인터럽트 프레임을 스택에 올리는 것은 아니고, 인터럽트 프레임 내 구조체 중 특정값(rsp)에 인자를 넣어주기 위함이다. 이후에 do_iret()에서 이 인터럽트 프레임을 스택에 올린다.

argument_stack() 추가

argument_stack() 함수는 유저 스택에 프로그램 이름과 인자들을 저장하는 함수이다.
parse : 프로그램 이름과 인자가 저장되어 있는 메모리 공간, count : 인자의 개수, esp : 스택 포인터를 가리키는 주소

void argument_stack(char **parse, int count, void **esp) {
  
  char *argv_address[count];
  uint8_t size = 0;

	// * argv[i] 문자열
	for (int i = count - 1; -1 < i; i--) {
		*esp -= (strlen(parse[i]) + 1);
		memcpy(*esp, parse[i], strlen(parse[i]) + 1);
		size += strlen(parse[i]) + 1;
		argv_address[i] = *esp;
	}

	if (size % 8) {
		for (int i = (8 - (size % 8)); 0 < i; i--) {
			*esp -= 1;
		**(char **)esp = 0;
	}
  }

  *esp -= 8;
  **(char **)esp = 0;

  // * argv[i] 주소
	for (int i = count - 1; -1 < i; i--) {
		*esp = *esp - 8;
		memcpy(*esp, &argv_address[i], strlen(&argv_address[i]));
	}

	// * return address(fake)
	*esp = *esp - 8;
	**(char **)esp = 0;

}

argumentstack() 코드를 뜯어보자. 배열 arg_address[128]은 아래 for문에서 스택에 담을 각 인자의 주소값을 저장하는 배열이다. 이후 for문을 돌면서 process_exec()에서 넣어주는 arg_list로부터 값을 하나하나씩 꺼내서 if->rsp에 하나씩 넣어준다. 이때 if_->rsp는 user stack에서 현재 위치를 가리키는 스택 포인터이자 인터럽트 프레임 내 멤버이다. 여기서 작업이 Gitbook에 나오는 테이블에 값을 채워넣는 것과 같다.

이때, 각 인자에서 인자 크기(argv_len)을 읽는데 이때 각 인자마다는 실제로 sentinel(\n)이 포함되어 있는데 여기서 역시 strlen은 sentinel을 읽지 않으니 +1을 해주는 것이다. for문 순서를 자세히 보면

  1. 먼저 스택 포인터를 넣어줄 공간만큼 쭉 내린다.(if->rsp = if->rsp - (argv_len +1)
  2. 그다음, 해당 공간에 인자값을 복붙한다(memcpy(if_->rsp, argv[i], argv_len+1))
  3. argaddress 배열에 인자값이 위치한 주소를 저장한다. (arg_address[i] = if->rsp)

이후에는 while문을 돌면서 패딩을 삽입한다.

이번에는 주소값 자체를 삽입한다. 이래서 위에 arg_address[] 배열을 따로 만들어 여기에 주소값을 저장한 이유이다. 똑같이 for문들 돌면서 넣는데, 처음 for문에서는(int i = argc-1)로 선언된 반면 여기 for문에서는 (int i = argc)로 선언된다. 이는 앞서 process_exec()에서 while문을 돌며 token을 받아오는 것을 보면, 맨 마지막 인자값으로 NULL을 받아 arg_list에 저장하게 된다. 따라서 argv[-1]= '\n'이 된다. 근데 이 인자 값을 스택에 저장할 때는 맨 끝에 있는 NULL 값을 저장하지 않은 반면 여기서는 NULL값 가리키는 포인터를 저장한다.

for문을 돌고 나면  fake address를 넣어준다. 해당 영역의 메모리는 0으로 초기화해준다.
마지막으로 인터럽트 프레임 if_의 멤버로 있는 레지스터 구조체의 rdi에 인자 count값인 argc, 그리고 rsi에는 fake address바로 위인 arg_address의 맨 앞을 가리키는 주소값을 넣는다.

Interrupt Frame 인터럽트 프레임 (struct Intr_frame)

인터럽트 프레임은 인터럽트가 들어왔을 때, 이전에 레지스터에 작업하던 context를 switching하기 위해 이 정보를 담아놓는 구조체이다. 그래서 구조체 intr_frame에 가보면 엄청 복잡하게 나와 있고, 그안에 멤버 구조체 gp_registers R을 들고 있다. 이 R은 기존 스레드가 작업하고 있을 때의 레지스터 값을 인터럽트가 들어오면 switching하기 위해 이 구조체에다가 정보를 담는다. 그래서 1주차에 do_schedule() 을 보면do_iret()이 나오고, 이 do_iret()는 어셈블리어로 되어 있는데, 여기가 기존까지 작업했던 context를 intr_frame에 담는 과정이라고 보면 되겠다.

즉, 인터럽트 프레임은 인터럽트와 같은 요청이 들어와서 기존까지 실행 중이던 context(레지스터 값 포함)를 스택에 저장하기 위한 구조체이다.

결과

결과(중간 결과)를 확인하기 위해서는 process_exec()함수에 hex_dump() 함수를 추가해줘야 한다.

hex_dump(_if.rsp, _if.rsp, KERN_BASE - _if.rsp, true);

process_exec()함수 중간에 load가 끝난 후 다음 위에 함수를 추가해준다.


또한 process_wait() 함수에 무한루프를 추가해줘야 결과를 제대로 볼 수 있다.

while (1){}

다음 구문을 함수 내 return 전에 추가해준다. 밑에 사진은 현재 pintos의 상태를 나타내는 사진이다.

결과

PintOS Project2 GIthub 주소 PintOS

1개의 댓글

comment-user-thumbnail
2022년 9월 29일

안녕하세요. 현재 프로젝트 2를 구현 중인 학생입니다. 테스트 케이스를 돌릴 때 hex_dump의 출력은 잘 되는데 그 뒤에 interrupt 0x0d..이런 식으로 exception이 뜨고 프로그램이 오류를 냅니다. 프로젝트 1의 모든 테스트 케이스는 통과한 상태이며, 혹시 어떤 방법으로 이 오류를 접근해야 하는지 도움을 주실 수 있겠습니까?

답글 달기