본문 바로가기
정글/pintos

[Project2] User Program(3) - Argument Passing

by 위대한초밥V 2023. 6. 14.

https://github.com/Blue-club/pintos_5/issues/16

 

[Project 2-1] Passing the argument and creating a thread(mywnajsldkf) · Issue #16 · Blue-club/pintos_5

 

github.com


1. Argument Passing

process_exec() 함수는 User program에서 입력한 명령을 수행할 수 있는 프로그램(=process)을 메모리에 적재하고 실행한다. 현재 문자열은 명령어에 대한 인자 passing이 제공되지 않은 상태이므로, 이를 구현해야 한다.

즉, 문장을 공백 기준으로 나누어 단어를 파싱하는 것이다.

예를 들어 bin/ls -l foo bar 라는 문장이 들어왔다면 첫번째 단어(bin/ls)는 프로그램 이름이고, l, foo, bar는 각각 인자가 된다.

 

x86-64 Calling Convention

Calling convention이란 서브루틴이나 함수가 호출자로부터 매개 변수를 받는 방법이나 결과를 반환하는 방법에 대해 다루는 것을 말한다. 우리가 코딩할 때, 따르는 camel case나 snake case 등도 이러한 컨벤션이다.

x86-64 컨벤션에서 정의하는 내용은 다음과 같다.

  1. User level application은 다음 시퀀스를 정리하기 위해 다음 정수 레지스터로 사용한다.
    %rdi, %rsi, %rdx, %rcx, %r8 and %r9
  2. 만약 그 이상의 인자가 들어오면 스택으로 들어가서, callee(호출 당함)의 첫번째 instruction으로 이동한다.
  3. callee가 실행한다.
  4. callee의 반환값이 있다면 RAX 레지스터에 저장된다.
  5. callee는 stack에서 반환 주소를 꺼내고, x86-64 RET 명령으로 지정된 위치로 이동한다.

예를 들어, f()가 3개의 정수형 인자를 받는다면 f(1, 2, 3)이 된다.

Program Startup Details

User program 진입점은 lib/user/entry.c의 _start() 에서부터 시작된다.

void
_start (int argc, char *argv[]) {
    exit (main (argc, argv));
}

커널은 사용자 프로그램 실행 전에 초기 함수에 대한 인수를 레지스터에 넣어준다.

/bin/ls -l foo bar 라는 커맨드를 어떻게 다루는지 살펴보자.

  1. 커맨드를 단어로 분리한다.
    /bin/ls, -l, foo, bar
  2. 단어들을 스택의 최상단에 올린다. pointer로 참조되기 때문에 순서는 중요하지 않다.
  3. 각 문자열의 주소와 null 포인터 센티널(반복 탐색을 끝내는 값) 스택의 오른쪽에서 왼쪽으로 넣는다.
    • 1번에서 분리한 /bin/ls, -l, foo, bar 이 스택에 들어간다.
    • null pointer sentinel은 argv[argc]가 널 포인터가 된다.
      • 이 부분은 C언어의 표준적인 관례(ISO/IEC 9899:1999)이다. 표준에 따르면 argc 값이 1보다 작을 때는 argv[0] 만 존재하고 argc 값이 1보다 크거나 같을 때 argv[0] 부터 argv[argc-1] 까지 인수가 존재한다. 이때는 null 포인터로 설정되어야 한다.
    • argv[0]은 가장 낮은 가상 주소에 있도록 보장한다.
    • word는 정렬 액세스가 정렬되지 않은 액세스보다 빠르기 때문에 스택 포인터를 8의 배수를 더해서 8의 배수가 되도록한다. (malloc을 떠올리시오!)
  4. %rsi가 argv(argv[0]의 주소0)를 가리키도록 하고, %rdi는 argc를 가리키도록 한다.
  5. 가짜 return address를 넣어준다. entry function을 절대 반환될 수 없기 때문에, stack frame은 다른 함수와 동일한 구조를 가져가야 한다. 즉, 가짜 주소는 entry function의 스택 프레임이 다른 함수의 스택 프레임과 형태를 갖도록 추가한 요소이다.

 

그럼 /bin/ls -l foo bar stack에 어떻게 담기는지 살펴보자.

다음 표는 사용자 프로그램이 시작되기 직전의 스택 상태와 관련 레지스터를 보여준다.

stack pointer는 0x4747ffb8에서 초기화되었고, USER_STACK의 초기값은 include/threads/vaddr.h에 담겨있다.

userprog/process.c의 setup_stack 함수를 확인해보면, USER_STACK 주소에서 0으로 채워진 최소 스택을 만드는 것을 알 수 있다.

