Pintos Project2 - Argument Passing

Jocy·2022년 5월 28일
0
post-thumbnail

명령어 실행 기능의 구현

일반적으로 유저 프로그램이 실행하기 전에, 커널은 레지스터에 맨 처음 function의 argument를 저장 해야한다. process_exec()은 유저가 입력한 명령어를 수행할 수 있도록, 프로그램을 메모리에 적재하고 실행하는 함수이다. Pintos는 f_name에 문자열로 이름이 저장되어있으나, 새로운 프로세스에 대한 인자 passing을 제공하지 않습니다. 이 부분을 구현하는 것이 목표이다.

/userprog/process.c -> process_exec()

int
process_exec (void *f_name) { // start_process 함수
	char *file_name = f_name; // f_name를 (void *)로 넘겨받았고, 해당 부분을 문자열로 인식하기 위해서 char * 로 변환함
	bool success;
	
    /* ----------- Project2 ----------- */
    char *fn_copy[128]; // 스택에 저장, 다른 함수에서 원본 문자열을 사용할 수 있기 때문에 복사본을 이용하여 파싱함
	memcpy(fn_copy, file_name, strlen(file_name) + 1); // 문자열에는 \0이 들어가는데 strlen에서는 \0 앞까지만 읽고 끝냄, 그래서 전체를 들고오기 위해 + 1
    /* ----------- Project2 ----------- */
    
    // 아래 intr_frame 기존에 스택에 있던 다른 쓰레기 값들이 들어 있을 수 있어서 초기화
	struct intr_frame _if; // intr_frame 내 구조체 멤버에 필요한 정보
	_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 (); // 현재 프로세스에 할당된 page directory를 지워준다.
	
    /* ----------- Project2 ----------- */
	/* And then load the binary */
	success = load (fn_copy, &_if); // file_name의 _if를 현재 프로세스에 load
    /* ----------- Project2 ----------- */
    
	/* If load failed, quit. */
	palloc_free_page (file_name);
	if (!success)
		return -1;
    
    /* ----------- Project2 ----------- */
    hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true);
	/* ----------- Project2 ----------- */

	/* Start switched process. */
	do_iret (&_if);
	NOT_REACHED ();
}

process_exec() 역할

현재 실행 중인 스레드의 context를 f_name에 해당하는 명령을 실행하기 위해
context switching하는 역할, 입력해주는 명령을 실행하기전에 실행중인 스레드가 돌고 있을 수 있다.
그래서 process_exec()에 context switching 역할이 있어야 한다. 

process_exec() 수정

  1. fn_copy 라는 복사한 문자열을 담을 배열을 만들었다.
    그 이유는 다른 함수에서 원본 문자열을 사용할 수 있기 때문에 복사본을 사용하여 파싱한 것이다.
  2. 문자열을 복사할 때 크기를 strlen(file_name) + 1과 같이 +1을 해주었다.
    문자열에는 문자열이 끝났다는 것을 표시하기 위해 \0(sentinel)이 들어가는데 strlen()함수 내부를 보면 for문을 돌면서 한 글자씩 count하다가 NULL을 만나는 순간 for문을 종료한다. 그래서 원본 문자열과 동일한 크기로 복사할 수 있도록 하기위해 + 1을 해준 것이다.
  3. 추가적으로 테스트 케이스에서 parsing이 잘되는지 보기 위해서 디버깅용 함수로 hex_dump()를 추가 했다.
  • intr_frame은 인터럽트와 같은 요청이 들어와서
    기존까지 실행 중이던 context(레지스터 값 포함)를 스택에 저장하기 위한 구조체

/userprog/process.c -> 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;
    
    /* ----------- Project2 ----------- */
    char *save_file, *token;
	char *arg_list[128];
	int cnt = 0;
    
	token = strtok_r(file_name, " ", &save_file); // 첫번째 이름
	while(token != NULL) {
		arg_list[cnt++] = token; // arg_list 배열에 인자들 추가
		token = strtok_r(NULL, " ", &save_file);
	}
    /* ----------- Project2 ----------- */
    
    ...
    
    /* Set up stack. */
	if (!setup_stack (if_))
		goto done;
        
	/* Start address. */
	if_->rip = ehdr.e_entry;
    
    /* ----------- Project2 ----------- */
    argument_stack(arg_list, cnt, if_);
    /* ----------- Project2 ----------- */
    
    success = true;
    
    ...
    

load() 역할

load()는 실행파일의 file name을 적재해서 실행하는 함수이다.
load()를 인자는 현재 process_exec()에서 입력한 커맨드(f_name의 복사본)가 file_name 인자로 넘어온다.

