[1000줄 OS 구현하기] 시스템 콜

2025. 3. 11. 21:45CS/OS

 

 

시스템 콜 | OS in 1,000 Lines

 

operating-system-in-1000-lines.vercel.app

 

애플리케이션이 커널 기능을 호출할 때 사용하는 시스템콜을 구현해 보자.

 


1. 시스템 콜 구현

 

User mode에서도 ecall을 통한 시스템 콜 구현하기.

 

  • user.c
int syscall(int sysno, int arg0, int arg1, int arg2) {
    register int a0 __asm__("a0") = arg0;
    register int a1 __asm__("a1") = arg1;
    register int a2 __asm__("a2") = arg2;
    register int a3 __asm__("a3") = sysno;

    __asm__ __volatile__("ecall"
                         : "=r"(a0)
                         : "r"(a0), "r"(a1), "r"(a2), "r"(a3)
                         : "memory");

    return a0;
}
  • : "=r"(a0) : 커널에서 반환하는 값은 a0 레지스터 저장.

 

ecall 명령어가 실행되면 예외 핸들러가 호출되어 제어권이 커널로 넘어갑니다.

 

  • ECALL
    : Environment CALL
    : RISC-V 아키텍처에서 상위 권한 모드에 서비스를 요청하기 위해 예외를 발생시키는 명령어.
더보기

💡 시스템 콜 명령어는 언제 어디서 정의되는가?

시스템 콜 명령어는 주로 CPU 아키텍처에 따라 달라집니다.

아키텍처 시스템 콜 명령어 비고
x86 (32비트) int 0x80, sysenter sysenter가 더 빠름
x86-64 (64비트) syscall 표준 방식
ARM svc ARM 시스템 콜 방식
RISC-V ecall ecall로 트랩 발생
Windows (NT) syscall ntdll.dll을 통해 간접 호출

2. 시스템 콜과 예외

 

가. 시스템 콜을 요청하는데 예외가 발생하는 이유

 

ecall은 RISC-V 아키텍처에서 예외(exception)로 처리된다.

 

RISC-V뿐만 아니라 x86 아키텍처에서는 syscall (Linux) 또는 int 0x2E or syscall (Windows) 명령어를 사용하여 시스템 콜을 구현하는데 이러한 시스템 콜도 RISC-V의 ecall과 유사하게 예외로 처리된다.

 

거의 모든 현대 운영체제에서 user mode의 시스템 콜은 더 상위 권한 모드(커널 모드)에서 처리되는 것이 일반적이다.

 

이는 보안과 시스템 안정성을 위한 기본적인 설계 원칙이다.

 

사용자 모드에서 실행되는 프로그램이 직접 커널의 자원에 접근하면 보안 문제가 발생할 수 있다.

 

이에 권한 상승이 동반되는 작업은 ‘예외’로 인식하고 인터럽트 벡터 테이블이나 트랩 핸들러와 같은 예외 처리 메커니즘을 이용하여 상위 모드로 전환된다.

 


나. RISC-V에서의 예외 처리 과정

 

RISC-V에서 예외가 발생하면 일반적으로…

발생 모드 처리 모드 진입점 레지스터
U-mode S-mode stvec
S-mode M-mode mtvec

이처럼 RISC-V에서는 예외가 발생하면 더 높은 권한을 가진 모드에서 이를 처리하게 됩니다.

 

  • mtvec
    : Machine Trap Vector
    : M-mode (Machine mode)에서 발생하는 예외나 인터럽트를 처리하는 핸들러의 주소를 저장
    : 부팅 시 OpenSBI가 M-Mode 트랩 핸들러 주소를 설정
  • stvec
    : Supervisor Trap Vector
    : S-mode (Supervisor mode)에서 발생하는 예외나 인터럽트를 처리하는 핸들러의 주소를 저장
    : 운영체제 커널에서 예외 처리 핸들러의 진입점을 설정

 

각 레지스터는 해당 권한 모드에서 예외가 발생했을 때 CPU가 점프할 주소를 지정하는 역할 수행.

 


3. 문자열 입출력 시스템 콜

 

가. putchar 시스템 콜

  • common.h
#define SYS_PUTCHAR 1
  • user.c
void putchar(char ch) {
    syscall(SYS_PUTCHAR, ch, 0, 0);
}

kernel.c에는 이미 putchar를 구현했기 때문에 이는 넘어간다.

 


나. getchar 시스템 콜

 

  • common.h
#define SYS_GETCHAR 2
  • user.c
int getchar(void) {
    return syscall(SYS_GETCHAR, 0, 0, 0);
}
  • user.h
int getchar(void);
  • kernel.c
long getchar(void) {
    struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2);
    return ret.error;
}

getchar의 경우 putchar와 다르게 kernel.c에 구현한 적 없어서 추가해야 한다.

 

SBI는 입력이 없으면 -1을 반환한다.

 


4. 커널에서 ecall 명령어 처리

 

