(프로젝트 개요)


현재 pintos는 시스템 콜 핸들러가 제대로 구현되어 있지 않다. 시스템 콜이 발생해도 system call!! 이라는 출력문 하나만 달랑 뿌리고, 그대로 스레드가 종료된다.(thread_exit()) 따라서, 이번 프로젝트 2는 핀토스의 시스템 콜 핸들러 함수를 구현하고, 간단한 시스템 콜(halt, exit, create, remove)를 구현해본다.



| 시스템 콜?

사용자 모드 수준에서의 프로그램이 커널 기능을 사용할 수 있도록 해주는 인터페이스.

시스템 콜의 기능들은 커널 모드에서 실행되고, 처리 후 사용자 모드로 복귀됨.



(프로젝트 해결 방법 개요)


우선, 2가지 함수를 구현해야 함. 


1. 특정 포인터(주소값)가 유저 영역에 존재하는지 확인하는 함수.

void check_address(void * address);


2. 유저 스택에 들어 있는 인자들의 주소를 저장하는 함수.

void get_argument(void *esp, void *arg, int count);


1. 유저 영역은 0x08048000 에서 0xc0000000 사이에 (경계값 제외) 존재하는 주소값이면 유저 영역인 것이다. check_address를 구현할 때 이 바깥 범위의 값이면 exit (-1)을 호출해 프로그램을 종료시켜야 한다. (유저 영역이 아닌 커널 영역을 건드리는 프로그램은 프로그램을 종료시키는 것이 적절한 운영체제의 반응이다.)


2. get_argument 의 함수는 esp 인자에서 인자들을 4바이트씩 긁어온 후, arg라는 변수에 저장하는 행동을 한다. 참고로, arg에 저장하는 값들은 인자의 주소값이다. (이 부분을 정확히 이해하라.) arg에 주소값을 저장하지 않고 인자값 자체를 저장하면 안된다. 왜냐하면, 시스템 콜의 인자들이 전부 4바이트로 표현할 수 있다는 보장이 없기 떄문이다. (가령 문자열이 전달되어 온다면 어떻게 할것인가..? 해결방법은 문자열 자체의 주소값을 저장하는 것이다.)


따라서, arg에는 반드시 인자의 주.소.값이 저장되어야 함을 무한 강조!!

f->esp 의 첫 4바이트는 시스템 콜 넘버이므로, 그 다음 4바이트씩 count 횟수만큼 긁어와서 arg에 저장하면 된다.



============

시스템 콜 핸들러 함수

static void syscall_handler (struct intr_frame *f UNUSED) { /* 유저 스택에 저장되어 있는 시스템 콜 넘버를 이용해 시스템 콜 핸들러 구현 */ /* 스택 포인터가 유저 영역인지 확인 */ /* 저장된 인자 값이 포인터일 경우 유저 영역의 주소인지 확인*/

}

f->esp (유저 스택)에는 4바이트씩 인자들이 저장되어 있다. f->esp의 첫 번째 4바이트에는 시스템 콜의 넘버가 저장되어 있고, 그 뒤로 4바이트씩 시스템 콜의 인자가 저장되어 있다.


예를 들어, create 시스템 콜은 인자로 파일 이름과 파일의 첫 크기를 인자로 받는다. 그러면, create 시스템 콜이 발생할 때 유저 스택 포인터는

다음과 같은 구조를 띈다.


f->esp

(4바이트) 4

(4바이트) 0x100

(4바이트) 0x104

....

0x100은 파일 이름 문자열의 주소값이고, 0x104는 파일의 첫 크기의 주소값이다. 물론, 파일의 첫 크기값은 정수일테지만, 정수값 자체를 유저 스택에 저장하지 않는다. 유저 스택에는 '주소값'이 저장되어 있음을 무조건 기억하라. 따라서, 나중에 시스템 콜 핸들러에서 create 시스템 콜을 처리할 때 다음과 같이 처리해야 한다.


    case SYS_CREATE:/* 4 */
      f->eax = create((char *)*(int *)arg[0], (unsigned)*(int *)arg[1]);
    break;

현재 arg에는 인자들의 '주소값'이 저장되어 있다. 즉, arg[0], arg[1],...등은 전부 '주소값'이다. 따라서, *arg[0]의 의미는, arg의 첫 번째 값의 내용물을 찾아오는 것이다. 따라서, 첫 번째 인자의 주소값의 내용물을 찾아와서 그들을 시스템 콜 함수의 인자로 넘겨줘야 하는 것이다. 


