ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • PintOS Project 2. User Program - WIL
    SWJungle 2023. 10. 18. 16:36

    argument passing은 프로그램을 실행할 때 필요한 인자들을 스택에 세팅하고 넘겨주는 것이다. 그런데 결국 프로그램을 실행하는 것은 함수를 호출하는 것과 비슷하다. 둘다 레지스터를 사용해서 인자를 넘겨준다. 그런데 프로그램 실행같은 경우, int main(int argc, char** argv) 함수를 통해 실행되는데, 여기서 argc는 인자의 개수이고, argv는 이후 설명하겠다.

    함수 호출하는 과정엔 약속된 규칙, 규약이 있다. x86-64 아키텍쳐의 호출 규약은 다음과 같다.

    x86 호출 규약 (x86 calling conventions, 위키피디아)

    1. 함수의 인자 전달에 사용되는 정수 레지스터들(rdi, rsi, rdx, rcx, r8, r9) 사용
    2. 호출자: 다음 명령어의 주소(rip값)를 스택에 푸시 + 피호출자의 첫번째 명령어로 jump
    3. rax레지스터: 피호출자의 리턴값 저장
    4. 피호출자가 ret instruction 만날 때: 스택에 넣어두었던 rip값을 stack pop 해서 rip 레지스터에 넣어줌

    함수 호출 예시

    함수 구조: int f(int a, int b, int c);

    함수 호출: f(a, b, c) → 3개의 int타입 a, b, c를 f의 인자로 넣고 호출시,

    1. f(a, b, c) 호출 직전에 rip에 들어있던 주소값(다음에 수행할 명령어의 주소, 0x4747fe70)을 스택에 push
    2. f()함수의 인자인 a, b, c를 각각 rdi, rsi, rdx에 넣기
    3. f()함수의 첫번째 명령어로 jump

    다음은 프로그램을 실행하는 과정이다.

    1. 터미널에다가 /bin/ls -l foo bar 입력
    2. 명령문을 /bin/ls, -l, foo, bar로 분리
    3. 유저 스택에다가 분리한 문자열을 하나씩 집어넣기
      • 스택에 push하는 과정은 rsp를 데이터 크기만큼 내린 후, 생긴 공간에 데이터를 memcpy로 복사
    4. 이제, 각 문자열이 저장된 스택의 주소를 스택에 집어넣기
      • Word align를 하는 이유는 프로세서가 데이터를 효율적으로 처리하기 위해 메모리 주소를 해당 아키텍처의 워드 크기에 맞게 정렬하여 효율적인 메모리 액세스를 가능하게 하기 때문이다.
      • 그전에, rsp를 8의 배수로 정렬시켜야 한다. 이것을 해주어야 하는 이유는, 64비트 아키텍처이기 때문에 한번에 64비트만큼을 처리하는데, rsp를 8의 배수로 나누어지게 정렬하면, 이후에 스택에 넣을 때 명령어 한번 처리할 때마다 하나의 포인터를 처리할 수 있기 때문이다. 8의 배수가 아니면 64비트만큼 처리해도 남은 비트가 있을테고, 이러면 두번째로 64비트를 처리할 때 온전히 포인터값을 얻어낼 수 있기 때문에 시간이 더 걸린다. → 확실하지 않다
      • 이때는, 문자열들이 저장된 주소를 거꾸로 넣어주어야 한다.
      • 그런데, 이 문자열들이 몇개 있는지를 표시해주기 위해서 맨 처음에는 NULL을 넣음으로써 인자는 이제 없다는 것을 표시한다. c언어의 표준 요구사항이기도 하다.
    5. 이제부터는 함수 호출규약과 동일하게 설정해주면 된다.
      • %rsi: 첫번째 인자의 포인터가 담긴 곳의 주소(&argv[0]), %rdi: argc(인자의 개수)로 설정
      • 가짜 리턴 어드레스(rip)를 스택에 push 해줌
    6. 이렇게 해주면, main에서는 argc로 인자의 개수를 받고, argv로 스택에 저장된 인자에 접근이 가능해진다.

    유저 프로그램 시작 바로 전 스택의 상태

    차례대로 인자, word-align, NULL, 인자의 포인터(주소), 가짜 rip가 스택에 적재됨


    System call

    시스템 콜은 유저모드인 프로세스가 커널모드상에서의 작업이 필요할 때 직접 커널코드에 접근하지 않고, syscall을 호출해서 모드를 커널모드로 세팅해서 OS에게 작업을 대신 맡기는 것이다.

    이 디자인은 하드웨어에 대한 접근을 직접 할 수 있게 하지 않고, system call이라는 인터페이스를 통해 간접 접근하게 함으로써 유저모드에서 무분별한 하드웨어 자원 접근 및 사용을 막기 위함이다.

    OS는 유저모드 프로세스를 위해 유용한 system call들을 미리 만들어 놓았다.

    핀토스에서의 syscall 동작 과정

    1. 예시: open syscall 호출(유저모드)

    open syscall 호출
    open

    2. syscall() 함수 호출(유저모드)

    num_에 syscall number(open syscall의 경우 SYS_OPEN)이 들어오고, a1에는 파일 이름이 들어간다.

    syscall 내부에서 rax = syscall number, rdi = 파일 이름

    이 값들을 rax, rdi에 넣고 syscall instruction 호출

     

    syscall instruction

    - x86 아키텍처 고유의 instruction

    - 특권 수준을 3(user mode) → 0(kernel mode)으로 변경

    - MSR 레지스터에 접근해서 syscall_entry 함수의 시작위치 받아 rip에 넣기

     

    syscall_init

    핀토스가 초기화될 때, syscall_entry 주소값을 MSR 레지스터에 넣는 과정 수행

     

    3. syscall_entry에서는 syscall을 호출하기 직전의 TLB(intr_frame) 데이터들을 우선 스택에 저장(syscall이 끝난 후 다시 원래 상태로 돌아오기 위해)

    syscall_entry의 스택 적재 부분

    4. syscall_handler 호출

    syscall_handler() 호출

    5. syscall_handler에서는 sycall에 맞는 로직 수행

    syscall_handler
    open 인 경우

    6. syscall_handler() 종료되면 다시 syscall_entry() 함수로 돌아와서 스택에 이전에 넣어줬던 데이터를 popq해서 레지스터에 넣기. 이때 rip, rsp 복원

    7. sysretq instruction 호출

       - 권한수준을 0→3: 유저모드로 변경

    8. 이제 open syscall 끝났고, 다음 명령어 수행하러 간다.

    open syscall 호출

     

Designed by Tistory.