/* Create a minimal stack by mapping a zeroed page at the USER_STACK */
static bool
setup_stack (struct intr_frame *if_) {
	uint8_t *kpage;
	bool success = false;

	kpage = palloc_get_page (PAL_USER | PAL_ZERO);
	if (kpage != NULL) {
		success = install_page (((uint8_t *) USER_STACK) - PGSIZE, kpage, true);
		if (success)
			if_->rsp = USER_STACK;
		else
			palloc_free_page (kpage);
	}
	return success;
}

더불어 setup_stack 함수는 process의 load 함수에서 실행된다.


2. 과제 구현

위에서 참고한 내용을 바탕으로 구현을 시작한다.

process_exec(userprog/process.c)

process_exec 함수는 file_name과 _if를 현재 프로세스에 로드한다.

 

자세한 내용은 이전 포스팅 참고!✔️

[Project2] User Program(2) - Passing the argument and creating a thread: process_exec 함수의 뿌리를 찾아서...

 

[Project2] User Program(2) - Passing the argument and creating a thread: process_exec 함수의 뿌리를 찾아서...

process_exec (void *f_name)는 현재 실행 컨텍스트를 f_name으로 바꾸는 함수이다. 이 과제에서 우리는 process_exec에서 호출하는 file을 로드하는 load() 함수에서 f_name을 통해 들어온 명령어를 잘 파싱하여

mywnajsldkf.tistory.com

 

userprog/process.c

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
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 */
	// 현재 프로세스에 할당된 page directory를 지운다.
	process_cleanup ();

	/* And then load the binary */
	// load() 함수에서 file_nmae을 명령과 인자로 parsing하는 작업을 진행했다.
	success = load (file_name, &_if);

	/* If load failed, quit. */
	// load에서 page allocation한 것을 free해준다.
	palloc_free_page (file_name);
	if (!success)
		return -1;
	
	/* Start switched process. */
	// 생성된 프로세스로 context switching한다.
	do_iret (&_if);
	NOT_REACHED ();
}

 

struct intr_frame(userprog/process.c)

Interrupt Frame은 인터럽트와 같은 요청이 들어왔을 때, 기존에 실행 중이던 context(레지스터 값 포함)를 스택에 저장하기 위한 구조체를 말한다.

구조체를 살펴보면, 멤버 구조체인 gp_Register R과 여러 정수형 인자들을 갖는다.

process_exec 함수는 load후 page allocation을 free한 이후에 do_iret 함수로 생성된 프로세스를 context switching하는데, interrupt frame과 do_iret 함수에 자세히 알고 싶다면 다음 글을 추천한다.

[OS] pintos의 interrupt frame 이란 + do_iret 함수가 하는 일

 

[OS] pintos의 interrupt frame 이란 + do_iret 함수가 하는 일

인터럽트 프레임 및 do_iret 함수 인터럽트 프레임이란 무엇인지, 그리고 do_iret 에서는 어떤 일을 하는 건지 이제서야 조금 알 것 같아 별도로 글을 작성한다. struct intr_frame do_iret을 이해하려면 인

stay-present.tistory.com

 

load(userprog/process.c)

load 함수에서는 file_name을 intr_frame에 parsing한다.

 

userprog/process.c

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;

	/* Project2: Command Line Parsing */
	// file_name이 const 이므로 변경할 수 없기 때문에, 복사해준다.
	char *copy_filename;
	copy_filename = palloc_get_page (0);
	// 생성한 copy_filename 문자열에 file_name을 복제해준다.
	strlcpy (copy_filename, file_name, MAXIMUM_NUMBER);

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

	// command line parsing
	char *argv[MAXIMUM_NUMBER/2];
	char *token, *save_ptr;
	int argc = 0;
	
	// 첫번째 이름을 받아온다.
	// save_ptr은 앞 문자열을 자르고 남은 문자열의 가장 앞을 가리키는 포인터 주소값을 말한다.
	token = strtok_r(copy_filename, BLANK_DELIMETER, &save_ptr);
	argv[argc] = token;		// 첫번째 인자가 저장된다.(ex. args-single onearg라면, args-single 저장)
	
	// 공백을 기준으로 문자열을 잘라서 argv 배열에 저장한다.
	while (token != NULL){
		token = strtok_r(NULL, BLANK_DELIMETER, &save_ptr);
		argc++;
		argv[argc] = token;
	}

	copy_filename = argv[0];

	...
}

문자열을 한글자씩 이동하다가 공백을 만나면 NULL을 넣은 다음 그 앞까지의 문자열을 반환하는 strtok_r 함수를 사용한다. load 함수는 file name을 const 형식으로 받아오기 때문에, NULL을 추가할 수 없다. 따라서 문자열을 자르기 전에 file name을 복제하였다.

 

strtok와 strtok_r 차이는 strtok는 한글자를 자르고 뒤에 남은 문자열을 지역변수를 저장하고, strtok_r은 전역 변수를 선언하고 저장하는 방식이다. 두 함수의 차이는 다음 글에 자세히 나와있다.

 

