Implement system call infrastructure.
Implement the system call handler in userprog/syscall.c
.
첫 프로젝트에서 CPU Protection, 즉 운영체제(커널)가 사용자 프로그램으로부터 주도권을 되찾을 수 있는 수단인 Timer 인터럽트를 다루었다. 이 타이머 인터럽트는 타이머 하드웨어에 의해 일어나므로 외부 인터럽트이다.
마찬가지로 OS는 소프트웨어에서 발생하는 예외도 다룰 수 있다. 소프트웨어 예외에는 Page Fault나 0으로 나누는 경우 등이 있을 수 있다. 또 하나 중요한 게 바로 시스템 콜이다.
사용자 프로그램이 시스템 콜을 호출한다.
write (STDOUT_FILENO, buf, strlen (buf));
사용자 프로그램 라이브러리(syscall.c)가 해당 시스템 콜의 넘버, 인자들, 해당 프로그램의 인터럽트 프레임을 정해진 순서대로 레지스터에 채워준다. 그리고 syscall
명령어를 CPU에게 때려준다. 소프트웨어 인터럽트가 발생되면서 CPU가 커널 모드로 전환된다.
userprog/syscall-entry.S
Previllege Level을 커널 모드(Ring 0)으로 올린다. 이 때 원래 Caller 사용자 스택 포인터를 RBX에 저장해둔다.
#include "threads/loader.h"
.text
.globl syscall_entry
.type syscall_entry, @function
syscall_entry:
movq %rbx, temp1(%rip)
movq %r12, temp2(%rip) /* callee saved registers */
movq %rsp, %rbx /* Store userland rsp */
movabs $tss, %r12 // 해당 커널 프로세스의 TSS의 주소를 받아온다.
movq (%r12), %r12
movq 4(%r12), %rsp
// TSS에 저장되어 있던 RSP0(커널 스택 시작주소)의 주소를 RSP로 옮긴다.
CPU와 OS가 자동으로 인터럽트 되기 전 인터럽트 당한 프로세스(커널이든 사용자든)의 Context를 프로세스 자신의 커널 스택에 저장해둔다. 이를 통해 인터럽트가 끝나면 다시 인터럽트 된 Task로 되돌아올 수 있다.
일단 이 밑 5개 레지스터 값은 CPU가 push해주는 레지스터 값이다.
RIP
를 통해 인터럽트 후 다시 되돌아가야 하는 인스트럭션의 주소를 알 수 있다. 또한 RSP
는 커널 스레드의 스택 최상단을 가리킨다(so we can return to the exact chain of function calls when the interrupt happened).
/* Now we are in the kernel stack 여기서 커널 스택으로 바뀐다. */
/* 여기서부터 유저 프로세스의 인터럽트 프레임을 커널 스택에 순서대로 push한다. */
push $(SEL_UDSEG) /* if->ss, 유저 스택 메모리 공간 주소 */
push %rbx /* if->rsp, RBX에 돌아가야 할 유저 메모리의 RSP 값을 저장한다. */
push %r11 /* if->eflags */
push $(SEL_UCSEG) /* if->cs, 유저 코드 세그먼트 메모리 시작 주소 */
push %rcx /* if->rip, RCX에 돌아가야 할 다음 PC 값(RIP)을 저장한다. */
subq $16, %rsp /* skip error_code, vec_no */
OS(intr_enrty)가 커널 스택에 또 추가적인 레지스터 값을 담아준다.
push $(SEL_UDSEG) /* if->ds */
push $(SEL_UDSEG) /* if->es */
push %rax
movq temp1(%rip), %rbx /* 인자들도 커널 스택에 push한다. */
push %rbx
pushq $0
push %rdx
push %rbp
push %rdi
push %rsi
push %r8
push %r9
push %r10
pushq $0 /* skip r11 */
movq temp2(%rip), %r12
push %r12
push %r13
push %r14
push %r15
<aside> 💡 즉, 다시 말해 인터럽트 시 인터럽트 당한 프로세스는 인터럽트 당하기 직전 CPU 레지스터에 담긴 정보들을 자신의 커널 스레드 스택에 담는다.
</aside>
커널 스택에 해당 프로세스의 인터럽트 프레임을 차곡차곡 쌓았으면, RDI를 RSP와 같은 값으로 맞춰줌으로써 바로 전까지 우리가 쌓은 커널 스택의 포인터를 인터럽트 프레임 함수(void intr_handler(struct intr_frame *frame)
)의 argument로 넣어준다.
movq %rsp, %rdi
// 이제 rdi는 rsp가 가리키는 주소를 가리킨다.
// 즉, 앞으로 CALL할 함수의 첫 번째 인자가
// 스택에 쌓여 있는 데이터(인터럽트 프레임)의 시작 주소를 가리킨다.
check_intr:
btsq $9, %r11 /* Check whether we recover the interrupt */
jnb no_sti
sti /* restore interrupt */
인터럽트 프레임으로 인자를 설정했으면 이제 인터럽트 핸들러 함수를 부른다.
no_sti:
movabs $syscall_handler, %r12
call *%r12 // 시스템 콜 핸들러를 부른다!
인터럽트 핸들러가 작업을 처리한 후, 아까 커널 스택에 우리가 쌓았던 인터럽트 프레임에 대한 정보들을 다 레지스터에 집어넣으면서 스택을 지워준다. CPU가 쌓았던 원래 Caller의 레지스터 값들을 다시 원복하는 과정이다.
popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8
popq %rsi
popq %rdi
popq %rbp
popq %rdx
popq %rcx
popq %rbx
popq %rax
addq $32, %rsp
popq %rcx /* if->rip */
addq $8, %rsp
popq %r11 /* if->eflags */
popq %rsp /* if->rsp */
sysretq // 리턴.
.section .data
.globl temp1
temp1:
.quad 0
.globl temp2
temp2:
.quad 0
<aside> 💡 시스템 콜 엔트리와 do_iret()의 차이는? do_iret()은 인터럽트 프레임을 레지스터에 넣어 실행시키고 나면 다시 그 이전 프로세스로 되돌아오지 않는다.
</aside>
x86과는 다르게 x86_64 시스템에서는 syscall
이라는 특별한 명령어를 제공한다. x86에서는 그냥 인터럽트 번호가 int 0x80이다. syscall
명령어는 시스템 콜 핸들러를 부를 수 있는 가장 빠른 방법이다.
사용자 프로그램은 syscall 명령어를 통해 시스템 콜을 보낼 수 있다. 이 때 다른 함수들을 호출할 때와 비슷하게 인자들과 리턴값이 레지스터에 저장되는데, 딱 두 가지가 다르다.
%rax
에는 시스템 콜 넘버가 저장된다(보통의 함수라면 이 자리에 callee의 반환값을 저장한다).%rcx
가 아닌 %r10
에 저장된다(원래 보통의 함수라면 %rdi
, %rsi
, %rdx
, %rcx
, %r8
그리고 %r9
에 저장된다).시스템 콜을 부른 사용자 함수의 레지스터 값은 struct intr_frame
에서 확인할 수 있다.
그리고 리턴값이 있는 시스템 콜의 경우 그 리턴값을 해당 구조체의 rax
를 수정해서 커널이 확인할 수 있다.