커널의 예외 트랩 핸들러에서 getchar, putcharecall 명령을 처리하도록 수정한다.

 

  • kernel.h
#define SCAUSE_ECALL 8
  • kernel.c
void handle_trap(struct trap_frame *f) {
    uint32_t scause = READ_CSR(scause);
    uint32_t stval = READ_CSR(stval);
    uint32_t user_pc = READ_CSR(sepc);
    if (scause == SCAUSE_ECALL) {    // 추가
        handle_syscall(f);    // 추가
        user_pc += 4;    // 추가
    } else {    // 수정
        PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc); // 수정
    }   // 수정

    WRITE_CSR(sepc, user_pc);   // 추가
}
  • if (scause == SCAUSE_ECALL) { : scause8이면 ecall로 인한 예외다.
  • user_pc += 4; : sepc가 예외를 발생시킨 명령어(즉, ecall)를 가리키기 때문, 만약 변경하지 않으면 커널이 동일한 위치로 돌아가 ecall 명령어를 반복 실행하게 됨.

 


5. 시스템 콜 핸들러

 

  • kernel.c
void handle_syscall(struct trap_frame *f) {
    switch (f->a3) {
        case SYS_GETCHAR:
            while (1) {
                long ch = getchar();
                if (ch >= 0) {
                    f->a0 = ch;
                    break;
                }

                yield();
            }
            break;
        case SYS_PUTCHAR:
            putchar(f->a0);
            break;
        default:
            PANIC("unexpected syscall a3=%x\n", f->a3);
    }
}
  • struct trap_frame *f : 예외가 발생했을 때 저장된 "레지스터 값들을 담은 구조체"
  • if (ch >= 0) { : SBI는 입력이 없으면 -1을 반환한다. 입력이 없으면 시스템 콜 종료.
  • yield(); : 입력 중에 다른 프로세스들이 실행될 수 있도록, 다른 프로세스에게 CPU 양보.

 


6. shell 구현하기

 

  • shell.c
void main(void) {
    while (1) {
prompt:
        printf("> ");
        char cmdline[128];
        for (int i = 0;; i++) {
            char ch = getchar();
            putchar(ch);
            if (i == sizeof(cmdline) - 1) {
                printf("command line too long\n");
                goto prompt;
            } else if (ch == '\r') {
                printf("\n");
                cmdline[i] = '\0';
                break;
            } else {
                cmdline[i] = ch;
            }
        }

        if (strcmp(cmdline, "hello") == 0)
            printf("Hello world from shell!\n");
        else
            printf("unknown command: %s\n", cmdline);
    }
}
  • } else if (ch == '\r') {
    : 디버그 콘솔에서 줄 바꿈 문자는 '\r'다.
    : 줄바꿈 문자가 입력될 때까지 문자를 읽어 들임.

  • if (strcmp(cmdline, "hello") == 0)
    : 쉘에 hello 입력 시 "Hello world from shell!\n” 출력

  • cmdline[i] = '\0';
    : 문자열의 끝을 나타내는 널 문자(\0)를 추가
    : strcmp 함수로 명령어를 비교할 때 널 문자가 필요

 

$ ./run.sh

> hello
Hello world from shell!

shell 같은 거 완성.

 


7. 프로세스 종료 시스템 콜

 

이제 프로세스를 종료하는 exit 시스템 콜을 구현해 보자.

 

  • common.h
#define SYS_EXIT 3
  • user.c
__attribute__((noreturn)) void exit(void) {
    syscall(SYS_EXIT, 0 ,0 ,0);
    for(;;); // Just in case!
}
  • kernel.h
#define PROC_EXITED 2
  • kernel.c
void handle_syscall(struct trap_frame *f) {
    switch (f->a3) {
        case SYS_EXIT:
            printf("process %d exited\n", current_proc->pid);
            current_proc->state = PROC_EXITED;
            yield();
            PANIC("unreachable");
        /* omitted */
    }
}

최근 프로세스를 상태를 PROC_EXITED로 변경.

 

yield를 호출해서 스케줄링.

 

교안에서는 단순하게 프로세스 상태만 PROC_EXITED로 변경했지만, 실제 OS에서는 페이지 테이블이나 할당된 메모리 영역과 같이 프로세스가 점유한 자원을 해제해야 한다.

 

  • shell.c
        if (strcmp(cmdline, "hello") == 0)
            printf("Hello world from shell!\n");
        else if (strcmp(cmdline, "exit") == 0)    // 추가
            exit();    // 추가
        else
            printf("unknown command: %s\n", cmdline);

shell에 exit 명령어를 추가한다.

 

$ ./run.sh

> hello
Hello world from shell!
> hello
Hello world from shell!
> asdf
Unkown command: asdf
> exit
process 2 exited
PANIC: kernel.c:357: unreachable

shell에 exit를 입력하면 스케줄러가 idle 프로세스를 선택하며 PANIC을 발생시키면 정상이다.