x86-64 기반 Pint OS 프로젝트 2의 System Calls를 구현하기 위해 공부한 개념을 정리한다.
Implementation
userprog/syscall.c에서 system call handler를 구현한다. 핀토스에서 기본적으로 제공된 스켈레톤 코드는 system call을 오직 프로세스 종료로만 다룬다. 우리는 system call number와 argument를 읽고, 적절한 actions을 수행하도록 해야한다.
System Call Detail
전통적인 x86 아키텍처에서 시스템 콜은 다른 software exceptions와 동일하게 처리되었다. 그러나 x86-64 아키텍처부터 설계자들은 syscall
이라는 특수한 명령어를 도입하여 시스템 콜 핸들러를 빠르게 호출하는 방법을 마련했다.
오늘날, syscall
명령어는 x86-64에서 시스템 콜을 호출하는 가장 보편적인 수단이 되었다. 이는 Pint OS에서도 마찬가지로, User Program은 Kernel에게 시스템 콜을 요청하기 위해 syscall
명령어를 호출한다. syscall
을 호출하기 전에 요청할 시스템 콜의 number와 추가적인 arguments를 특정 레지스터에 설정해야 한다.
%rax
는 system call number를 저장한다.- arguments는 순서대로 rdi, rsi, rdx, r10, r8, r9이며, 총 6개까지 가능하다.
함수 리턴값에 대한 x86-64의 규약은 반환값을 RAX 레지스터에 저장하는 것이다. struct intr_frame의 멤버 rax에 저장하면 된다.
syscall in User Program
- syscall(): 유저 프로그램이 커널에게 시스템 콜을 요청하는 함수. 요청할 시스템 콜의 넘버와 추가적인 arguments를 레지스터에 설정하고 실행한다.
// lib/user/syscall.c
// 내부적으로 어셈블리어로 동작한다.
__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(
"mov %1, %%rax\n"
"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;
}
- 래퍼 함수
// lib/user/syscall.c
#define syscall0(NUMBER) ( \
syscall(((uint64_t) NUMBER), 0, 0, 0, 0, 0, 0))
/* Invokes syscall NUMBER, passing argument ARG0, and returns the
return value as an `int'. */
#define syscall1(NUMBER, ARG0) ( \
syscall(((uint64_t) NUMBER), \
((uint64_t) ARG0), 0, 0, 0, 0, 0))
/* Invokes syscall NUMBER, passing arguments ARG0 and ARG1, and
returns the return value as an `int'. */
#define syscall2(NUMBER, ARG0, ARG1) ( \
syscall(((uint64_t) NUMBER), \
((uint64_t) ARG0), \
((uint64_t) ARG1), \
0, 0, 0, 0))
#define syscall3(NUMBER, ARG0, ARG1, ARG2) ( \
syscall(((uint64_t) NUMBER), \
((uint64_t) ARG0), \
((uint64_t) ARG1), \
((uint64_t) ARG2), 0, 0, 0))
#define syscall4(NUMBER, ARG0, ARG1, ARG2, ARG3) ( \
syscall(((uint64_t *) NUMBER), \
((uint64_t) ARG0), \
((uint64_t) ARG1), \
((uint64_t) ARG2), \
((uint64_t) ARG3), 0, 0))
#define syscall5(NUMBER, ARG0, ARG1, ARG2, ARG3, ARG4) ( \
syscall(((uint64_t) NUMBER), \
((uint64_t) ARG0), \
((uint64_t) ARG1), \
((uint64_t) ARG2), \
((uint64_t) ARG3), \
((uint64_t) ARG4), \
0))
- 시스템 콜: 유저 프로그램이 사용하는 시스템 콜은 내부적으로 syscall을 호출하는 방식으로 동작한다. 유저 프로그램은 "시스템 콜을 호출한다"기보다는, "커널에 시스템 콜을 요청한다"고 표현하는 것이 맞는 것 같다.
// lib/user/syscall.c
void
halt (void) {
syscall0 (SYS_HALT);
NOT_REACHED ();
}
void
exit (int status) {
syscall1 (SYS_EXIT, status);
NOT_REACHED ();
}
pid_t
fork (const char *thread_name){
return (pid_t) syscall1 (SYS_FORK, thread_name);
}
int
exec (const char *file) {
return (pid_t) syscall1 (SYS_EXEC, file);
}
System Call 로직
시스템 콜은 유저 프로그램이 운영체제의 기능을 사용하기 위해 호출하는 인터페이스이다. 일반적으로 다음과 같은 로직으로 동작한다.
- 사용자 모드
- 유저 프로그램 실행: 유저 프로그램이 사용자 모드에서 시작된다. 사용자 모드에서는 유저 프로그램이 실행되고, 사용자 모드의 자원과 권한에 제한을 받는다. 유저 프로그램이 최초에 실행될 때, 커널에서는 시스템 콜에 대한 초기화를 수행한다. 이때, 유저 프로그램에서의 시스템 콜 발생 시,
void system_entry(void)
함수를 호출하도록 레지스터에 설정한다. - 시스템 콜 호출: 유저 프로그램이 특정 시스템 콜을 호출하기 위해 해당 시스템 콜 번호와 필요한 인자(argument)를 레지스터에 지정한다. 일반적으로 시스템 콜은 라이브러리 함수 형태로 제공되며, C언어에서는 해당 라이브러리 함수를 호출하여 시스템 콜을 호출한다. 예를 들면,
read()
,write()
,fork()
등이 있다. x86-64 핀토스에서는 각 시스템 콜은 내부적으로syscall()
을 호출하는 하는 방식으로 작동한다.
- 유저 프로그램 실행: 유저 프로그램이 사용자 모드에서 시작된다. 사용자 모드에서는 유저 프로그램이 실행되고, 사용자 모드의 자원과 권한에 제한을 받는다. 유저 프로그램이 최초에 실행될 때, 커널에서는 시스템 콜에 대한 초기화를 수행한다. 이때, 유저 프로그램에서의 시스템 콜 발생 시,
- 커널 모드
- 사용자 모드에서 커널 모드로 전환: 시스템 콜을 호출한 유저 프로그램은 커널 모드로 전환된다. 이는 특권 명령어나 예외 상황 시에 발생하며, 커널 모드에서는 운영체제의 자원과 권환에 접근할 수 있다. 이는 커널 내부에서
void system_entry(void)
라는 어셈블리어 함수에 의해 실행된다. - 시스템 콜 처리: 커널 모드에서 시스템 콜을 처리하는
syscall_handler()
함수가 호출된다.syscall_handler()
함수는 시스템 콜 번호를 확인하여 해당하는 동작(시스템 콜)을 수행한다. 이 동작은 운영체제의 핵심 기능인 File I/O, 프로세스 관리, 메모리 관리 등을 포함할 수 있다. - 시스템 콜 결과 반환:
syscall_handler()
함수는 시스템 콜을 처리한 결과를 유저 프로그램에 반환한다. 이는 유저 프로그램이 호출한 시스템 콜에 따라 다르게 처리될 수 있다. 예를 들면,read()
시스템 콜은 읽은 데이터를 유저 프로그램으로 복사하고,fork()
시스템 콜은 새로운 프로세스를 생성하여 유저 프로그램으로 반환한다. - 커널 모드에서 사용자 모드로 전환: 시스템 콜 처리가 완료되면, 커널 모드에서 다시 사용자 모드로 전환된다. 이후 유저 프로그램은 시스템 콜 호출 이후부터 실행을 계속한다.
- 사용자 모드에서 커널 모드로 전환: 시스템 콜을 호출한 유저 프로그램은 커널 모드로 전환된다. 이는 특권 명령어나 예외 상황 시에 발생하며, 커널 모드에서는 운영체제의 자원과 권환에 접근할 수 있다. 이는 커널 내부에서
syscall_entry(): 어셈블리어
#include "threads/loader.h"
.text
.globl syscall_entry
.type syscall_entry, @function /* functino declaration */
syscall_entry:
movq %rbx, temp1(%rip)
movq %r12, temp2(%rip) /* callee saved registers */
movq %rsp, %rbx /* Store userland rsp */
movabs $tss, %r12
movq (%r12), %r12
movq 4(%r12), %rsp /* Read ring0 rsp from the tss */
/* Now we are in the kernel stack */
push $(SEL_UDSEG) /* if->ss */
push %rbx /* if->rsp */
push %r11 /* if->eflags */
push $(SEL_UCSEG) /* if->cs */
push %rcx /* if->rip */
subq $16, %rsp /* skip error_code, vec_no */
push $(SEL_UDSEG) /* if->ds */
push $(SEL_UDSEG) /* if->es */
...
'프로젝트 > Pint OS' 카테고리의 다른 글
[Pint OS] System Calls (2) (0) | 2023.06.11 |
---|---|
[Pint OS] 에러: missing "begin" message (0) | 2023.06.09 |
[Pint OS] User Memory Access (0) | 2023.06.07 |
[Pint OS] 에러: Kernel panic... thread_yield() (0) | 2023.06.05 |
[Pint OS] Argument Passing (0) | 2023.06.05 |