pintos 운영체제에는 자체 내장되어 있는 테스트 프로그램이 있습니다. 

src/userprog/ 디렉토리에서 'make check' 쉘 명령어를 입력하면, 76개의 테스트를 실행하며, 마지막에 테스트의 결과를 알려줍니다.



76개의 테스트 중 (제가 봤을 때) 제일 디버깅이 어려운 테스트는 'multi-oom'테스트입니다.

multi-oom테스트의 목적은 pintos운영체제의 메모리 누수가 존재하는지를 확인합니다.



메모리 누수가 일어나는 곳을 찾아야 하는데 쉽지 않습니다. 

multi-oom 테스트를 해결해나가는 약간의 팁을 제공하자면,



1) malloc - free를 정확하게 사용해라. 

malloc함수는 힙영역에 새로운 메모리를 할당하는 함수입니다. 만약에 어떤 함수에서 메모리를 새로 할당한 이후, 그 함수가 종료되기 전까지 할당한 메모리가 사라지지 않는다면, 계속해서 쓸모없는 메모리가 남게 되어 메모리 누수가 발생합니다. 따라서, 함수 루틴 안에서 반드시 malloc을 사용한 경우에는 그에 대응하는 free를 해주기 바랍니다. 참고로, malloc을 한 이후, 그 메모리를 가리키는 포인터를 다음에도 계속 가지고 있다면, 즉시 free를 해줄 필요는 없습니다. 최종적으로 그 메모리가 필요없게 되었을 때 정확히 free를 해주는 것이 관건인 것이지요.


간단히 말하자면, 함수 내에서 임시적으로 사용하기 위해 할당한 메모리는 그 함수가 끝나는 시점에서 즉시 free로 메모리 해제를 해주어야 하고, 스레드 내에서 파일 디스크립터 테이블과 같이 스레드가 죽을 때까지 가지고 있어야 하는 정보에 대한 메모리는 스레드가 죽을 때, 메모리 해제를 해주면 되는 것이지요.



2) 1)과 같은 이유로, palloc_get_page에 대응하는 palloc_free_page를 철저하게 사용하라.

1)과 동일한 이유입니다. palloc_get_page도 메모리를 할당하는 함수이고, palloc_free_page도 메모리를 해제하는 함수입니다.



3) 문자열에 대한 메모리 누수를 정확하게 계산하라.

운영체제를 건축하는 일은 C언어의 메모리 사용을 '정확히' 알아야 합니다.

하지만, 제가 학부생으로서 다른 친구들을 보았을 때 C언어의 메모리(포인터) 사용을 정확하게 아는 사람은 정말 드물어요.

아마 4학년 졸업한 후에도 10명 중 1명이 채 안될 겁니다. 정말 세밀한 부분까지 완벽하게 이해한 사람은 거의 없습니다.

물론 저도 아직 부족한 부분이 많고, 운영체제를 건축해보면서 더더욱 부족한 부분을 알게 되고 수정해나가네요.

그런 부분에서 큰 성장을 하는 것 같아 기분이 좋습니다. :) 



/* tokenize the 'string' with a deliminator " ",
   returns how many arguments in 'string' */
int get_argument_count(const char * string)
{
  char * new_string = (char *)malloc(sizeof(char)*(strlen(string)+1));
  strlcpy(new_string, string, strlen(string)+1);

  char *token, *save_ptr;
  int i=0;
  for(token=strtok_r(new_string, " ", &save_ptr); token;
      token=strtok_r(NULL, " ", &save_ptr))
  {
    i++;
  }
  free(new_string);
  return i;
}

/* tokenize the 'parse' with a deliminator " ",
   each arguments will be saved into the 'esp'. */
