int main (void)
{
uint64_t mem_end;
char **argv;
bss_init ();
/* Break command line into arguments and parse options. */
argv = read_command_line ();
argv = parse_options (argv);
:
:
printf ("Boot complete.\n");
:
/* Run actions specified on kernel command line. */
run_actions (argv);
:
/* Finish up. */
if (power_off_when_done)
power_off ();
thread_exit ();
}
pintos 시작 시 가장 먼저 실행되는 /threads/init.c 파일의 main 함수이다. 이곳에서 read_command_line()과 parse_options(argv)를 통해 명령어를 읽고 parsing한다.
이렇게 읽어 온 명령어는 run_actions() 함수로 전달되고
argv[] 안의 'run'은 run_task를 실행시킨다.
예를 들어 전달된 argv가 "~ run 'args-multiple arg1 hey2 hw3 til4'"라면 run이 run_task를 실행시키고, 'args-multiple arg1 hey2 hw3 til4'가 process_create_initd의 인자로 들어가게 된다.
tid_t process_create_initd (const char *file_name)
{
char *fn_copy;
:
:
strlcpy (fn_copy, file_name, PGSIZE);
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
:
:
return tid;
}
그리고 process_create_initd 함수에서 initd 함수를 실행하고 initd는 process_exec을 호출하게 된다. pintos-kaist의 설명서를 보면,
Currently, process_exec() does not support passing arguments to new processes. Implement this functionality, by extending process_exec() so that instead of simply taking a program file name as its argument, it divides it into words at spaces. The first word is the program name, the second word is the first argument, and so on. That is, process_exec("grep foo bar") should run grep passing two arguments foo and bar.
현재 핀토스는 새로운 프로세스에 인자를 전달하는 것을 지원하지 않는다고 한다. 이 함수를 확장하여, 예를 들어 'args-multiple arg1 hey2 hw3 til4'가 들어오면 공백을 기준으로 첫번째 단어인 'args-multiple'은 프로그램 이름, 두번째 단어인 'arg1'은 첫번째 인자, 'hey2'는 두번째 인자 이런식으로 나누어야 한다. 적당한 반복문과 strtok 함수를 이용하여 구현했다.
다시 설명서를 보면, 이렇게 나눈 인자들을 어떻게 해야 하는지 나와있다.
Consider how to handle arguments for the following example command:
/bin/ls -l foo bar
.
- Break the command into words:
/bin/ls
,l
,foo
,bar
.- Place the words at the top of the stack. Order doesn't matter, because they will be referenced through pointers.
- Push the address of each string plus a null pointer sentinel, on the stack, in right-to-left order. These are the elements of argv. The null pointer sentinel ensures that
argv[argc]
is a null pointer, as required by the C standard. The order ensures thatargv[0]
is at the lowest virtual address. Word-aligned accesses are faster than unaligned accesses, so for best performance round the stack pointer down to a multiple of 8 before the first push.- Point
%rsi
toargv
(the address ofargv[0]
) and set%rdi
toargc
.- Finally, push a fake "return address": although the entry function will never return, its stack frame must have the same structure as any other.
조건에 맞게 새로 생성되어야 할 함수는 아래와 같다.
void argument_stack(char **argv, int argc, struct intr_frame *if_)
{
char *arg_address[128];
// 1. 스택 공간 확보 및 복사
for(int i = argc - 1; i >= 0; i--){
int argv_with_senti_len = strlen(argv[i]) + 1;
if_->rsp -= (argv_with_senti_len);
memcpy(if_->rsp, argv[i], argv_with_senti_len);
arg_address[i] = if_->rsp;
}
// 2. word-align
while(if_->rsp % SAU != 0){
if_->rsp--;
memset(if_->rsp, 0, 8);
}
// 3. word-align 이후 ~argv[0]의 주소를 넣어준다
for(int j = argc; j >= 0; j--){
if_->rsp -= SAU;
if(j == argc){
memset(if_->rsp, 0, 8);
} else{
memcpy(if_->rsp, &arg_address[j], 8);
}
}
// 4. Fake return address
if_->rsp -= 8;
memset(if_->rsp, 0, 8);
// 5. Set rdi, rsi (rdi : 문자열 목적지 주소, rsi : 문자열 출발지 주소)
if_->R.rdi = argc;
if_->R.rsi = if_->rsp + SAU;
}
유저 메모리 접근을 구현해야 한다.
To implement syscalls, you need to provide ways to read and write data in user virtual address space. You don't need this ability when getting the arguments. However, when you read the data from the pointer provided as the system call's argument, you should proxied through this functionallity. This can be a bit tricky: what if the user provides an invalid pointer, a pointer into kernel memory, or a block partially in one of those regions? You should handle these cases by terminating the user process.
유저가 유효하지 않은 포인터(NULL, 커널 메모리를 가리키는 포인터 등)을 가리키는 포인터일 때 유저 프로세스를 종료시켜야 한다.
void check_address(void *addr)
{
struct thread *curr = thread_current();
// 체크 조건
// user virtual address 인지 (is_user_vaddr) = 커널 VM이 아닌지
// 주소가 NULL 은 아닌지
// 유저 주소 영역 내를 가리키지만 아직 할당되지 않았는지
// 밥은 잘 먹고 다니는지 내 생각은 하는지
(pml4_get_page)
if (!is_user_vaddr(addr) || addr == NULL || pml4_get_page(curr->pml4, addr) == NULL){
exit(-1);
}
}
When the system call handler syscall_handler() gets control, the system call number is in the rax, and arguments are passed with the order %rdi, %rsi, %rdx, %r10, %r8, and %r9.
The caller's registers are accessible to struct intr_frame passed to it. (struct intr_frame is on the kernel stack.)
구현해야 하는 시스템 콜은 다음과 같다.
/* process 관련 system calls */
void halt (void);
void exit (int status);
pid_t fork (const char *thread_name);
int exec (const char *cmd_line);
int wait (pid_t pid);
/* file 관련 system calls */
bool create (const char *file, unsigned initial_size);
bool remove (const char *file);
int open (const char *file);
int filesize (int fd);
int read (int fd, void *buffer, unsigned size);
int write (int fd, const void *buffer, unsigned size);
void seek (int fd, unsigned position);
unsigned tell (int fd);
void close (int fd);
Whenever a user process terminates, because it called exit or for any other reason, print the process's name and exit code, formatted as if printed by
printf ("%s: exit(%d)\n", ...);
아래 printf 문을 통해 출력된 것과 같은 형식으로 exit 함수를 호출 했거나 다른 어떤 이유들로 유저 프로세스가 종료될 때 마다 프로세스의 이름과 exit 코드를 출력한다.
The name printed should be the full name passed to fork(). Do not print these messages when a kernel thread that is not a user process terminates, or when the halt system call is invoked. The message is optional when a process fails to load.
Aside from this, don't print any other messages that Pintos as provided doesn't already print. You may find extra messages useful during debugging, but they will confuse the grading scripts and thus lower your score.
출력되는 프로세스의 이름은 fork()에 전달되는 이름 전체여야 한다. 유저 프로세스가 아닌 커널 쓰레드가 프로세스를 종료하는 상황이거나, halt 시스템 콜이 호출된 상황이라면 이 메세지를 출력하면 안된다. 프로세스가 load되는데 실패한 경우라면 메세지를 출력하고 말고는 선택이다.
이 외에도, Pintos가 아직 인쇄하지 않은 다른 메시지를 인쇄하면 안된다. 디버깅에 유용한 다른 메세지들을 생각해낼 수도 있겠지만 이 메세지들이 grading 스크립트를 혼동시켜 여러분의 점수를 낮게 만들 수도 있다.
Add code to deny writes to files in use as executables. Many OSes do this because of the unpredictable results if a process tried to run code that was in the midst of being changed on disk. This is especially important once virtual memory is implemented in project 3, but it can't hurt even now.
You can use file_deny_write() to prevent writes to an open file. Calling file_allow_write() on the file will re-enable them (unless the file is denied writes by another opener). Closing a file will also re-enable writes. Thus, to deny writes to a process's executable, you must keep it open as long as the process is still running.
실행중인 파일, 즉 실행 파일에 쓰기를 거부하는 코드를 작성하라.
어떤 코드가 디스크에서 수정되고 있는 도중에 프로세스가 그 코드를 실행하려고 시도하면 예상치 못한 결과를 낳을 수 있기 때문에, 많은 OS들이 이를 방지한다.
이는 프로젝트 3에서 가상 메모리가 구현된 이후에 특히 중요해지는 부분이지만, 그렇다고 해서 지금 코드가 손상되어도 괜찮다는 말은 아니다.
열려있는 파일에 쓰기를 방지하려면 file_deny_write() 함수를 사용하면 된다.
그리고 file_allow_write()를 파일 안에서 호출하면 다시 쓰기가 가능해지도록 만들 수 있다 (이 파일을 여는 또다른 무언가에 의해 쓰기가 거부되지만 않는다면).
그리고 파일을 닫아도 다시 쓰기가 가능해지게 된다. 그러므로, 프로세스의 실행 파일에 쓰기를 계속 거부하려면, 프로세스가 돌아가는 동안에는 실행 파일이 쭉 열려 있게끔 해야 한다.