<aside> ❗ 목차
Argument Parsing
System Call a. halt b. create c. remove d. filesize e. seek f. tell g. open h. write i. read j. close k. exec l. wait, exit m. fork n. dup2
</aside>
Argument Parsing
우리가 흔히 터미널에서 파일을 실행시키기 위해서 명령어를 입력한다. 예를들어 /bin/ls -l foo boo 를 입력하면 ls 파일에 -l , foo , boo 인자를 주어 실행한다. 하지만 한줄로 운영체제에게 준다면 운영체제는 알아듣지 못한다. 커널이 알아 들을 수 있도록 공백을 기준으로 parsing 하여 레지스터에 입력해주어야 한다. 아래 그림은 /bin/ls -l foo boo 를 스택으로 쌓은 것을 나타낸다. 우리는 코드를 통해 이와 같이 /bin/ls -l foo boo 를 공백 기준으로 나눈 뒤 이중연결 리스트를 활용하여 연결 시켜주어야 한다.

단순하게 쌓으면 될 것 같지만 고려해 주어야 하는 사항이 또 있다. cpu는 8바이트 단위로 데이터를 읽어 들인다. 보통 다른 핀토스 자료는 32bit 운영체제 이기 때문에 4바이트씩 읽어들인다. 만약 공백이 있기 전 데이터가 3바이트인 경우 패딩을 넣어 4바이트나 8바이트 단위로 읽어들이게 끔 조정해준다.

위 그림은 32비트 운영체제 기준으로 유저 스택을 나타낸 것이다. 우선 PHYS_BASE는 커널 영역과 유저스택의 경계로 유저스택은 커널 영역을 침범하면 안된다. 예를 들어 파란색 영역의 b,a,r,00 를 살펴보자. pintos는 little endian 방식으로 수를 채우기 때문에 맨 왼쪽 b의 주소값이 가장 작다. b: 0xbffffffc, a: 0xbffffffd, r: 0xbffffffe, 00: 0xbfffffff 이다. 주소 0xbfffffe4에는 fc,ff,ff,bf가 적혀 있는데 이것은 argv[3]의 시작주소가 little endian 방식으로 적혀 있는 것이다. 마지막으로 노란 영역의 00은 위에서 언급한 내용으로 4바이트를 맞추기 위해 추가한 데이터이다.
little endian에 대해 궁금 하다면 아래 블로그를 참고하자.
[Byte Order 바이트 오더] 빅엔디안(Big Endian)과 리틀엔디안(little endian) - 1편
/* process.c */
int
process_exec (void *f_name) {
...
/* argument parsing */
char *argv[30];
int argc = 0;
char *token, *save_ptr;
token = strtok_r(file_name, " ", &save_ptr);
while (token != NULL)
{
/* 공백 기준으로 명령어 나누어 리스트로 조합 */
argv[argc] = token;
token = strtok_r(NULL, " ", &save_ptr);
argc++;
}
...
void **rspp = &_if.rsp;
set_userStack(argv, argc, rspp);
_if.R.rdi = argc;
_if.R.rsi = (uint64_t)*rspp + sizeof(void *);
...
}
void set_userStack(char **argv, int argc, void **rspp)
{
// 1. Save argument strings (character by character)
for (int i = argc - 1; i >= 0; i--)
{
int N = strlen(argv[i]);
for (int j = N; j >= 0; j--)
{
char individual_character = argv[i][j];
(*rspp)--;
**(char **)rspp = individual_character; // 1 byte
}
argv[i] = *(char **)rspp; // push this address too
}
// 2. Word-align padding
int pad = (int)*rspp % 8;
for (int k = 0; k < pad; k++)
{
(*rspp)--;
**(uint8_t **)rspp = (uint8_t)0; // 1 byte
}
// 3. Pointers to the argument strings
size_t PTR_SIZE = sizeof(char *);
(*rspp) -= PTR_SIZE;
**(char ***)rspp = (char *)0;
for (int i = argc - 1; i >= 0; i--)
{
(*rspp) -= PTR_SIZE;
**(char ***)rspp = argv[i];
}
// 4. Return address
(*rspp) -= PTR_SIZE;
**(void ***)rspp = (void *)0;
}
procee_exec 함수에서 공백 기준으로 argv와 argc를 설정해 준다. 이때 strtok_r 내장함수를 이용한다. rspp 에 현재 인터럽트 프레임의 스택포인터를 저장한 후 set_userStack 함수로 넘겨 준다. set_userStack 함수에서 위의 그림과 같이 user stack을 설정한다. 함수 종료 후 rspp의 포인터를 리턴 주소가 아닌 argc로 재조정 해준다.
System Call
User mode에서 Kernel mode로의 전환이 필요하면 프로세스는 운영체제에게 system call을 요청한다. 운영체제는 시스템 콜 번호에 따라 알맞은 작업을 수행한다. 현재 pintos 에서는 syscall 번호와 매개 변수들이 레지스터에 입력되어있다. 예를 들면 f의 rax에는 시스템콜 번호가 입력된다. 우리는 주어진 시스템 콜 번호와 인자들을 활용하여 적절한 대응을 하도록 코드를 구현한다.
void
syscall_handler (struct intr_frame *f UNUSED) {
// TODO: Your implementation goes here.
// printf("syscall! , %d\\n",f->R.rax);
switch (f->R.rax)
{
case SYS_HALT:
halt();
break;
case SYS_EXIT:
exit(f->R.rdi);
break;
case SYS_FORK:
f->R.rax = fork(f->R.rdi, f);
break;
case SYS_EXEC:
if (exec(f->R.rdi) == -1)
exit(-1);
break;
case SYS_WAIT:
f->R.rax = process_wait(f->R.rdi);
break;
case SYS_CREATE:
f->R.rax = create(f->R.rdi, f->R.rsi);
break;
case SYS_REMOVE:
f->R.rax = remove(f->R.rdi);
break;
case SYS_OPEN:
f->R.rax = open(f->R.rdi);
break;
case SYS_FILESIZE:
f->R.rax = filesize(f->R.rdi);
break;
case SYS_READ:
f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_SEEK:
seek(f->R.rdi, f->R.rsi);
break;
case SYS_TELL:
f->R.rax = tell(f->R.rdi);
break;
case SYS_CLOSE:
close(f->R.rdi);
break;
case SYS_DUP2:
f->R.rax = dup2(f->R.rdi, f->R.rsi);
break;
default:
exit(-1);
break;
}
}
더 알아야 할 것 및 느낀점