load() 수정

  1. 변수 token과 save_file을 추가한다. 이 변수들은 strtok_r()가 입력한 커맨드 문자열을 잘라서 사용하기 위한 변수이다. strtok_r 함수는 지정된 문자(이를 delimiters라고 한다)를 기준으로 문자열을 자른다. split()과 같은 기능이라 친숙하기도 하다.
  2. token에 첫번째 이름을 arg_list[0]에 저장하고
    추가적으로 while문과 strok_r()를 사용하여 arg_list 배열에 나머지 요소들을 저장해준다.
  3. 인자값을 스택에 올리는 함수 argument_stack()도 추가해준다

Gitbook에 나오는 예제를 통해 argument_stack이 인수를 처리하는 방법

유저 프로그램은 %rdi, %rsi, %rdx, %rcx, %r8, %r9 순서를 전달하기 위한 정수 레지스터를 사용합니다.
예를들어 argument_stack을 사용하여 /bin/ls -l foo bar 를 커맨드라인의 입력값을 넘겨준다면 아래의 Gitbook의 내용과 같이 스택에 올려주게 됩니다. 이것을 구현하는 것입니다.

/userprog/process.c -> argument_stack()

/* ----------- Project2 ----------- */
void argument_stack(char **arg_list, int cnt, struct intr_frame *if_) {
	char *arg_address[128];
	
    /* 맨 끝을 알려주기 위한 arg의 마지막 요소인 NULL 값을 제외하고 스택에 저장 */
	for (int i = cnt-1; i >= 0; i--) {
		int argv_len = strlen(arg_list[i]);
        // if_->rsp 는 현재 user_stack에서 현재 위치를 가리키는 stack pointer
		if_->rsp = if_->rsp - (argv_len + 1); // 문자열크기 + sentinel(1) 값까지 포함하여 공간만큼 rsp를 내려주고
		memcpy(if_->rsp, arg_list[i], argv_len + 1); // 인자로 받은 arg_list의 값을 하나씩 스택에 추가
		arg_address[i] = if_->rsp; // arg_address 배열에 저장한 값들의 문자열 시작 주소를 저장
	}

	// 성능을 위해 스택포인터가 8의 배수 될때까지 rsp를 1씩 내려서 나머지 영역을 padding
	while (if_->rsp % 8 != 0) {
		if_->rsp--;
		*(uint8_t *)if_->rsp = 0;
	}
    
	// arg_address[] 배열을 따로 만들어 주소값을 저장했던 것을 활용해서 주소값 자체를 삽입
	for (int i = cnt; i >= 0; i--) { 
		if_->rsp = if_->rsp - 8;
		if (i == cnt) { // arg_address에 담긴 주소의 마지막을 표시하고, 다른 프로세스가 해당 메모리를 전에 사용했을 수도 있기 때문에 0으로 초기화
			memset(if_->rsp, 0, sizeof(char **));
		} else { // stack에 arg_address의 값을 저장
			memcpy(if_->rsp, &arg_address[i], sizeof(char **));
		}
	}

	if_->rsp = if_->rsp - 8; // rsp를 fake address까지 이동시키고
	memset(if_->rsp, 0, sizeof(void *)); // return address에 0으로 초기화

	if_->R.rdi = cnt;
	if_->R.rsi = if_->rsp + 8; // arg_address의 맨처음 가리키는 주소값, fake address의 바로 위
}
/* ----------- Project2 ----------- */

argument_stack() 역할

argument_stack()에 들어가는 인자값을 스택에 올려주는 함수
위에서 parsing한 다음 한 문자씩 넣어준 배열 arg_list와 배열내에 담긴 갯수인 cnt, intr_frame을 인자로 넣는다.
이 함수 자체에서 intr_frame을 스택에 올리는 것은 아니고, 인터럽트 프레임 내 구조체 중 특정값(rsp)에 인자를 넣어주기 위함이다. 이후에 do_iret()에서 이 인터럽트 프레임을 스택에 올린다.

argument_stack() 추가

