거북이의 쉼터

(2022.02.02) Argument Passing 구현 본문

코딩 삽질/KAIST PINTOS (CS330)

(2022.02.02) Argument Passing 구현

onlim 2022. 2. 2. 16:02

우선 오랜만에 다시 시작한 프로젝트여서 제대로 작동하는지 여부를 보기 위해 프로젝트 1을 다시 검사했다.

All 27 tests passed.
make[1]: Leaving directory '/home/z3r0/pintos-kaist/threads/build'

결과는 잘 동작한다. 이제 Argument Passing을 구현할 차례이다.

 

지난 가이드라인에서 file_name으로 들어오는 문자열을 파싱해야 한다고 적어놨었다. 매뉴얼에서는 파싱에서 어려움이 있을 때는 strtok_r 함수를 사용하라고 적어놨는데, man strtok_r을 해보면 다음의 예시코드가 나온다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int
main(int argc, char *argv[])
{
   char *str1, *str2, *token, *subtoken;
   char *saveptr1, *saveptr2;
   int j;

   if (argc != 4) {
       fprintf(stderr, "Usage: %s string delim subdelim\n",
               argv[0]);
       exit(EXIT_FAILURE);
   }

   for (j = 1, str1 = argv[1]; ; j++, str1 = NULL) {
       token = strtok_r(str1, argv[2], &saveptr1);
       if (token == NULL)
           break;
       printf("%d: %s\n", j, token);

       for (str2 = token; ; str2 = NULL) {
           subtoken = strtok_r(str2, argv[3], &saveptr2);
           if (subtoken == NULL)
               break;
           printf(" --> %s\n", subtoken);
       }
   }

   exit(EXIT_SUCCESS);
}

오랜만에 다시 보는 함수라 기억은 가물가물하지만, 코드를 보면서 다음의 형식으로 불러야 했던 것이 기억났다.

 

  • 가장 처음 호출할 때는 strtok_r([파싱할 문자열], [파싱의 기준이 되는 문자들], [char 포인터 주소])로 놓고 반환되는 값(포인터)이 가리키는 곳에 파싱된 문자열의 결과가 있다.
  • 두 번재 부터는 가장 처음 인자로 넣어주었던 [char 포인터 주소]가 파싱할 문자열과 관련된 주소를 가지고 있으므로, strtok_r(NULL, [파싱의 기준이 되는 문자들], [char 포인터 주소])로 호출한다. 반환 결과는 역시 파싱된 문자열이다.
  • 더 이상 파싱할 것이 남아있지 않으면 NULL이 반환된다.

이러한 점을 상기하면서 코딩을 해야 한다.

 

파싱한 argument를 stack에 넣는 순서는 상관이 없다고 매뉴얼에서 나와 있지만, 추후 혼동을 피하기 위해 매뉴얼과 같은 순서로 넣기로 하자. stack은 위에서 아래로 자란다. 다시 설명하자면 stack은 높은 주소부터 시작해 낮은 주소 방향으로 data를 축적한다. 매뉴얼대로 argument를 넣기 위해서는 우선 모든 파싱이 끝난 뒤에 역순으로 argument를 넣어주어야 한다는 사실을 알 수 있다. 이를 위해서는 argument를 임시적으로 저장할 장소가 필요하며, malloc을 통해 임시저장 공간을 만들어줄 수 있다. malloc을 호출하여 사용하려면 공간을 어느정도 할당할지를 알아야 하는데, 이를 위해서는 argument의 개수 argc를 우선 파악해야 한다. 따라서 순차적으로 따져보면 파싱이 두 번 필요함을 알 수 있다.

 

  1. argc 파악을 위한 1회 파싱
  2. 파악된 argc를 기반으로 malloc을 통해 argv 임시공간 할당 이후 임시공간에 파싱된 각 argument의 포인터 저장을 위한 1회 파싱 

