PintOS 2주차 Argument Passing에 대해서 정리해봄

Designated Hitter Jack·2023년 10월 7일

SW 사관학교 정글

목록 보기
24/44
post-thumbnail

Argument Passing 구현 목표

현재 process.c의 int process_exec() 함수에서는
인자로 f_name을 받는다. 하지만 이 f_name은 user가 입력한 command line 전체가 들어가 있는데 이를 프로그램 이름과 인자로 나누어서 함수로 보내야 한다.

process_exec ()

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 ();
}
  • char *file_name = f_name;
    위의 인자는void 형인데 처리해야 하는 command line은 문자열이니 문자열 포인터로 형을 변경한다.

  • struct intr_frame _if;
    intr_frame 구조체 멤버에 필요한 정보를 담는다. 여기서 intr_frame은 인터럽트 스택 프레임이다.

  • process_cleanup ();
    새로운 실행 파일을 현재 스레드에 담기 전에 먼저 현재 process에 담긴 context를 지워준다. 이때 지운다는 말은 현재 프로세스에 할당된 page directory를 지운다는 뜻이다.

  • success = load (file_name, &_if);
    file_name, _if를 현재 프로세스에 load한다. success는 bool type이니 load에 성공하면 1, 실패하면 0을 return한다.

  • palloc_free_page (file_name);
    file_name은 프로그램 파일을 받기 위해 만든 임시 변수이다. load가 끝나면 메모리를 반환한다.
    page의 할당은 위의 load 함수 내의 setup_stack 함수 내부에서 이루어진다. 여기서 할당한 페이지를 해제하기 위한 함수이다.

우선 file_name을 parse 하기 전에 원본을 남겨놓는다.

