거북이의 쉼터

(2021.11.02) Argument Passing 가이드라인 본문

코딩 삽질/KAIST PINTOS (CS330)

(2021.11.02) Argument Passing 가이드라인

onlim 2021. 11. 2. 15:04

이전에 진행할 때, 핀토스는 x86 방식으로 구현이 되어있어, calling convention 또한 x86 방식을 따라서 구현했었다. 그러나 대대적 개편 이후 핀토스는 이제 x86-64 방식을 채택하게 되었다. 매뉴얼을 보면 이제 calling convention 또한 그에 맞춰서 되어 있는 것을 확인할 수 있다. x86-64 방식의 calling convention을 개략적으로 설명하자면 argument가 6개 이하일 때는 rdi, rsi, rdx, rcx, r8, r9 순서로 레지스터에 넣고 함수를 호출한다. 함수가 반환될 때, 리턴 값이 있을 경우, rax 레지스터에 값을 담아 리턴한다.

 

이런 상황에서 우리가 구현해야 할 것은 함수가 호출될 때 argument를 받아 레지스터와 스택을 구성하도록 하는 것이다. 정확히는 process_exec에서 실행되어야 하는 명령줄을 받을 때, 실행되는 프로그램과 인수를 분리해서 실행하도록 하는 것이다. 따라서 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 ();
}

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

	/* 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", file_name);
		goto done;
	}

	/* Read program headers. */
	file_ofs = ehdr.e_phoff;
	for (i = 0; i < ehdr.e_phnum; i++) {
		struct Phdr phdr;

		if (file_ofs < 0 || file_ofs > file_length (file))
			goto done;
		file_seek (file, file_ofs);

		if (file_read (file, &phdr, sizeof phdr) != sizeof phdr)
			goto done;
		file_ofs += sizeof phdr;
		switch (phdr.p_type) {
			case PT_NULL:
			case PT_NOTE:
			case PT_PHDR:
			case PT_STACK:
			default:
				/* Ignore this segment. */
				break;
			case PT_DYNAMIC:
			case PT_INTERP:
			case PT_SHLIB:
				goto done;
			case PT_LOAD:
				if (validate_segment (&phdr, file)) {
					bool writable = (phdr.p_flags & PF_W) != 0;
					uint64_t file_page = phdr.p_offset & ~PGMASK;
					uint64_t mem_page = phdr.p_vaddr & ~PGMASK;
					uint64_t page_offset = phdr.p_vaddr & PGMASK;
					uint32_t read_bytes, zero_bytes;
					if (phdr.p_filesz > 0) {
						/* Normal segment.
						 * Read initial part from disk and zero the rest. */
						read_bytes = page_offset + phdr.p_filesz;
						zero_bytes = (ROUND_UP (page_offset + phdr.p_memsz, PGSIZE)
								- read_bytes);
					} else {
						/* Entirely zero.
						 * Don't read anything from disk. */
						read_bytes = 0;
						zero_bytes = ROUND_UP (page_offset + phdr.p_memsz, PGSIZE);
					}
					if (!load_segment (file, file_page, (void *) mem_page,
								read_bytes, zero_bytes, writable))
						goto done;
				}
				else
					goto done;
				break;
		}
	}

	/* 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). */

	success = true;

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

와... 길다. 일단 해석하는 것이 우선되어야 할 것 같다. 그래서 이번 가이드라인에서는 해당 코드를 이해하는 것을 목적으로 두도록 하자.

 

우선 주석에 나와 있듯, load() 함수는 실행할 프로그램의 binary 파일을 메모리에 올리는 역할을 한다.

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

각 프로세스가 실행이 될 때, 각 프로세스에 해당하는 VM(virtual memory)이 만들어져야 하므로, 이를 위해 페이지 테이블 엔트리를 생성하는 과정이 우선된다. 

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

그 뒤, 파일을 실제로 VM에 올리는 과정이 진행된다. 파일이 제대로 된 ELF 인지 검사하는 과정이 동반되며, 세그먼트 단위로 PT_LOAD의 헤더 타입을 가진 부분을 하나씩 메모리로 올리는 작업을 진행한다.

case PT_LOAD:
	if (validate_segment (&phdr, file)) {
		bool writable = (phdr.p_flags & PF_W) != 0;
		uint64_t file_page = phdr.p_offset & ~PGMASK;
		uint64_t mem_page = phdr.p_vaddr & ~PGMASK;
		uint64_t page_offset = phdr.p_vaddr & PGMASK;
		uint32_t read_bytes, zero_bytes;
		if (phdr.p_filesz > 0) {
			/* Normal segment.
			 * Read initial part from disk and zero the rest. */
			read_bytes = page_offset + phdr.p_filesz;
			zero_bytes = (ROUND_UP (page_offset + phdr.p_memsz, PGSIZE)
					- read_bytes);
		} else {
			/* Entirely zero.
			 * Don't read anything from disk. */
			read_bytes = 0;
			zero_bytes = ROUND_UP (page_offset + phdr.p_memsz, PGSIZE);
		}
		if (!load_segment (file, file_page, (void *) mem_page,
					read_bytes, zero_bytes, writable))
			goto done;
	}
	else
		goto done;
	break;

전부 메모리로 올린 뒤에 스택을 만드는 과정이 실행되고,

/* Set up stack. */
if (!setup_stack (if_))
	goto done;

어떤 명령부터 실행되는지를 가리키는, 즉 entry point 역할의 rip를 설정하고, 열었던 실행 파일을 닫는 것으로 load()가 끝난다.

	/* Start address. */
	if_->rip = ehdr.e_entry;

	/* TODO: Your code goes here.
	 * TODO: Implement argument passing (see project2/argument_passing.html). */

	success = true;

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

load()가 성공적으로 끝난 뒤에는 바로 해당 프로세스를 실행하도록 리턴하는 것으로, process_exec()도 끝이 난다. 여기까지 process_exec()의 실행 과정을 개략적으로 살펴보았다.

 

코드를 본 뒤에 든 생각은 일단 스택이 만들어진 다음에 스택의 내용물을 채워넣어야 하기 때문에, 우리가 구현할 부분은 setup_stack() 이후에 들어간다는 것이다. 따라서 들어온 입력을 파싱해 스택에 채워넣는 루틴은 주석이 작성된 위치에 넣으면 적합할 것이다.

 

문제는 만약 입력으로 들어오는 명령이 argument를 포함하고 있다면 load()의 코드에서 filename이 이제 진정한 파일의 이름이 아닐 수 있다는 점이다. 따라서 순수한 실행 파일의 이름을 얻어내기 위해서, 파싱 자체는 load() 함수의 시작 즈음에 이루어져야 한다. 파싱을 위해서 매뉴얼에서는 strtok_r() 함수를 사용할 것을 권장하고 있기에, 이를 사용할 것이다. 마지막으로 사소하지만 중요한 사항은, x86-64이기에 포인터 변수의 크기가 8 바이트씩이라는 것이다.

 

여기까지 살펴본 내용들을 정리하면 다음과 같다.

 

  • load() 함수 초반에 파싱을 통해 제대로 된 파일 이름을 구하도록 정정
  • load() 함수 setup_stack() 이후 파싱 및 스택 구성

이제 다음 포스팅에서 실제 코딩을 해보도록 하자.

Comments