2025. 3. 11. 16:49ㆍCS/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.c
의 main()
의 무한루프인지 확인한다.
- 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 아키텍처에서 발생하는 페이지 폴트의 한 종류입니다:
|
'CS > OS' 카테고리의 다른 글
[1000줄 OS 구현하기] 시스템 콜 (0) | 2025.03.11 |
---|---|
[1000줄 OS 구현하기] 애플리케이션 (0) | 2025.03.09 |
[1000줄 OS 구현하기] 페이지 테이블 (0) | 2025.03.07 |
[1000줄 OS 구현하기] 프로세스 (0) | 2025.03.06 |
[1000줄 OS 구현하기] 메모리 할당 (0) | 2025.02.11 |