void argument_stack(char ** parse, int count, void ** esp)
{
/* parse = sentence including 'process and arguments'
   count = argc(argument counts) through 'GetArgumentCount' func.
   esp = &if_.esp */

  char * command_line = *parse; // like 'echo x y z'

  char * fn_copy;
  fn_copy = palloc_get_page(PAL_USER);
  if(fn_copy == NULL)
    thread_exit();
  strlcpy(fn_copy, command_line, PGSIZE);

  char * token, * save_ptr;
  int i=0;
  int * variable_index = (int *)malloc(sizeof(int)*count);

  for(token = strtok_r(fn_copy, " ", &save_ptr); token;
      token = strtok_r(NULL, " ", &save_ptr))
  {
    printf("'%s'\n", token);
    variable_index[i++] = token - fn_copy;
  }

  int size = strlen((char*)(*parse));

  *esp -= size + 1;
  int command_position = (int)*esp; // stack pointer position for command line

  for(i=0; i<=size; i++)
  {
    *(char*)(*esp) = fn_copy[i];
    *esp += 1;
  }
  *esp = (int)command_position;

  *esp -= ((int)(*esp)%4<0? (int)(*esp)%4+4 : (int)(*esp)%4);
  *esp -= 4;
  *(int*)(*esp) = 0;

  // argv[1,2,3,...]: arguments
  for(i=count-1; i>=0; i--)
  {
    *esp -= 4;
    *(void**)(*esp) = command_position + variable_index[i];
  }

  // argv[0]: process_name
  *esp -= 4;
  *(char **)(*esp) = (*esp + 4);
  // argc: argument count
  *esp -= 4;
  *(int *)(*esp) = count;
  // return address - fake (0)
  *esp -= 4;
  *(int*)(*esp) = 0;

  free(variable_index);
}


pintos 는 현재, 프로그램이름과 인자를 구분하는 규칙이 존재하지 않는다.

ls -l // 핀토스는 'ls -l' 을 하나의 프로그램 명으로 인식하고 있다. 


첫 번째 과제의 목표는 프로그램의 이름과 인자를 구분하여 스택에 저장, 인자를 응용 프로그램에 전달하는 기능을 구현하는 것이다.


이를 위해 소규모 목표를 잡아 보면,

1. 응용프로그램의 실행 흐름을 추적하여 파싱 시점을 알아내야 한다.

2. 파싱을 어떻게 할 것인가?

3. 파싱한 인자들을 전달하는 인자 전달 메커니즘을 이해하고, 인자를 전달하는 인터페이스를 구현한다.




| 1. 응용프로그램의 실행 흐름을 추적하여 파싱 시점을 알아내야 한다.


< pintos 의 응용 프로그램 실행 메커니즘 >

운영체제의 main 함수에서 run_action 함수 호출

-> run_task 함수 호출

-> process_execute 함수 호출

-> thread_create 함수와 start_process 함수 호출

...


이러한 과정을 지나치며 응용 프로그램이 시작된다. 


run_task에서 start_process 함수까지 진행이 되는 동안 함수들의 인자는 char ** argv 가 전달되는데, 이는 커맨드 라인 전체를 포함하고 있다. (예를 들어, 'ls -a'를 pintos 시작 시 받은 경우, argv 를 'ls -a' 전체를 받는 것이다.)

따라서, 이를 파싱해야 할 시점을 찾을 수 있다. 바로, start_process 에서 load 함수를 호출하는 그 때, 문자열을 파싱하여 넘겨주어야 한다.





| 2. 파싱을 어떻게 할 것인가?


start_process 함수에서 load 함수를 호출하기 전에 문자열을 파싱해야 한다. 문자열 파싱 함수로는 strtok_r 을 쓰도록 하자.


char *strtok_r (char *s, const char *delimiters, char **save_ptr) /* string.h */ 


example)

char s[] = "String to tokenize.";
char *token, *save_ptr;
for (token = strtok_r (s, " ", &save_ptr); token != NULL;

     token = strtok_r (NULL, " ", &save_ptr))

     printf ("'%s'\n", token); 






| 3. 파싱한 인자들을 전달하는 인자 전달 메커니즘을 이해하고, 인자를 전달하는 인터페이스를 구현한다.


유저 스택에 파싱된 토큰들을 저장하는 함수를 구현해야 한다.


void argument_stack(char **parse ,int count ,void **esp);

parse: 프로그램 이름과 인자가 저장되어 있는 메모리 공간

count: 인자의 개수

esp: 스택 포인터를 가리키는 주소


이 함수는 parse에서 커맨드 라인을 읽어와 파싱하고, esp에 알맞게 저장하는 함수이다. 


이 함수가 start_process 함수에서 호출될 때는 다음과 같은 꼴로 사용해야 한다.


