과제목표
process_exec() 내에 사용자 프로그램을 위한 인자를 셋업해라.
유저 프로그램을 실행하기 전에, 커널은 레지스터에다가 맨 처음 function의 argument를 저장해야 한다. process_exec()은 유저가 입력한 명령어를 수행할 수 있도록 프로그램(=process)을 메모리에 적재하고 실행하는 함수이다. 해당 프로그램은 f_name에 문자열로 저장되어 있으나 현재 상태에서 process_exec() 은 새로운 프로세스에 대한 인자 passing을 제공하지 않는다. 이 기능을 구현하는 것이 이번 과제이다. process_exec() 에 코드를 추가해서 간단히 프로그램 파일 이름을 인자로 넣는것 대신에, space가 올 때마다 단어를 parsing하도록 만들어야 한다. 이때, 첫 번째 단어는 프로그램 이름이고 두세 번째 단어는 각각 첫 번째, 두 번째 인자이다.
명령어 라인과 함께, 여러 개 space는 하나의 space와 동일하게 취급해야 한다. 이때 명령어 인자의 길이에 제한을 둘 수 있다.
구현
process_create_initd() 수정
원본 코드
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE);
/* Create a new thread to execute FILE_NAME. */
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page (fn_copy);
return tid;
}
현재 process_create_initd()
함수는 커맨드 라인의 첫 번째 토큰을 thread_create()
함수의 첫 인자로 전달 되도록 프로그램을 수정해야 한다. 현재는 커맨드 라인 전체가 thread_create()
에 전달되고 있다.
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE);
/* Create a new thread to execute FILE_NAME. */
char *save_ptr;
strtok_r(file_name, " ", &save_ptr);
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page (fn_copy);
return tid;
}
strtok_r()
: strtok_r 함수는 지정된 문자(이를 delimiters라고 한다)를 기준으로 문자열을 자른다.
예를 들어 위 코드에서 token = strtok_r(file_name, " ", &save_ptr);이라고 하면 file_name의 가장 첫번째 문자열이 나온다. 긴 말 할 것 없이 아래 예시를 보자. "The little prince"를 공백을 기준으로 잘라서 출력한다고 하자. 그러면 출력값은 "The", "Little", "Prince"일 것이다. strtok_r은 한글자씩 이동하다가(T, h, e, ...) 공백을 만나면 그곳에 NULL 값을 넣은 다음, 그 앞까지의 문자열(=The)을 반환한다. 여기서 &the_last는 그 뒤의 문자열("\nLittie Prince\n")에서 가장 첫번째 문자의 주소값을 나타낸다. 이렇게 한 글자씩 잘라서 반환하는 개념이기에 while문을 돌리면서 한글자씩 뽑아내는 것으로 이해하면 된다.
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;
/* 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;
}
•••
원래 코드 이후에는 load()를 실행하는 코드가 나온다. load()함수는 실행파일의 file_name을 적재해 실행하는 함수이다. load()를 부른 caller인 process_exec()에서 입력한 커맨드 전체가 file_name인자로 넘어온다.
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 ());
char *token, *save_ptr;
char *argv[64];
uint64_t cnt = 0;
for (token = strtok_r(file_name, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr)) {
argv[cnt++] = token;
}
/* Open executable file. */
file = filesys_open (argv[0]);
if (file == NULL) {
printf ("load: %s: open failed\n", file_name);
goto done;
}
•••
for 문을 보면 매개변수로 받은 인자 file_name을 NULL을 기준으로 문자열을 잘라서 argv 배열에 하나씩 넣어준다.
•••
/* 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). */
argument_stack(argv, cnt, &if_->rsp);
if_->R.rdi = cnt;
if_->R.rsi = if_->rsp + 8;
success = true;
// * 추가
t->running_file = file;
file_deny_write(file);
done:
/* We arrive here whether the load is successful or not. */
return success;
}
다음은 인자값을 스택에 올리는 함수 argument_stack()이 필요하다. 위에서 parsing한 다음 한 문자씩 넣어준 배열 arg_list와 count값인 token_count, 그리고 인터럽트 프레임도 인자로 넣는다. 이 함수 자체에서 인터럽트 프레임을 스택에 올리는 것은 아니고, 인터럽트 프레임 내 구조체 중 특정값(rsp)에 인자를 넣어주기 위함이다. 이후에 do_iret()에서 이 인터럽트 프레임을 스택에 올린다.
argument_stack() 추가
argument_stack()
함수는 유저 스택에 프로그램 이름과 인자들을 저장하는 함수이다.
parse : 프로그램 이름과 인자가 저장되어 있는 메모리 공간, count : 인자의 개수, esp : 스택 포인터를 가리키는 주소
void argument_stack(char **parse, int count, void **esp) {
char *argv_address[count];
uint8_t size = 0;
// * argv[i] 문자열
for (int i = count - 1; -1 < i; i--) {
*esp -= (strlen(parse[i]) + 1);
memcpy(*esp, parse[i], strlen(parse[i]) + 1);
size += strlen(parse[i]) + 1;
argv_address[i] = *esp;
}
if (size % 8) {
for (int i = (8 - (size % 8)); 0 < i; i--) {
*esp -= 1;
**(char **)esp = 0;
}
}
*esp -= 8;
**(char **)esp = 0;
// * argv[i] 주소
for (int i = count - 1; -1 < i; i--) {
*esp = *esp - 8;
memcpy(*esp, &argv_address[i], strlen(&argv_address[i]));
}
// * return address(fake)
*esp = *esp - 8;
**(char **)esp = 0;
}
argumentstack() 코드를 뜯어보자. 배열 arg_address[128]은 아래 for문에서 스택에 담을 각 인자의 주소값을 저장하는 배열이다. 이후 for문을 돌면서 process_exec()에서 넣어주는 arg_list로부터 값을 하나하나씩 꺼내서 if->rsp에 하나씩 넣어준다. 이때 if_->rsp는 user stack에서 현재 위치를 가리키는 스택 포인터이자 인터럽트 프레임 내 멤버이다. 여기서 작업이 Gitbook에 나오는 테이블에 값을 채워넣는 것과 같다.
이때, 각 인자에서 인자 크기(argv_len)을 읽는데 이때 각 인자마다는 실제로 sentinel(\n)이 포함되어 있는데 여기서 역시 strlen은 sentinel을 읽지 않으니 +1을 해주는 것이다. for문 순서를 자세히 보면
이후에는 while문을 돌면서 패딩을 삽입한다.
이번에는 주소값 자체를 삽입한다. 이래서 위에 arg_address[] 배열을 따로 만들어 여기에 주소값을 저장한 이유이다. 똑같이 for문들 돌면서 넣는데, 처음 for문에서는(int i = argc-1)로 선언된 반면 여기 for문에서는 (int i = argc)로 선언된다. 이는 앞서 process_exec()에서 while문을 돌며 token을 받아오는 것을 보면, 맨 마지막 인자값으로 NULL을 받아 arg_list에 저장하게 된다. 따라서 argv[-1]= '\n'이 된다. 근데 이 인자 값을 스택에 저장할 때는 맨 끝에 있는 NULL 값을 저장하지 않은 반면 여기서는 NULL값 가리키는 포인터를 저장한다.
for문을 돌고 나면 fake address를 넣어준다. 해당 영역의 메모리는 0으로 초기화해준다.
마지막으로 인터럽트 프레임 if_의 멤버로 있는 레지스터 구조체의 rdi에 인자 count값인 argc, 그리고 rsi에는 fake address바로 위인 arg_address의 맨 앞을 가리키는 주소값을 넣는다.
Interrupt Frame
인터럽트 프레임 (struct Intr_frame)
인터럽트 프레임은 인터럽트가 들어왔을 때, 이전에 레지스터에 작업하던 context를 switching하기 위해 이 정보를 담아놓는 구조체이다. 그래서 구조체 intr_frame에 가보면 엄청 복잡하게 나와 있고, 그안에 멤버 구조체 gp_registers R을 들고 있다. 이 R은 기존 스레드가 작업하고 있을 때의 레지스터 값을 인터럽트가 들어오면 switching하기 위해 이 구조체에다가 정보를 담는다. 그래서 1주차에 do_schedule() 을 보면do_iret()이 나오고, 이 do_iret()는 어셈블리어로 되어 있는데, 여기가 기존까지 작업했던 context를 intr_frame에 담는 과정이라고 보면 되겠다.
즉, 인터럽트 프레임은 인터럽트와 같은 요청이 들어와서 기존까지 실행 중이던 context(레지스터 값 포함)를 스택에 저장하기 위한 구조체이다.
결과
결과(중간 결과)를 확인하기 위해서는 process_exec()
함수에 hex_dump()
함수를 추가해줘야 한다.
hex_dump(_if.rsp, _if.rsp, KERN_BASE - _if.rsp, true);
process_exec()
함수 중간에 load가 끝난 후 다음 위에 함수를 추가해준다.
또한 process_wait()
함수에 무한루프를 추가해줘야 결과를 제대로 볼 수 있다.
while (1){}
다음 구문을 함수 내 return 전에 추가해준다. 밑에 사진은 현재 pintos의 상태를 나타내는 사진이다.
결과
PintOS Project2 GIthub 주소 PintOS
안녕하세요. 현재 프로젝트 2를 구현 중인 학생입니다. 테스트 케이스를 돌릴 때 hex_dump의 출력은 잘 되는데 그 뒤에 interrupt 0x0d..이런 식으로 exception이 뜨고 프로그램이 오류를 냅니다. 프로젝트 1의 모든 테스트 케이스는 통과한 상태이며, 혹시 어떤 방법으로 이 오류를 접근해야 하는지 도움을 주실 수 있겠습니까?