C 언어 코딩 도장: 45.1 문자를 기준으로 문자열 자르기

 

C 언어 코딩 도장: 45.1 문자를 기준으로 문자열 자르기

45 문자열 자르기 지금까지 문자열을 복사하거나 붙이는 방법을 알아보았습니다. 이번에는 주어진 문자열을 자르는 방법을 알아보겠습니다. 참고로 문자열 자르기는 포인터를 이용하는 방식이

dojang.io

이제 argv 리스트에 공백 기준으로 잘린 값들이 저장되었다.

다음은 인자값들을 interrupt_frame 구조체에 저장한다. 이 과정은 구조체 특정값에 인자를 넣어주는 것이고, 이후 do_iret()에서 인터럽트 프레임을 스택에 올릴 것이다.

 

argument_stack(userprog/process.c)

userprog/process.c

static bool
load (const char *file_name, struct intr_frame *if_) {
	...
	/* TODO: Your code goes here.
	 * TODO: Implement argument passing (see project2/argument_passing.html). */
	// argv: list, argc: token_count, if_: interrupt_frame
	argument_stack(argv, argc, if_);	// 👉 작성해야하는 함수!
	success = true;

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

userprog/process.c

void 
argument_stack(char **argv, int argc, struct intr_frame *if_)
{
	// file_name 파싱 결과를 담을 배열을 생성한다.
	char *arg_address[MAXIMUM_NUMBER];

	// word-align 전까지
	// argv 뒤에서부터 인자들을 userstack에 저장한다.(NULL 끝 값은 제외한다.)
	// /bin/ls -l foo bar
	for (int i = argc-1; i >= 0; i--)
	{
		int arg_len = strlen(argv[i]) + 1;	// 1은 sentinel(\\0)을 의미한다.
		if_->rsp = if_->rsp - (arg_len);	  // 인자 크기만큼 스택 포인터를 이동시킨다.
		memcpy(if_->rsp, argv[i], arg_len); // 해당 공간이 인자를 복사 붙여넣기 한다.
		arg_address[i] = if_->rsp;			// arg_address 배열에 현재 문자열 시작 위치를 저장한다.
	}
	
	// word-algin -> 정렬하기 위해
	while (if_->rsp % 8 != 0)
	{
		if_->rsp--;
		*(uint8_t *) if_->rsp = 0;	// 포인터 타입이 unit8_t이니까 rsp 데이터에 0을 넣는다.-> 패딩을 채운다. 
	}

	// 주소값을 삽입한다.
	for (int i = argc; i >= 0; i--)
	{
		if_->rsp = if_->rsp - 8;
		if (i == argc)	// null sentinel 자리
		{
			*(char **) if_->rsp = 0;
		}
		else
			memcpy(if_->rsp, &arg_address[i], sizeof(char *));
	}
	
	// rdi, rsi 
	if_->R.rdi = argc;
	if_->R.rsi = if_->rsp;

	// fake address를 저장한다.
	if_->rsp = if_->rsp - 8;
	memset(if_->rsp, 0, sizeof(char *));
}

 

기타 함수 수정

테스트를 위해 함수를 일부 수정한다.

 

hex_dump() : 디버깅 툴

parsing 결과를 확인하기 위해 디버깅 툴을 추가한다.

userprog/process.c

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
// 사용자가 입력한 명령을 수행하도록 프로그램을 메모리에 올려 실행한다.
// 실행 프로그램 파일과 옵션을 구분하는 작업을 추가한다.
int
process_exec (void *f_name) {
	...

	/* Project2: Command Line Parsing 디버깅 */
	hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true);
	
	...
}

 

process_wait() : 반복문으로 대기

무한루프를 돌아야한다고 힌트에 작성되어있지만, 적당히 시간을 두어서 바로 종료되지 않도록 하였다.

+) 이 부분은 system call에서 구현할 것이다.

userprog/process.c

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++)
	{
		/* code */
	}
	
	return -1;
}

마지막으로 헤더 파일이 잘 선언되었는지 확인한다.

 

과제 테스트

userprog 경로에서 make 실행 후, 생성된 build 폴더에서 다음 명령을 실행하면 된다.

pintos -v -k -T 60 -m 20   --fs-disk=10 -p tests/userprog/args-single:args-single -- -q   -f run 'args-single onearg'

실행 결과는 다음과 같다.

 

Passing 기능을 테스트할 수 있는 함수를 더 알아보고 싶다면, issue에서 확인하면 된다.

[Project 2-1] Passing the argument and creating a thread(mywnajsldkf) · Issue #16 · Blue-club/pintos_5

 

[Project 2-1] Passing the argument and creating a thread(mywnajsldkf) · Issue #16 · Blue-club/pintos_5

 

github.com

 

반응형