argument_stack(&parse, count, &if_.esp);


즉, 첫 번째와 세 번째 인자는 주소값으로 인자를 전달한다는 가정 하에 다음 설명을 진행하겠다.


pintos에서는 스택이 아래로 자라는 경향이 있다.

제일 처음에는 *esp 값이 0xc0000000(유저스택의 최고점, 3GB)일 것이다.


*esp -= 4; 


이렇게 하면, esp 값은 0xbffffffc가 될 것이다. 이 때,


**esp = 10;  // 정확하게는, *(int *)(* esp) = 10; (캐스팅을 제대로 해야 한다.)


이렇게 되면, 0xbffffffc 라는 유저 스택 위치에서, 10이라는 값이 저장되는 것이다. 

이러한 논리를 이용해서 프로젝트를 진행할 수 있을 것이다. 




| 결과 확인


첫 번째 프로젝트를 제대로 완료했다면, 다음 명령어를 입력해보자.


$ pintos -v -- run 'echo x y z'


참고로, 결과를 확인하기 전에는 반드시 핀토스 파일 시스템 안에 'echo'라는 응용 프로그램이 들어가 있어야 한다.


| 핀토스 운영체제란?


핀토스 운영체제는 x86 아키텍처에서 운영체제의 기본을 이해하기 위해 고안된 교육용 운영체제입니다. 2004년 스탠포드 대학에서 Ben Pfaff라는 사람에 의해 만들어졌고, 이 운영체제는 커널 스레드, 유저 프로그램을 로딩하고 실행하는 것, 파일 시스템 등 운영체제가 갖춰야 할 기본적인 기능들을 지원하고 있습니다. 핀토스 운영체제는 Bochs나 QEMU 등 x86 시뮬레이터 위에서 사용해야 합니다.


포스트에서는 '리눅스 우분투' 운영체제에서 'Bochs' 시뮬레이터를 통해서 Pintos를 설치하고 정상작동 확인하는 것 까지 다뤄보도록 하겠습니다.




| 핀토스 운영체제 설치하기


1. 가상머신 혹은 호스트 운영체제로 '우분투' 운영체제 등 리눅스 계열 운영체제를 설치한다. 


※ 가상머신 Virtual Box에 리눅스 우분투 설치하기 ► http://thinkpro.tistory.com/40



2. 우분투 위에 Bochs 시뮬레이터를 설치한다.


2-1. 다음 사이트에서 Bochs 다운로드받기

► http://bochs.sourceforge.net



tar.gz 확장자를 받으셔야 합니다.



2-2. 다운 받은 압축 파일을 원하는 폴더로 이동한 후, 다음 명령어로 파일 압축 해제 해줍니다.


$ cd ~ // 홈 디렉토리에 bochs 파일을 압축 해제. 원하는 경로로 설정하세요.

$ tar xvf bochs-2.6.2.tar.gz


2-3. bochs 폴더로 이동합니다. 그리고 컴파일 및 설치 과정을 진행합니다.


$ ./configure --enable-gdb-stub --with-nogui

$ make

$ sudo make install


※ "C compiler cannot create executables" 오류가 난다면

$ sudo apt-get install libc6-dev g++ gcc 


※ "X windows libraries were not found" 오류가 난다면

$ sudo apt-get install xorg-dev


※ 그 외 오류의 경우 gcc 버전이 호환되지 않는 경우가 많습니다.

gcc 버전을 다운그레이드하세요.

$ sudo apt-get install gcc-4.6

$ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.6 50




3. Pintos 설치 및 작동 확인하기


여기서 소스 코드를 다운로드 받읍시다.

► http://pintos.software.informer.com/download/


3-1. 소스 코드를 압축 해제합니다.

$ tar xvf pintos.tar.gz


3-2. bashrc 파일에 환경 설정

$ vi ~/.bashrc


파일 내에 마지막에 

export PATH="$PATH:/home/usr/pintos/src/utils"

추가(usr는 자신의 username으로 변경해야 함.)


$ source ~/.bashrc // 이는 환경설정을 등록하는 명령어임.




4. Pintos 가 제대로 작동하는지 확인하기.


$ cd pintos/src/threads

$ make

$ cd build

$ pintos -- run alarm-multiple


오류없이 프로그램이 돌아간다면 성공한것입니다.

+ Recent posts