[1000줄 OS 구현하기] 유저 모드

2025. 3. 11. 16:49CS/OS

 

 

 

유저 모드 | OS in 1,000 Lines

 

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

 

유저 애플리케이션을 실행해 보자.

 


1. 바이너리 파일로 프로세스 생성

 

  • kernel.h
// 애플리케이션 이미지의 기본 가상 주소입니다. 이는 `user.ld`에 정의된 시작 주소와 일치해야 합니다.
#define USER_BASE 0x1000000

앞서 “[1000줄 OS 구현하기] 애플리케이션”에서 shell.bin.o를 만들었다.

 

shell.o의 메모리 매핑 정보를 날려버렸기 때문에 kernel.h에 따로 주소를 저장한다.

 

  • kernel.c
extern char _binary_shell_bin_start[], _binary_shell_bin_size[];
  • _binary_shell_bin_start[]: shell.bin.o에 포함된 바이너리의 시작 주소를 가리키는 포인터
  • _binary_shell_bin_size[]: shell.bin.o에 포함된 바이너리의 크기

 

  • kernel.c
void user_entry(void) {
    PANIC("not yet implemented");
}

struct process *create_process(const void *image, size_t image_size) {
    /* omitted */
    *--sp = 0;                      // s3
    *--sp = 0;                      // s2
    *--sp = 0;                      // s1
    *--sp = 0;                      // s0
    *--sp = (uint32_t) user_entry;  // ra  // 수정

    uint32_t *page_table = (uint32_t *) alloc_pages(1);

    // Map kernel pages.
    for (paddr_t paddr = (paddr_t) __kernel_base;
         paddr < (paddr_t) __free_ram_end; paddr += PAGE_SIZE)
        map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X);

    // Map user pages.
    for (uint32_t off = 0; off < image_size; off += PAGE_SIZE) {  // 추가
        paddr_t page = alloc_pages(1);  // 추가

        // Handle the case where the data to be copied is smaller than the
        // page size.
        size_t remaining = image_size - off;  // 추가
        size_t copy_size = PAGE_SIZE <= remaining ? PAGE_SIZE : remaining;  // 추가

        // Fill and map the page.
        memcpy((void *) page, image + off, copy_size);  // 추가
        map_page(page_table, USER_BASE + off, page,  // 추가
                 PAGE_U | PAGE_R | PAGE_W | PAGE_X);  // 추가
    }  // 추가
  • *--sp = (uint32_t) user_entry; : 프로세스 시작 시 user_entry() 호출.
  • memcpy((void *) page, image + off, copy_size);
    : 메모리 격리를 위해 복사.
    : 실행 이미지를 직접 매핑하면, 동일한 애플리케이션의 프로세스들이 동일한 물리 페이지를 공유하게 됨.

 

애플리케이션의 바이너리 이미지를 페이지 단위로 반복하면서 메모리를 할당하고 매핑.

 

각 반복마다 새 페이지를 할당하고 이미지 데이터를 할당된 페이지에 복사.

 

사용자 접근 권한(PAGE_U) 및 읽기/쓰기/실행 권한 부여한다.

 

이로써 하나의 프로세스는 커널 페이지와 유저 페이지를 가지게 된다.

 

  • kernel.c
void kernel_main(void) {
    memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);

    printf("\n\n");

    WRITE_CSR(stvec, (uint32_t) kernel_entry);

    idle_proc = create_process(NULL, 0); // 수정
    idle_proc->pid = 0; // idle
    current_proc = idle_proc;

    create_process(_binary_shell_bin_start, (size_t) _binary_shell_bin_size); // 추가

    yield();
    PANIC("switched to idle process");
}

 


2. 사용자 모드로 전환하기

 

애플리케이션을 실행할 때는 U-Mode로 실행한다.

 

이를 위해 프로세서 시작 시 U-Mode로 전환하는 코드를 추가한다.

 

  • kernel.h
#define SSTATUS_SPIE (1 << 5)
  • kernel.c
// ↓ __attribute__((naked)) is very important!
__attribute__((naked)) void user_entry(void) {
    __asm__ __volatile__(
        "csrw sepc, %[sepc]        \n"
        "csrw sstatus, %[sstatus]  \n"
        "sret                      \n"
        :
        : [sepc] "r" (USER_BASE),
          [sstatus] "r" (SSTATUS_SPIE)
    );
}

sret(Supervisor Return) 명령어를 사용하여 S-Mode에서 U-Mode로 전환함.

 

다만, 모드를 변경하기 전에 두 개의 CSR에 값을 기록해야 함.

 

  • sepc
    : Supervisor Exception Program Counter
    : U-Mode로 전환 시 실행할 프로그램 카운터.
    : sret 명령어가 점프할 위치.

  • sstatus 레지스터의 SPIE 비트
    : U-Mode로 진입 시 인터럽트 활성화.
    : 이후 프로세스에서 stvec 레지스터에 설정된 핸들러를 호출할 수 있음.
    : 시스템콜, 예외처리 키보드 입력 등 인터럽트가 처리되지 않을 수 있음.

 

참고로 U-Mode에서 S-Mode로 돌아오려면 시스템 콜(ecall)을 사용해야 한다. (다음 장에서 구현한다)

 


3. 사용자 모드 실행하기

 

shell.c가 단순히 무한 루프를 돌기 때문에 화면상에서 제대로 동작하는지 확인하기 어렵습니다.

(qemu) info registers

CPU#0
 V      =   0
 pc       0100000e

대신 pc를 확인하여 shell.c의 무한 루프에 도달했는지 확인한다.

 

llvm-addr2line -e shell.elf 0x100000e
/home/tiredi/MyOS/13_user_mode/shell.c:4

addr2line을 사용하여 0x1000000e의 위치가 shell.cmain()의 무한루프인지 확인한다.

 

  • shell.c
#include "user.h"

void main(void) {
    *((volatile int *) 0x80200000) = 0x1234; // new!
    for (;;);
}

유저모드에서 커널의 메모리 영역인 0x80200000에 접근해 본다.

 

$ ./run.sh

...

PANIC: kernel.c:130: unexpected trap scause=0000000f, stval=80200000, sepc=01000018

15번째 예외(scause = 0xf = 15)는 "Store/AMO 페이지 폴트"에 해당합니다.

 

Store/AMO 페이지 폴트란?RISC-V 아키텍처에서 발생하는 페이지 폴트의 한 종류입니다:
  • 발생 원인
    • 메모리에 데이터를 저장(Store)하려 할 때 접근 권한이 없거나
    • 해당 가상 주소에 매핑된 물리 페이지가 없는 경우 발생
AMO란?
  • Atomic Memory Operation의 약자로, 원자적 메모리 연산을 의미
  • 메모리의 읽기와 쓰기를 하나의 분할할 수 없는 단위로 수행하는 명령어
위 예제에서는 유저 모드 프로세스가 커널 영역(0x80200000)에 데이터를 쓰려고 시도했기 때문에 Store 페이지 폴트가 발생했습니다.