ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Pintos - System Call
    SW사관학교 Jungle 2021. 2. 17. 21:06

    feat. 2월 중순 되돌아보기 link

    pintos-kaist-gitbook을 참고해 작성한 글입니다.

    2월 4일부터 17일까지 2주간 Pintos의 System Call들을 구현했다.

    그동안 당연하게 여기고 사용하고 있었던 함수들, Process 그리고 Thread들이 어떻게 작동하는지 이해하고, 살펴볼 수 있는 기회였다. 이론으로만 막연하게 익혔었던, Lock과 Semaphore를 어떻게 적용할 수 있는지 실습해볼 수 있었던 좋은 기회이기도 했다.

    Argument Passing

    pintos에서 main함수를 실행할 때 인자들을 전달하는 argument_stack함수를 구현해 볼 것이다.

    void
    _start (int argc, char *argv[]) {
        exit (main (argc, argv));
    }

    다음과 같은 프로그램이 있다.

    이 프로그램을 /bin/ls -l foo bar명령어로 실행시키는 상황을 가정해보자.

    프로그램은 /bin/ls -l foo bar를 파싱한 후에 각각의 단어들을 stack에 쌓는다. 이 단어들을 쌓을 때는 역순으로 단어들이 저장되게 되며, 각 문자들은 \0를 통해 구분된다. 단어들을 저장한 후에는 메모리 align을 위해 padding을 집어넣는다. 우리가 argument passing을 구현하는 64bit운영체제에서는 포인터 자료형의 크기는 8bytes이고, 이들은 모두 8byte-align되어있어야 한다. 따라서, align을 위한 padding이 필요하다.

    padding다음으로는, 파싱된 각 문자열의 시작주소를 가리키는 포인터를 저장한다. 마지막 포인터는 NULL포인터이고, argument를 확인할 때, NULL포인터를 마지막으로 탐색을 종료하기 때문에 넣어주어야 한다. 문자열들을 가리키는 포인터와 NULL포인터를 마찬가지로 역순으로 stack에 쌓아준다. 마지막으로 return address를 입력해 준다. 이 자리에는 함수가 반환될 때 이동할 주소를 저장한다. 하지만, main함수는 return을 하지않기 때문에 0을 넣어준다. return을 하지 않더라도 다른 함수들이 인자를 전달할 때 사용하는 방식과 동일한 방식을 사용해야 함으로 return을 하지 않더라도, 0을 넣어주자!

    Address Name Data Type
    0x4747fffc argv[3][...] 'bar\0' char[4]
    0x4747fff8 argv[2][...] 'foo\0' char[4]
    0x4747fff5 argv[1][...] '-l\0' char[3]
    0x4747ffed argv[0][...] '/bin/ls\0' char[8]
    0x4747ffe8 word-align 0 uint8_t[]
    0x4747ffe0 argv[4] 0 char *
    0x4747ffd8 argv[3] 0x4747fffc char *
    0x4747ffd0 argv[2] 0x4747fff8 char *
    0x4747ffc8 argv[1] 0x4747fff5 char *
    0x4747ffc0 argv[0] 0x4747ffed char *
    0x4747ffb8 return address 0 void (*) ()

    인자들을 모두 저장하면, 위의 표와 같이 인자들이 저장되게 된다.

    argument_stack은 파싱된 문자열들이 담긴 배열인 parse와 문자열의 개수가 저장된 count 그리고 stack pointer esp를 인자로 받는다.

    esp를 이동시키며 인자들을 저장하자. stack은 높은 주소에서 낮은 주소로 거꾸로 자라기 때문에 저장할 인자의 size만큼 esp에서 뺀 후에 해당 메모리에 값을 저장해야 한다.

    void argument_stack(char **parse ,int count ,void **esp){
        //parse : 파싱된 문자열들
        //count : 문자열들의 개수
        //esp   : stack pointer(64bit운영체제에서의 rsp와 동일)
        int i, j;
        void* save_pointer[count];
    
        //인자들을 역순으로 저장
        for(i = count -1; i>-1; i--){
            for(j = strlen(parse[i]); j>-1; j--){
                *esp = (char*)*esp - 1;
                **(char **)esp = parse[i][j];
            }
            save_pointer[i] = *(char **)esp;
        }
        //8byte-allign
        *esp = *(int64_t*)esp & ~(int64_t)7;
    
        //마지막 널값의 주소!
        *esp = (int64_t*)*esp - 1;
        **(int64_t**)esp = NULL;
    
        //각 문자열들의 시작 주소
        for(i = count - 1; i > -1; i--){
            *esp = (int64_t*)*esp - 1;
            **(int64_t**)esp = save_pointer[i];
        }
        //return value : trash주소값 0
        *esp = (int64_t*)*esp - 1;
        **(int64_t **)esp = NULL;
        return;
    }

    System Calls

    유저 프로그램들이 커널 영역의 데이터를 수정 삭제해, 운영체제에 손상을 미치는 것을 방지하기 위해서 OS는 System Call을 지원한다. 유저 프로그램은 커널 영역에 접근할 수 없으며, System Call을 통해서만 커널모드로 전환되어 특정 작업을 처리할 수 있다. System Call들을 구현해보자!

    Pintos Project 1주차에 진행했던 thread_scheduling에서 interrupt에 대해 다루었다. interrupt는 시스템 외부에서 신호를 주어 제어권을 운영체제에게 넘겨주었다면, systemcall은 프로그램의 코드 내부에서 발생한다. fork, wait, exec, read, write, open, close 그리고 exit등 다양한 systemcall들을 구현했지만, 이들 중 몇 가지만 이번 포스팅에서 다루어 볼 예정이다.

    systemcall을 호출할 때는 systemcall번호와 인자들을 레지스터에 저장해 두어야 한다.

    %rax에는 systemcall번호가 저장된다.

    %rdi %rsi %rdx %r10 %r8 %r9systemcall에 전달되는 첫 번째 인자부터 6번째 인자가 저장된다.

    먼저, 프로그램에서 read함수를 호출한다.

    //lib/user/syscall.c
    
    #define syscall3(NUMBER, ARG0, ARG1, ARG2) ( \
            syscall(((uint64_t) NUMBER), \
                ((uint64_t) ARG0), \
                ((uint64_t) ARG1), \
                ((uint64_t) ARG2), 0, 0, 0))
    
    int
    read (int fd, void *buffer, unsigned size) {
        return syscall3 (SYS_READ, fd, buffer, size);
    }

    read함수는 syscall에 3개의 인자를 전달한다. syscall3매크로는 인자를 3개 전달해야 하는 systemcall을 위한 매크로이며, syscall1부터 syscall6까지 존재한다.

    read함수는 syscall3의 첫 번째 인자로 SYS_READ를 전달하고, 그 뒤로 나머지 인자들을 전달한다. syscall3syscall을 호출하며, syscall함수에서 레지스터에 전달받은 인자들을 차례로 저장한다. 위에서 언급했듯이 %rax에 systemcall번호가 저장되고, %rdi부터 %r9까지 6개의 인자들이 저장된다. read함수의 경우 3개의 인자를 전달함으로, %rdi %rsi %rdx에 차례로 fd, buffer, size가 저장되고, %r10 %r8 %r9에는 0이 저장된다.

    __attribute__((always_inline))
    static __inline int64_t syscall (uint64_t num_, uint64_t a1_, uint64_t a2_,
            uint64_t a3_, uint64_t a4_, uint64_t a5_, uint64_t a6_) {
        int64_t ret;
        register uint64_t *num asm ("rax") = (uint64_t *) num_;
        register uint64_t *a1 asm ("rdi") = (uint64_t *) a1_;
        register uint64_t *a2 asm ("rsi") = (uint64_t *) a2_;
        register uint64_t *a3 asm ("rdx") = (uint64_t *) a3_;
        register uint64_t *a4 asm ("r10") = (uint64_t *) a4_;
        register uint64_t *a5 asm ("r8") = (uint64_t *) a5_;
        register uint64_t *a6 asm ("r9") = (uint64_t *) a6_;
        /* [인라인 어셈블리]
         * __asm__ __volatile__ (asms : output : input : clobber);
         * output의 인자들 부터 input의 인자들 까지 순서대로 %0, %1, %2 ~ 를 사용해 변수들을 사용할 수 있다.
         * 아래의 코드는 input으로 전달된 system call number와 6개의 인자들을 각각 
         * Register의 rax, rdi~r9에 집어넣는다. */
        __asm __volatile(
                "mov %1, %%rax\n"  //1번째 input
                "mov %2, %%rdi\n"
                "mov %3, %%rsi\n"
                "mov %4, %%rdx\n"
                "mov %5, %%r10\n"
                "mov %6, %%r8\n"
                "mov %7, %%r9\n"
                "syscall\n"       
                : "=a" (ret)
                : "g" (num), "g" (a1), "g" (a2), "g" (a3), "g" (a4), "g" (a5), "g" (a6)
                : "cc", "memory");
        return ret;

    위의 syscall함수가 호출되면 아래의 user mode에서 kernel mode로 전환되고, syscall_handler가 호출된다.

    /* The main system call interface */
    void
    syscall_handler (struct intr_frame *f UNUSED) {
      //f->R.rdi ~ f->R.rdi: 첫 번째 인자 ~ 여섯 번째 인자
      //f->rsp   : stack pointer
      //f->R.rax : system call number
      check_address(f->rsp,f->R.rdi);  //유효한 주소인지 확인
      switch((f->R.rax)){
        case SYS_HALT:
          halt();
          break;
        case SYS_EXIT:
          exit(f->R.rdi);
          break;
    
        <-case별로 함수를 호출한다.->      
        ...
      }

    syscall_handler함수는 프로세스의 interrupt frame을 인자로 받으며, interrupt frame에는 rsp 그리고 interrupt frame내의 gp_registers에 저장되어 있는 rax ,rsp, rdi~r9가 존재한다.

    struct intr_frame {
        /* Pushed by intr_entry in intr-stubs.S.
           These are the interrupted task's saved registers. */
        struct gp_registers R; //? R의 gp_registers안에 rax존재!
    
        ...
    }
    
    /* Interrupt stack frame. */
    struct gp_registers {
        uint64_t r15;
        uint64_t r14;
        uint64_t r13;
        uint64_t r12;
        uint64_t r11;
        uint64_t r10;
        uint64_t r9;
        uint64_t r8;
        uint64_t rsi;
        uint64_t rdi;
        uint64_t rbp;
        uint64_t rdx;
        uint64_t rcx;
        uint64_t rbx;
        uint64_t rax;
    } __attribute__((packed));

    f->R.rax에 저장되어 있는 systemcall number에 해당하는 함수를 실행시킨다.

    함수 실행을 마친 후에는 함수의 return값을 f->R.rax에 저장해 user_mode에서 실행했던 함수의 반환값으로 넘겨줄 수 있다.

    'SW사관학교 Jungle' 카테고리의 다른 글

    Pintos Project - Thread  (0) 2021.02.04
    JUNGLE_5주차  (0) 2021.01.20
    Crafton Q&A  (0) 2021.01.11
    JUNGLE_4주차  (0) 2021.01.07
    JUNGLE_3주차  (0) 2021.01.01
Designed by Tistory.