본문 바로가기

[pintOS] 시스템 콜 과정

@정소민fan2025. 5. 15. 21:41

project2에 돌입했다.

과제 2번은 시스템 콜을 구현하는 것이었다.

pintOS에서 시스템 콜은 어떤 과정을 통해 이루어질까?? 하나씩 따라가보자

 

init.c

먼저 커널이 부팅을 해야한다.

threads/init.c 의 main 함수가 커널 부팅을 시작해준다.

/* Pintos main program. */
int main(void)
{
	uint64_t mem_end;
	char **argv;

	/* Clear BSS and get machine's RAM size. */
	bss_init();

	/* Break command line into arguments and parse options. */
	argv = read_command_line();
	argv = parse_options(argv);
    ....
    	#ifdef USERPROG
		exception_init();
		syscall_init(); // 여기에서 시스템 콜 초기화
	#endif
	/* Start thread scheduler and enable interrupts. */
	thread_start();
	serial_init_queue();
    ...
 }

우린 이 함수에서 #ifdef USERPROG 로 감싸져있는 syscall_init() 를 따라간다.

 

syscall_init

syscall_init 는 userprog/syscall.c 안에 있다. 시스템 콜을 초기화 해주는 역할을 한다

void syscall_init(void)
{
	write_msr(MSR_STAR, ((uint64_t)SEL_UCSEG - 0x10) << 48 |
							((uint64_t)SEL_KCSEG) << 32);
	write_msr(MSR_LSTAR, (uint64_t)syscall_entry);

	/* 인터럽트 서비스 루틴은 syscall_entry가 사용자 스택을 커널 모드 스택으로
	 * 교체할 때까지 어떤 인터럽트도 처리하지 않아야 합니다.
	 * 따라서 FLAG_FL을 마스킹했습니다. */
	write_msr(MSR_SYSCALL_MASK,
			  FLAG_IF | FLAG_TF | FLAG_DF | FLAG_IOPL | FLAG_AC | FLAG_NT);
}

우린 이 함수에서 write_msr(MSR_LSTAR, (uint64_t)syscall_entry);

이 코드에 주목하자.

 

여기서 우리는 두가지 함수에 대해 알아야 한다. 먼저 write_msr 그리고 syscall_entry 에 대해 알아보자

write_msr

이 함수는 MSR(Model-Specific Register)에 값을 기록하는 함수이다.

MSR은 인텔과 AMP CPU에만 존재하는 하드웨어의 특수 기능 제어나 상태 정보를 담는 64비트 레지스터 세트이다.

운영체제가 CPU의 동작 방식을 제어하는데 사용한다고 한다.

저 코드를 해석해보면, MSR_LSTAR 라는 상수를 번호로 가진 MSR 레지스터에 syscall_entry가 있는 주소를 저장하라는 뜻이다.

이후 syscall이 발생하면 CPU가 자동으로 MSR_LSTAR 에 담긴 주소로 점프한다고 한다.

 

그럼 syscall 명령이 발생하면 저 MSR_LSTAR 에 담긴 주소로 점프하는건 대체 어디서 정해진건가? 누가 정해주는건가?

이건 x86-64 아키텍처에서 명세로 약속된 규칙이다. 참고로 32비트 호환 모드에서는 MSR_STAR로 이동한다고 한다.

 

syscall_entry

그럼 이제 이 함수가 무엇인지 알아보자. 일단 안을 까보면

#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
	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 */

어셈블리어로 작성되어있다. 그럼 이 함수가 무슨 일을 하냐?

  • 사용자 프로세스는 현재 유저 스택을 사용하고 있지만, 시스템 콜로 커널로 진입하면 커널 스택으로 스위칭 진행
  • 유저 모드에서 넘어온 CPU 상태들을 저장 (intr_frame에)
  • C에서 시스템 콜 처리 함수 호출 → syscall_handler 호출
  • 시스템 콜 처리 후, 유저 모드로 복귀, 이후 저장해둔 CPU 상태 복원

엥? intr_frame은 또 뭐야?

intr_frame

이 구조체 또한 그냥 넘어갈수 없는 중요한 포인트다.

내부를 또 까보자

intr_frame은 threads/interrupt.h 파일 안에 선언되어 있다.

struct intr_frame {
	/* Pushed by intr_entry in intr-stubs.S.
	   These are the interrupted task's saved registers. */
	struct gp_registers R;
	uint16_t es;
	uint16_t __pad1;
	uint32_t __pad2;
	uint16_t ds;
    ...
}

우리는 여기서 또 gp_registers 구조체를 까봐야 한다

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));

레지스터 값들이 전부 여기 담겨서 intr_frame에 저장되는 것이다 !!

위에서 syscall_entry는 syscall_handler를 호출한다고 했다.

그럼 시스템 콜 처리 함수 syscall_handler는 뭘 하면 될까?

바로 이 intr_frame 안의 gp_registers를 까서 레지스터 값을 꺼내서 처리해주면 된다는 것이다 !!

 

일단 시스템 콜은 각자 번호로 구분되는데, 이는 rax 레지스터에 담긴다. 시스템 콜마다 필요한 인자들이 있는데, 이 인자들은 순서대로 rdi, rsi, rdx, r10, r8, r9 순으로 담겨서 전달된다.

 

그럼 이제 syscall_handler를 보자 !!

syscall_handler

이 함수도 userprog/syscall.c 밑에 존재한다.

/* The main system call interface */
void syscall_handler(struct intr_frame *f UNUSED)
{
	// TODO: Your implementation goes here.
	printf("system call!\n");
	thread_exit();
}

그럼 우린 이 코드에서 어떻게 구현을 해주면 될까?

 

여기까지 읽었으면 간단하다.

void syscall_handler(struct intr_frame *f) {
    uint64_t syscall_num = f->R.rax;     // 시스템 콜 번호
    uint64_t arg1 = f->R.rdi;            // 첫 번째 인자
    uint64_t arg2 = f->R.rsi;            // 두 번째 인자
    uint64_t arg3 = f->R.rdx;            // 세 번째 인자
    // ... 필요하다면 4, 5, 6번째 인자(r10, r8, r9)도 사용

    switch (syscall_num) {
        case SYS_WRITE:
            f->R.rax = sys_write(arg1, (const void*)arg2, (unsigned)arg3);
            break;
        case SYS_EXIT:
            // ...
            break;
        // ... 나머지 시스템 콜도 분기 처리
        default:
            // 알 수 없는 시스템 콜
            break;
    }
}

세상에 이렇게 쉽다니 !! 나는 이제 pintOS의 신이야

 

흐름

  1. init.c의 main 함수로 커널이 부팅되면서 syscall_init 호출
  2. syscall_init가 MSR_LSTAR 레지스터에 syscall_entry 주소 저장
  3. syscall 발생
  4. MSR_LSTAR에 저장되어 있던 syscall_entry 주소로 점프
  5. syscall_handler 실행

아 쉽다 쉬워

'크래프톤 정글' 카테고리의 다른 글

[pintOS] 유저 프로그램 실행 흐름  (0) 2025.05.17
[pintOS] 버그/에러 모음  (0) 2025.05.17
9주차 개발일지  (0) 2025.05.14
8주차 개발일지  (1) 2025.05.07
7주차 개발일지  (0) 2025.04.30
정소민fan
@정소민fan :: 코딩은 관성이야

코딩은 관성적으로 해야합니다 즐거운 코딩 되세요

목차