배열 argaddress[128]은 아래 for문에서 스택에 담을텍스트 각 인자의 주소값을 저장하는 배열
for문을 돌면서 process_exec()에서 넣어주는 arg_list로부터 값을 하나씩 꺼내서 if
->rsp에 삽입,
이때 if_->rspuser stack에서 현재 위치를 가리키는 스택 포인터이자 intr_frame 내의 멤버이다.
각 인자에서 인자 크기(argv_len)을 읽는데 이때 각 인자마다는 문자열이라 끝에 \0(sentinel)이 포함되어 있다. strlen은 sentinel을 읽지 않아서 +1을 추가해줘야 한다.

  • 첫번째 for문이 동작하는 방식
    1. 먼저 스택 포인터를 넣어줄 공간만큼 쭉 내린다.(if->rsp = if->rsp - (argvlen +1)
    2. 그다음, 해당 공간에 인자값을 복붙한다(memcpy(if
    ->rsp, argv[i], argvlen+1))
    3. arg_address 배열에 인자값이 위치한 주소를 저장한다. (arg_address[i] = if
    ->rsp)

  • while문을 돌면서 패딩을 만들어준다
    8배수 정렬이 되면 정렬되지 않은 것보다 속도가 빠르기 때문에 최상의 성능을 위해 스택 포인터를 첫 번째 푸시 전에 8의 배수로 반올림 해줍니다.
    성능을 위해 스택포인터가 8의 배수 될때까지 rsp를 1씩 내려서 나머지 영역을 padding 해준다.

  • 두번째 for문이 동작하는 방식
    위에 arg_address[] 배열을 따로 만들어 주소값을 저장했던 것을 활용해서 주소값 자체를 삽입
    64bit의 주소값8byte이기 때문에 rsp - 8 하여 그 공간에 값을 넣어준다.
    for문에서는(int i = cnt-1)로 선언된 반면 여기 for문에서는 (int i = cnt)로 선언된다.
    이는 앞서 process_exec()에서 while문을 돌며 token을 받아오는 것을 보면,
    맨 마지막 인자값으로 NULL을 받아 arg_list에 저장하게 된다. 따라서 argv[-1]= \0이 된다.
    근데 이 인자 값을 스택에 저장할 때는 맨 끝에 있는 NULL 값을 저장하지 않은 반면
    여기서는 NULL값 가리키는 포인터를 저장한다.
    처음 if문을 통과해서 arg_address에 담긴 주소의 마지막을 표시하고, 다른 프로세스가 해당 메모리를 전에 사용했을 수도 있기 때문에 0으로 초기화해주고
    두번째부터는 인자값에 대한 주소를 스택포인터가 낮은 순서대로 차례대로 저장해준다

  • for문을 돌고 나면  fake address를 넣어준다.
    rsp를 fake address까지 이동시키고 메모리는 0으로 초기화 해준다.

  • 마지막으로 intr_frame if_의 멤버로 있는 레지스터 구조체rdi에 인자 count값인 cnt를 넣고 rsi에는 rsp + 8byte 크기만큼 스택포인터를 증가시켜 arg_address맨 앞의 주소값을 넣는다.


/userprog/process.c -> 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. */
     /* ----------- Project2 ----------- */
	while (1){} // 자식 프로세스 종료될 때까지 무한 대기
    /* ----------- Project2 ----------- */
	return -1;
}

process_wait() 수정

프로세스 생성 함수를 자세히 뜯어보면 process_wait종료 구현이 안 되어있는 부분이 있다. Hint라고 친절하게 적어뒀는데 이 부분을 수정하지 않아서 테스트 케이스를 통과하는데 꽤나 애를 먹었다. 핀토스는 유저 프로세스를 생성한 후 프로세스 종료를 대기해야 하는데 자식 프로세스가 종료될 때까지 무한 대기한다. 그래서 while (1){} 추가가 필요하다.


include/userprog/process.h에 선언 해야되는 함수

#ifndef USERPROG_PROCESS_H
#define USERPROG_PROCESS_H

...

void process_exit (void);
void process_activate (struct thread *next);
/* ----------- Project2 ----------- */
void argument_stack(char **arg_list, int cnt, struct intr_frame *if_);
/* ----------- Project2 ----------- */
#endif /* userprog/process.h */

process.c에서 사용하기 위해 추가되는 함수를 헤더에 선언


❗️테스트 케이스를 쉽게하기 위해 추가할 것(opt)

Makefile.build

  • 우리가 make를 쳤을 때 해당 폴더에 build라는 파일이 생김
  • build 디렉터리 안에 Makefile이 생기는데 거기에 생기는 Makefile 내용이 담긴 곳
all: os.dsk

disk:
	pintos-mkdisk filesys.dsk 10
	pintos --fs-disk filesys.dsk -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'

include ../../Make.config
include ../Make.vars
include ../../tests/Make.tests

userprog/Makefile

$(Make) -C build disk: 이건 현재 위치에서 build 디렉터리로 이동해서 Makefile로 이동 disk명령을 수행하라

include ../Makefile.kernel

disk:
	$(MAKE) -C build disk

실행방법

/userprog 의 경로에서 아래의 순서로 실행합니다.
1. make
2. make disk

결과

system call을 호출하면서 무한대기

profile
Software Engineer

2개의 댓글

comment-user-thumbnail
2022년 5월 28일

정리 잘 하셨네요!!
덕분에 큰 도움이 되었습니다 :)

1개의 답글