int
process_exec (void *f_name) {
	char *file_name = f_name;
	bool success;

	//project 2. argument passing
	//original file_name copy
	char file_name_copy[128];
	//+1은 \n이 들어갈 자리
	memcpy(file_name_copy, file_name, strlen(file_name) + 1);
	//project 2. argument passing 
	
    ...

pintos에서 커널에 입력할 수 있는 문자열의 길이가 128 바이트라고 자료에 나와있었으므로 원본을 복사할 배열의 크기를 128 바이트로 정하고 원본 문자열을 memcpy로 복사했다. strlen에 1을 더하는 이유는 널 문자(\0) 때문.

본격적인 parsing은 load 함수 내에서 실행된다.

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;

	//project 2. argument passing
	uint64_t len_include_args = strlen(file_name) + 1;
	char *argv[128];
	memset(argv, 0, sizeof(argv));
	char *save_ptr = NULL;
	//첫번째 인자 = 프로그램 이름	
	char *token = strtok_r(file_name, " ", &save_ptr);
	uint64_t argc = 0;

	while (token != NULL) {
		argv[argc++] = token;
		token = strtok_r (NULL, " ", &save_ptr);
	}
	ASSERT (argc > 0);
	ASSERT (argv[argc] == NULL);
	//project 2. argument passing


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

문자열의 parsing에는 strtok_r함수를 사용하면 된다.
이 함수는 lib/string.c에 선언되어 있다.
함수 위에 (영어로 된) 주석이 상세하게 설명되어 있는데 내 나름대로 간단하게 이해한 것이 다음과 같다.

char *s로 입력받은 문자열을 
const char *delimiters (여기서는 " " 공백)이 나오면 
이를 NULL로 치환하고 save_ptr를 넘기고, 
아닌게 나오면 char *token 이라는 포인터에 save_ptr을 옮긴다.
token에 들어갈 문자열을 찾았다면 다음 공백이 나올때 까지 save_ptr을 옮긴다. 
이를 널문자(\0)가 나올 때 까지 반복한다.

이와 같은 과정을 통해서 strtok_r 함수를 반복하면 *token이라는 변수 안에 공백으로 구분되는 단어가 하나씩 들어가게 된다.
이후 인자를 몇개나 가지고 있는지를 파악하기 위해서 반복문을 돌려 argc의 값을 알아낸다.

테스트를 통과하기 위해선 argv[argc]값, 즉 배열 argv에 인자를 다 담고 난 후의 원소가 NULL이어야 하는데 (배열이 끝났음을 알아내는 방법) 이는 char *argv[128]; 로 argv 배열을 선언하고 memset(argv, 0, sizeof(argv));으로 해당 배열을 전부 0으로 초기화 해버렸기 때문에 자연스럽게 이루어진다.

	/* TODO: Your code goes here.
	 * TODO: Implement argument passing (see project2/argument_passing.html). */
	
	//project 2. argument passing
	//USER_STACK = 0x47480000
	// stack 공간확보 -> rsp를 아래로 이동 (uint64_t 이므로 -1당 1씩 빠진다)
	if_->rsp = (uintptr_t)((uint64_t) if_->rsp - ROUND_UP(len_include_args, 8));

	/* Put argv[i][...] into stack */
	uint64_t write_point = if_->rsp;
	for (int i = 0; i < argc; i++) {
		uint64_t argv_len = strlen(argv[i]) + 1;
		memcpy((void *) write_point, (void *) argv[i], argv_len);
		argv[i] = write_point; // stack에서의 argv[i]가 저장된 주소를 argv[i]에 저장

		// printf("argv[%d] place in 0x%p\n", i, argv[i]);
		write_point += argv_len; // 다음 복사위치로 이동
	}

	/* Put argv[i] into stack */
	// 현재 argv[i] 에는 stack에서의 인자가 저장된 주소를 가지고 있다. ex) %rsp + 11
	for (int i = argc; i >= 0; i--) {
		if_->rsp -= sizeof(uint64_t);
		memcpy((void *) if_->rsp, (void *) &argv[i], sizeof(uint64_t));

		// char *temp = argv[i];
		// __asm __volatile(
        //     /* Fetch input once */
        //     "movq %0, %%rax\n"
        //     "movq %1, %%rcx\n"
        //     "movq %%rcx, (%%rax)\n"
        //     : : "g"(if_->rsp), "g"(temp) : "memory");
		// printf("rsp: %p, ptr: %p, value: %p, string: %s\n", if_->rsp, &argv[i], argv[i], argv[i]);
	}

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

	/* Put argc, argv into register */
	// %rdi: argc   %rsi: &argv[0]
	if_->R.rdi = argc;
	if_->R.rsi = (uint64_t) (if_->rsp + sizeof(uint64_t));

	// hex_dump(if_->rsp, if_->rsp, USER_STACK - if_->rsp, true);
	//project 2. argument passing
	success = true;

이후 스택에 데이터를

이런 모양으로 쌓을 수 있도록 저장하면 된다.
위의 코드는 데이터와 패딩의 위치가 그림과 반대지만 어쨌든 바이트에 맞게 정렬은 된 것이다.
주의해야 할 점은 전부 다 쌓은 후 R -> rsi가 위의 표의 return address 위치가 아니라 argv[0]의 위치를 가리키게 해야 한다는 것이다.

데이터를 잘 쌓았는지 터미널로 확인하려면 hex_dump라는 함수를 통해서 확인할 수 있다.

test

잘 돌아가는지 테스트를 하기 위해선

root에서 source ./activate
userprog 디렉토리에서 make
buidl 디렉토리에서
# -v: no vga, -k: kill-on-failure, --fs-disk: 임시 디스크 생성, -p: put, -g: get  // -f: format
pintos -v -k --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'

같이 명령어를 입력하면 되는데, 현재 핀토스의 프로세스 흐름상 뭐가 돌아가기도 전에 프로세스가 종료되게 된다. 이를 임시로 방지하기 위해선 process_wait함수 위에 주석이 달려있는 것 처럼 process_wait 함수가 끝나는 걸 막기 위해 무한루프를 걸어놓으면 된다.
이후 system call을 구현하는 과정에서 wait를 제대로 동작하게 짜면 이런 일은 발생하지 않을것이다.

/* Waits for thread TID to die and returns its exit status.  If
 * it was terminated by the kernel (i.e. killed due to an
 * exception), returns -1.  If TID is invalid or if it was not a
 * child of the calling process, or if process_wait() has already
 * been successfully called for the given TID, returns -1
 * immediately, without waiting.
 *
 * This function will be implemented in problem 2-2.  For now, it
 * does nothing. */
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. */
	/* --- Project 2: Command_line_parsing ---*/
	while (1) {
		
	}
	/* --- Project 2: Command_line_parsing ---*/
	return -1;
}
profile
Fear always springs from ignorance.

0개의 댓글