이러한 2회 파싱 때문에 file_name의 복사본을 파싱하는 것이 더 좋겠다고 판단했고, malloc으로 file_name_copy라는 임시공간을 만들어 file_name을 file_name_copy로 복사해서 파싱하였다.

 

파싱된 결과물을 stack에 집어넣는 과정은 간단하다. 우선 파싱된 각 argument의 경우, argv 임시공간에 string 포인터가 있으므로, 각 string의 길이만큼 감산해가며 strlcpy를 사용하여 복사한다. 또한, 추후 stack 내 해당 argument를 가리키는 포인터가 필요하기 때문에 argv 임시공간에 stack 포인터를 저장해가면서 진행한다.

 

각 argument를 전부 복사했다면 이제 word-align을 맞추고, 각 argument를 가리키는 포인터값을 stack에 넣어야 한다. align은 포인터 크기인 8의 배수가 되도록 맞추면 되며, 포인터값은 다시 임시공간에서 루프를 역순으로 돌면서 stack에 넣어주면 된다. 

 

이제 마지막으로 rsi, rdi 레지스터를 argv, argc로 설정하고 fake return address를 넣어주면 된다. argc는 1회차 파싱때 파악했으므로 쉽고, argv는 stack에 가장 마지막으로 포인터값을 넣었을 때의 rsp 값이므로 그대로 넣어주면 된다. setup_stack을 할 때 stack의 값이 전부 0으로 채워지게 되어있으므로 rsp만 다시 8을 줄이면 fake return address도 설정이 끝난다. 여기까지 stack에 값을 채우는 것이 완료되었다.

 

실제로 구현한 코드는 아래와 같다.

static bool
load (const char *file_name, struct intr_frame *if_) {
	...
	
	/* Parse file_name into real name and arguments */
	char *file_name_copy = (char *) malloc (strlen (file_name) + 1);
	strlcpy (file_name_copy, file_name, strlen (file_name));
	
	char *r_file_name = NULL;
	char *ptr = NULL;
	
	r_file_name = strtok_r(file_name_copy, " ", &ptr);

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

	/* Read and verify executable header. */
	if (file_read (file, &ehdr, sizeof ehdr) != sizeof ehdr
			|| memcmp (ehdr.e_ident, "\177ELF\2\1\1", 7)
			|| ehdr.e_type != 2
			|| ehdr.e_machine != 0x3E // amd64
			|| ehdr.e_version != 1
			|| ehdr.e_phentsize != sizeof (struct Phdr)
			|| ehdr.e_phnum > 1024) {
		printf ("load: %s: error loading executable\n", r_file_name);
		goto done;
	}
	
	...
	
	/* 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). */

	char *token = NULL;
	int argc = 1; // already parsed real file name

	while ((token = strtok_r(NULL, " ", &ptr)) != NULL)
		argc += 1;

	char **argv = (char **) malloc (sizeof(char *) * argc);
	strlcpy(file_name_copy, file_name, strlen(file_name));

	ptr = NULL;
	int idx = 0, len;
	
	for (token = strtok_r(file_name_copy, " ", &ptr); token != NULL; 
	     token = strtok_r(NULL, " ", &ptr), idx++)
		argv[idx] = token;

	for (idx = argc - 1; idx >= 0; idx--)
	{
		len = strlen(argv[idx]);
		if_->rsp -= (len + 1);
		strlcpy(if_->rsp, argv[idx], len);
		argv[idx] = if_->rsp; // for filling in argv part of stack
	}
	
	if_->rsp -= (((uint64_t)if_->rsp) & 7); // word-align
	if_->rsp -= 8; // argv[argc] = NULL

	for (idx = argc - 1; idx >= 0; idx--)
	{
		if_->rsp -= 8;
		*((uint64_t *)if_->rsp) = argv[idx];
	}

	// fill in rdi, rsi
	if_->R.rdi = argc;
	if_->R.rsi = if_->rsp;

	// fill in fake return address
	if_->rsp -= 8;

	hex_dump(if_->rsp, if_->rsp, 100, true);

	free(file_name_copy);
	free(argv);

	success = true;

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

여기까지 하고 hex_dump를 이용해 argument의 파싱이 잘 이루어졌는지 분석해보려고 했다. 근데 아무것도 나오지 않는다. 

 

 

왜인지 실행결과를 보면서 분석해 보았다.

 


실행결과 중 이 부분을 살펴보자.

Executing 'args-multiple some arguments for you!':
Execution of 'args-multiple some arguments for you!' complete.

해당 부분은 다음 코드에 의해 출력이 된 것이다.

/* Runs the task specified in ARGV[1]. */
static void
run_task (char **argv) {
	const char *task = argv[1];

	printf ("Executing '%s':\n", task);
#ifdef USERPROG
	if (thread_tests){
		run_test (task);
	} else {
		process_wait (process_create_initd (task));
	}
#else
	run_test (task);
#endif
	printf ("Execution of '%s' complete.\n", task);
}

USERPROG는 define되어 있으니 process_wait (process_create_initd (task)); 가 제대로 실행이 안되고 있는 것이다. process_create_initd는 별 문제가 없어보였으나, process_wait에서 다음과 같은 주석을 발견했다.

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

즉, process_wait이 제대로 구현되기 전에는 무한루프를 추가하라는 것이다. 무한루프를 다음과 같이 추가하였고,

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. */
	while (true)
	{

	}
	return -1;
}

이제 무언가 실행이 되기 시작했다.

 

hex_dump된 부분을 확대하면 다음과 같다.

000000004747ffa0  00 00 00 00 00 00 00 00-db ff 47 47 00 00 00 00 |..........GG....|
000000004747ffb0  e9 ff 47 47 00 00 00 00-ee ff 47 47 00 00 00 00 |..GG......GG....|
000000004747ffc0  f8 ff 47 47 00 00 00 00-fc ff 47 47 00 00 00 00 |..GG......GG....|
000000004747ffd0  00 00 00 00 00 00 00 00-00 00 00 61 72 67 73 2d |...........args-|
000000004747ffe0  6d 75 6c 74 69 70 6c 00-00 73 6f 6d 00 00 61 72 |multipl..som..ar|
000000004747fff0  67 75 6d 65 6e 74 00 00-66 6f 00 00 79 6f 00 00 |gument..fo..yo..|

파싱된 결과물들이 조금씩 잘렸다. 찾아보니 strlcpy는 맨 마지막에 지정한 인자를 n이라 할 때 최대 n-1까지만 복사해준다고 한다. 관련된 코드를 전부 수정한 뒤 다시 실행하였다. 그 결과 중 hex_dump 된 부분만을 보면

000000004747ffa0  00 00 00 00 00 00 00 00-da ff 47 47 00 00 00 00 |..........GG....|
000000004747ffb0  e8 ff 47 47 00 00 00 00-ed ff 47 47 00 00 00 00 |..GG......GG....|
000000004747ffc0  f7 ff 47 47 00 00 00 00-fb ff 47 47 00 00 00 00 |..GG......GG....|
000000004747ffd0  00 00 00 00 00 00 00 00-00 00 61 72 67 73 2d 6d |..........args-m|
000000004747ffe0  75 6c 74 69 70 6c 65 00-73 6f 6d 65 00 61 72 67 |ultiple.some.arg|
000000004747fff0  75 6d 65 6e 74 73 00 66-6f 72 00 79 6f 75 21 00 |uments.for.you!.|

제대로 stack이 구성되었음을 알 수 있다. 여기에 추가로 rdi, rsi까지 검증하면 5, 0x4747ffa8로 바르게 나온다.

 

여기까지 argument parsing은 성공적으로 마무리했다. 이제 나머지 과제도 매뉴얼을 읽어보면서 차근차근 진행해보자.

 

PS. 오랜만에 와서 OS 전용 틀을 만들어 놓은 것을 까먹고 신나게 글을 쓰다보니 너무 많이 써버렸다.

다음부터는 형식대로 진행하겠다...

Comments