├── .gitignore ├── LICENCE.md ├── README.md ├── common.c ├── common.h ├── disk ├── hello.txt └── meow.txt ├── epub ├── metadata.xml ├── stylesheet.css └── title.txt ├── kernel.c ├── kernel.h ├── kernel.ld ├── make-epub.sh ├── run.sh ├── screenshot.png ├── shell.c ├── user.c ├── user.h ├── user.ld └── website ├── .gitignore ├── .vitepress ├── config.mts └── theme │ └── index.ts ├── en ├── 01-setting-up-development-environment.md ├── 02-assembly.md ├── 03-overview.md ├── 04-boot.md ├── 05-hello-world.md ├── 06-libc.md ├── 07-kernel-panic.md ├── 08-exception.md ├── 09-memory-allocation.md ├── 10-process.md ├── 11-page-table.md ├── 12-application.md ├── 13-user-mode.md ├── 14-system-call.md ├── 15-virtio-blk.md ├── 16-file-system.md ├── 17-outro.md └── index.md ├── images └── virtio.svg ├── index.md ├── ja ├── 01-setting-up-development-environment.md ├── 02-assembly.md ├── 03-overview.md ├── 04-boot.md ├── 05-hello-world.md ├── 06-libc.md ├── 07-kernel-panic.md ├── 08-exception.md ├── 09-memory-allocation.md ├── 10-process.md ├── 11-page-table.md ├── 12-application.md ├── 13-user-mode.md ├── 14-system-call.md ├── 15-virtio-blk.md ├── 16-file-system.md ├── 17-outro.md └── index.md ├── ko ├── 01-setting-up-development-environment.md ├── 02-assembly.md ├── 03-overview.md ├── 04-boot.md ├── 05-hello-world.md ├── 06-libc.md ├── 07-kernel-panic.md ├── 08-exception.md ├── 09-memory-allocation.md ├── 10-process.md ├── 11-page-table.md ├── 12-application.md ├── 13-user-mode.md ├── 14-system-call.md ├── 15-virtio-blk.md ├── 16-file-system.md ├── 17-outro.md └── index.md ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── vercel.json └── zh ├── 01-setting-up-development-environment.md ├── 02-assembly.md ├── 03-overview.md ├── 04-boot.md ├── 05-hello-world.md ├── 06-libc.md ├── 07-kernel-panic.md ├── 08-exception.md ├── 09-memory-allocation.md ├── 10-process.md ├── 11-page-table.md ├── 12-application.md ├── 13-user-mode.md ├── 14-system-call.md ├── 15-virtio-blk.md ├── 16-file-system.md ├── 17-outro.md └── index.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.map 2 | *.tar 3 | *.o 4 | *.elf 5 | *.bin 6 | *.log 7 | *.pcap 8 | *.epub 9 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | - `*.md` in `website` directory: [CC BY 4.0 license](https://creativecommons.jp/faq). 4 | - Everything else: [MIT license](https://opensource.org/licenses/MIT). 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Writing an Operating System in 1,000 Lines 2 | 3 | **[English](https://operating-system-in-1000-lines.vercel.app/en)** ∙ **[日本語](https://operating-system-in-1000-lines.vercel.app/ja/)** ∙ **[简体中文](https://operating-system-in-1000-lines.vercel.app/zh/)** *(translated by [@YianAndCode](https://github.com/YianAndCode))* ∙ **[한국어](https://operating-system-in-1000-lines.vercel.app/ko/)** *(translated by [@JoHwanhee](https://github.com/JoHwanhee))* 4 | 5 | ![Operating System in 1,000 Lines](./screenshot.png) 6 | 7 | This repository contains the source code for the website [Operating System in 1,000 Lines](https://operating-system-in-1000-lines.vercel.app/), and a reference implementation. 8 | 9 | ## More interesting implementations 10 | 11 | The book only covers the basics of an operating system. You can do more with the knowledge you have gained. Here are some ideas: 12 | 13 | | Implementation | Author | 14 | | --- | --- | 15 | | [Shutdown command](https://github.com/nuta/operating-system-in-1000-lines/pull/59/files) | [@calvera](https://github.com/calvera) | 16 | 17 | Let me know if you have implemented something interesting! 18 | -------------------------------------------------------------------------------- /common.c: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | 3 | void *memset(void *buf, char c, size_t n) { 4 | uint8_t *p = (uint8_t *) buf; 5 | while (n--) 6 | *p++ = c; 7 | return buf; 8 | } 9 | 10 | void *memcpy(void *dst, const void *src, size_t n) { 11 | uint8_t *d = (uint8_t *) dst; 12 | const uint8_t *s = (const uint8_t *) src; 13 | while (n--) 14 | *d++ = *s++; 15 | return dst; 16 | } 17 | 18 | char *strcpy(char *dst, const char *src) { 19 | char *d = dst; 20 | while (*src) 21 | *d++ = *src++; 22 | *d = '\0'; 23 | return dst; 24 | } 25 | 26 | int strcmp(const char *s1, const char *s2) { 27 | while (*s1 && *s2) { 28 | if (*s1 != *s2) 29 | break; 30 | s1++; 31 | s2++; 32 | } 33 | 34 | return *(unsigned char *)s1 - *(unsigned char *)s2; 35 | } 36 | 37 | void putchar(char ch); 38 | 39 | void printf(const char *fmt, ...) { 40 | va_list vargs; 41 | va_start(vargs, fmt); 42 | while (*fmt) { 43 | if (*fmt == '%') { 44 | fmt++; 45 | switch (*fmt) { 46 | case '\0': 47 | putchar('%'); 48 | goto end; 49 | case '%': 50 | putchar('%'); 51 | break; 52 | case 's': { 53 | const char *s = va_arg(vargs, const char *); 54 | while (*s) { 55 | putchar(*s); 56 | s++; 57 | } 58 | break; 59 | } 60 | case 'd': { 61 | int value = va_arg(vargs, int); 62 | unsigned magnitude = value; 63 | if (value < 0) { 64 | putchar('-'); 65 | magnitude = -magnitude; 66 | } 67 | 68 | unsigned divisor = 1; 69 | while (magnitude / divisor > 9) 70 | divisor *= 10; 71 | 72 | while (divisor > 0) { 73 | putchar('0' + magnitude / divisor); 74 | magnitude %= divisor; 75 | divisor /= 10; 76 | } 77 | 78 | break; 79 | } 80 | case 'x': { 81 | unsigned value = va_arg(vargs, unsigned); 82 | for (int i = 7; i >= 0; i--) { 83 | unsigned nibble = (value >> (i * 4)) & 0xf; 84 | putchar("0123456789abcdef"[nibble]); 85 | } 86 | } 87 | } 88 | } else { 89 | putchar(*fmt); 90 | } 91 | 92 | fmt++; 93 | } 94 | 95 | end: 96 | va_end(vargs); 97 | } 98 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef int bool; 4 | typedef unsigned char uint8_t; 5 | typedef unsigned short uint16_t; 6 | typedef unsigned int uint32_t; 7 | typedef unsigned long long uint64_t; 8 | typedef uint32_t size_t; 9 | typedef uint32_t paddr_t; 10 | typedef uint32_t vaddr_t; 11 | 12 | #define true 1 13 | #define false 0 14 | #define NULL ((void *) 0) 15 | #define align_up(value, align) __builtin_align_up(value, align) 16 | #define is_aligned(value, align) __builtin_is_aligned(value, align) 17 | #define offsetof(type, member) __builtin_offsetof(type, member) 18 | #define va_list __builtin_va_list 19 | #define va_start __builtin_va_start 20 | #define va_end __builtin_va_end 21 | #define va_arg __builtin_va_arg 22 | #define PAGE_SIZE 4096 23 | #define SYS_PUTCHAR 1 24 | #define SYS_GETCHAR 2 25 | #define SYS_EXIT 3 26 | #define SYS_READFILE 4 27 | #define SYS_WRITEFILE 5 28 | 29 | void *memset(void *buf, char c, size_t n); 30 | void *memcpy(void *dst, const void *src, size_t n); 31 | char *strcpy(char *dst, const char *src); 32 | int strcmp(const char *s1, const char *s2); 33 | void printf(const char *fmt, ...); 34 | -------------------------------------------------------------------------------- /disk/hello.txt: -------------------------------------------------------------------------------- 1 | Can you see me? Ah, there you are! You've unlocked the achievement "Virtio Newbie!" -------------------------------------------------------------------------------- /disk/meow.txt: -------------------------------------------------------------------------------- 1 | meow! 2 | -------------------------------------------------------------------------------- /epub/metadata.xml: -------------------------------------------------------------------------------- 1 | Writing an Operating System in 1,000 Lines, v0.1-alpha 2 | en-US 3 | Seiya Nuta 4 | seiya.me 5 | 2025-01-09 6 | Copyright © 2025 Seiya Nuta 7 | -------------------------------------------------------------------------------- /epub/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* wrap long lines of code */ 2 | pre, code { 3 | white-space: pre-wrap !important; 4 | } 5 | 6 | 7 | /* for all alerts */ 8 | .title { 9 | font-weight: bold; 10 | font-size: 150%; 11 | margin-left: 10%; 12 | } 13 | 14 | 15 | /* for notes */ 16 | .note::before { 17 | content: "📝"; 18 | float: left; 19 | font-size: 150%; 20 | } 21 | 22 | .note { 23 | margin-left: 10%; 24 | } 25 | 26 | 27 | /* for tips */ 28 | .tip::before { 29 | content: "💡"; 30 | float: left; 31 | font-size: 150%; 32 | } 33 | 34 | .tip { 35 | margin-left: 10%; 36 | } 37 | 38 | 39 | /* for warnings */ 40 | .warning::before { 41 | content: "❓"; 42 | float: left; 43 | font-size: 150%; 44 | } 45 | 46 | .warning { 47 | margin-left: 10%; 48 | } 49 | -------------------------------------------------------------------------------- /epub/title.txt: -------------------------------------------------------------------------------- 1 | % Writing an Operating System in 1,000 Lines, v0.1-alpha 2 | % Seiya Nuta 3 | -------------------------------------------------------------------------------- /kernel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "common.h" 3 | 4 | #define PROCS_MAX 8 5 | #define PROC_UNUSED 0 6 | #define PROC_RUNNABLE 1 7 | #define PROC_EXITED 2 8 | #define SATP_SV32 (1u << 31) 9 | #define SSTATUS_SPIE (1 << 5) 10 | #define SSTATUS_SUM (1 << 18) 11 | #define SCAUSE_ECALL 8 12 | #define PAGE_V (1 << 0) 13 | #define PAGE_R (1 << 1) 14 | #define PAGE_W (1 << 2) 15 | #define PAGE_X (1 << 3) 16 | #define PAGE_U (1 << 4) 17 | #define USER_BASE 0x1000000 18 | #define FILES_MAX 2 19 | #define DISK_MAX_SIZE align_up(sizeof(struct file) * FILES_MAX, SECTOR_SIZE) 20 | #define SECTOR_SIZE 512 21 | #define VIRTQ_ENTRY_NUM 16 22 | #define VIRTIO_DEVICE_BLK 2 23 | #define VIRTIO_BLK_PADDR 0x10001000 24 | #define VIRTIO_REG_MAGIC 0x00 25 | #define VIRTIO_REG_VERSION 0x04 26 | #define VIRTIO_REG_DEVICE_ID 0x08 27 | #define VIRTIO_REG_QUEUE_SEL 0x30 28 | #define VIRTIO_REG_QUEUE_NUM_MAX 0x34 29 | #define VIRTIO_REG_QUEUE_NUM 0x38 30 | #define VIRTIO_REG_QUEUE_ALIGN 0x3c 31 | #define VIRTIO_REG_QUEUE_PFN 0x40 32 | #define VIRTIO_REG_QUEUE_NOTIFY 0x50 33 | #define VIRTIO_REG_DEVICE_STATUS 0x70 34 | #define VIRTIO_REG_DEVICE_CONFIG 0x100 35 | #define VIRTIO_STATUS_ACK 1 36 | #define VIRTIO_STATUS_DRIVER 2 37 | #define VIRTIO_STATUS_DRIVER_OK 4 38 | #define VIRTIO_STATUS_FEAT_OK 8 39 | #define VIRTQ_DESC_F_NEXT 1 40 | #define VIRTQ_DESC_F_WRITE 2 41 | #define VIRTQ_AVAIL_F_NO_INTERRUPT 1 42 | #define VIRTIO_BLK_T_IN 0 43 | #define VIRTIO_BLK_T_OUT 1 44 | 45 | struct process { 46 | int pid; // 0 if it's an idle process 47 | int state; // PROC_UNUSED, PROC_RUNNABLE, PROC_EXITED 48 | vaddr_t sp; // kernel stack pointer 49 | uint32_t *page_table; // points to first level page table 50 | uint8_t stack[8192]; // kernel stack 51 | }; 52 | 53 | struct sbiret { 54 | long error; 55 | long value; 56 | }; 57 | 58 | struct trap_frame { 59 | uint32_t ra; 60 | uint32_t gp; 61 | uint32_t tp; 62 | uint32_t t0; 63 | uint32_t t1; 64 | uint32_t t2; 65 | uint32_t t3; 66 | uint32_t t4; 67 | uint32_t t5; 68 | uint32_t t6; 69 | uint32_t a0; 70 | uint32_t a1; 71 | uint32_t a2; 72 | uint32_t a3; 73 | uint32_t a4; 74 | uint32_t a5; 75 | uint32_t a6; 76 | uint32_t a7; 77 | uint32_t s0; 78 | uint32_t s1; 79 | uint32_t s2; 80 | uint32_t s3; 81 | uint32_t s4; 82 | uint32_t s5; 83 | uint32_t s6; 84 | uint32_t s7; 85 | uint32_t s8; 86 | uint32_t s9; 87 | uint32_t s10; 88 | uint32_t s11; 89 | uint32_t sp; 90 | } __attribute__((packed)); 91 | 92 | struct virtq_desc { 93 | uint64_t addr; 94 | uint32_t len; 95 | uint16_t flags; 96 | uint16_t next; 97 | } __attribute__((packed)); 98 | 99 | struct virtq_avail { 100 | uint16_t flags; 101 | uint16_t index; 102 | uint16_t ring[VIRTQ_ENTRY_NUM]; 103 | } __attribute__((packed)); 104 | 105 | struct virtq_used_elem { 106 | uint32_t id; 107 | uint32_t len; 108 | } __attribute__((packed)); 109 | 110 | struct virtq_used { 111 | uint16_t flags; 112 | uint16_t index; 113 | struct virtq_used_elem ring[VIRTQ_ENTRY_NUM]; 114 | } __attribute__((packed)); 115 | 116 | struct virtio_virtq { 117 | struct virtq_desc descs[VIRTQ_ENTRY_NUM]; 118 | struct virtq_avail avail; 119 | struct virtq_used used __attribute__((aligned(PAGE_SIZE))); 120 | int queue_index; 121 | volatile uint16_t *used_index; 122 | uint16_t last_used_index; 123 | } __attribute__((packed)); 124 | 125 | struct virtio_blk_req { 126 | uint32_t type; 127 | uint32_t reserved; 128 | uint64_t sector; 129 | uint8_t data[512]; 130 | uint8_t status; 131 | } __attribute__((packed)); 132 | 133 | struct tar_header { 134 | char name[100]; 135 | char mode[8]; 136 | char uid[8]; 137 | char gid[8]; 138 | char size[12]; 139 | char mtime[12]; 140 | char checksum[8]; 141 | char type; 142 | char linkname[100]; 143 | char magic[6]; 144 | char version[2]; 145 | char uname[32]; 146 | char gname[32]; 147 | char devmajor[8]; 148 | char devminor[8]; 149 | char prefix[155]; 150 | char padding[12]; 151 | char data[]; 152 | } __attribute__((packed)); 153 | 154 | struct file { 155 | bool in_use; 156 | char name[100]; 157 | char data[1024]; 158 | size_t size; 159 | }; 160 | 161 | #define READ_CSR(reg) \ 162 | ({ \ 163 | unsigned long __tmp; \ 164 | __asm__ __volatile__("csrr %0, " #reg : "=r"(__tmp)); \ 165 | __tmp; \ 166 | }) 167 | 168 | #define WRITE_CSR(reg, value) \ 169 | do { \ 170 | uint32_t __tmp = (value); \ 171 | __asm__ __volatile__("csrw " #reg ", %0" ::"r"(__tmp)); \ 172 | } while (0) 173 | 174 | #define PANIC(fmt, ...) \ 175 | do { \ 176 | printf("PANIC: %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 177 | while (1) {} \ 178 | } while (0) 179 | -------------------------------------------------------------------------------- /kernel.ld: -------------------------------------------------------------------------------- 1 | ENTRY(boot) 2 | 3 | SECTIONS { 4 | . = 0x80200000; 5 | __kernel_base = .; 6 | 7 | .text :{ 8 | KEEP(*(.text.boot)); 9 | *(.text .text.*); 10 | } 11 | 12 | .rodata : ALIGN(4) { 13 | *(.rodata .rodata.*); 14 | } 15 | 16 | .data : ALIGN(4) { 17 | *(.data .data.*); 18 | } 19 | 20 | .bss : ALIGN(4) { 21 | __bss = .; 22 | *(.bss .bss.* .sbss .sbss.*); 23 | __bss_end = .; 24 | } 25 | 26 | . = ALIGN(4); 27 | . += 128 * 1024; /* 128KB */ 28 | __stack_top = .; 29 | 30 | . = ALIGN(4096); 31 | __free_ram = .; 32 | . += 64 * 1024 * 1024; /* 64MB */ 33 | __free_ram_end = .; 34 | } 35 | -------------------------------------------------------------------------------- /make-epub.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | cd website/en 5 | pandoc \ 6 | -f gfm+alerts \ 7 | -o ../../"Writing an OS in 1,000 Lines, v0.1.1-alpha.epub" \ 8 | ../../epub/title.txt \ 9 | {index.md,0*.md,1*.md} \ 10 | --css=../../epub/stylesheet.css \ 11 | --epub-metadata=../../epub/metadata.xml \ 12 | --table-of-contents 13 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xue 3 | 4 | QEMU=qemu-system-riscv32 5 | CC=/opt/homebrew/opt/llvm/bin/clang 6 | OBJCOPY=/opt/homebrew/opt/llvm/bin/llvm-objcopy 7 | 8 | CFLAGS="-std=c11 -O2 -g3 -Wall -Wextra --target=riscv32-unknown-elf -fno-stack-protector -ffreestanding -nostdlib" 9 | 10 | # Build the shell. 11 | $CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf shell.c user.c common.c 12 | $OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin 13 | $OBJCOPY -Ibinary -Oelf32-littleriscv shell.bin shell.bin.o 14 | 15 | # Build the kernel. 16 | $CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf \ 17 | kernel.c common.c shell.bin.o 18 | 19 | (cd disk && tar cf ../disk.tar --format=ustar *.txt) 20 | 21 | $QEMU -machine virt -bios default -nographic -serial mon:stdio --no-reboot \ 22 | -d unimp,guest_errors,int,cpu_reset -D qemu.log \ 23 | -drive id=drive0,file=disk.tar,format=raw,if=none \ 24 | -device virtio-blk-device,drive=drive0,bus=virtio-mmio-bus.0 \ 25 | -kernel kernel.elf 26 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuta/operating-system-in-1000-lines/33bf69c1841c759e0a102b5a51364dbff919f987/screenshot.png -------------------------------------------------------------------------------- /shell.c: -------------------------------------------------------------------------------- 1 | #include "user.h" 2 | 3 | void main(void) { 4 | while (1) { 5 | prompt: 6 | printf("> "); 7 | char cmdline[128]; 8 | for (int i = 0;; i++) { 9 | char ch = getchar(); 10 | putchar(ch); 11 | if (i == sizeof(cmdline) - 1) { 12 | printf("command line too long\n"); 13 | goto prompt; 14 | } else if (ch == '\r') { 15 | printf("\n"); 16 | cmdline[i] = '\0'; 17 | break; 18 | } else { 19 | cmdline[i] = ch; 20 | } 21 | } 22 | 23 | if (strcmp(cmdline, "hello") == 0) 24 | printf("Hello world from shell!\n"); 25 | else if (strcmp(cmdline, "exit") == 0) 26 | exit(); 27 | else if (strcmp(cmdline, "readfile") == 0) { 28 | char buf[128]; 29 | int len = readfile("hello.txt", buf, sizeof(buf)); 30 | buf[len] = '\0'; 31 | printf("%s\n", buf); 32 | } 33 | else if (strcmp(cmdline, "writefile") == 0) 34 | writefile("hello.txt", "Hello from shell!\n", 19); 35 | else 36 | printf("unknown command: %s\n", cmdline); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /user.c: -------------------------------------------------------------------------------- 1 | #include "user.h" 2 | 3 | extern char __stack_top[]; 4 | 5 | int syscall(int sysno, int arg0, int arg1, int arg2) { 6 | register int a0 __asm__("a0") = arg0; 7 | register int a1 __asm__("a1") = arg1; 8 | register int a2 __asm__("a2") = arg2; 9 | register int a3 __asm__("a3") = sysno; 10 | 11 | __asm__ __volatile__("ecall" 12 | : "=r"(a0) 13 | : "r"(a0), "r"(a1), "r"(a2), "r"(a3) 14 | : "memory"); 15 | 16 | return a0; 17 | } 18 | 19 | void putchar(char ch) { 20 | syscall(SYS_PUTCHAR, ch, 0, 0); 21 | } 22 | 23 | int getchar(void) { 24 | return syscall(SYS_GETCHAR, 0, 0, 0); 25 | } 26 | 27 | int readfile(const char *filename, char *buf, int len) { 28 | return syscall(SYS_READFILE, (int) filename, (int) buf, len); 29 | } 30 | 31 | int writefile(const char *filename, const char *buf, int len) { 32 | return syscall(SYS_WRITEFILE, (int) filename, (int) buf, len); 33 | } 34 | 35 | __attribute__((noreturn)) void exit(void) { 36 | syscall(SYS_EXIT, 0, 0, 0); 37 | for (;;); 38 | } 39 | 40 | __attribute__((section(".text.start"))) 41 | __attribute__((naked)) 42 | void start(void) { 43 | __asm__ __volatile__( 44 | "mv sp, %[stack_top]\n" 45 | "call main\n" 46 | "call exit\n" ::[stack_top] "r"(__stack_top)); 47 | } 48 | -------------------------------------------------------------------------------- /user.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "common.h" 3 | 4 | struct sysret { 5 | int a0; 6 | int a1; 7 | int a2; 8 | }; 9 | 10 | void putchar(char ch); 11 | int getchar(void); 12 | int readfile(const char *filename, char *buf, int len); 13 | int writefile(const char *filename, const char *buf, int len); 14 | __attribute__((noreturn)) void exit(void); 15 | -------------------------------------------------------------------------------- /user.ld: -------------------------------------------------------------------------------- 1 | ENTRY(start) 2 | 3 | SECTIONS { 4 | . = 0x1000000; 5 | 6 | .text :{ 7 | KEEP(*(.text.start)); 8 | *(.text .text.*); 9 | } 10 | 11 | .rodata : ALIGN(4) { 12 | *(.rodata .rodata.*); 13 | } 14 | 15 | .data : ALIGN(4) { 16 | *(.data .data.*); 17 | } 18 | 19 | .bss : ALIGN(4) { 20 | *(.bss .bss.* .sbss .sbss.*); 21 | 22 | . = ALIGN(16); /* https://github.com/nuta/operating-system-in-1000-lines/pull/23 */ 23 | . += 64 * 1024; /* 64KB */ 24 | __stack_top = .; 25 | 26 | ASSERT(. < 0x1800000, "too large executable"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.vitepress/cache 3 | /.vitepress/dist 4 | -------------------------------------------------------------------------------- /website/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { 3 | groupIconMdPlugin, 4 | groupIconVitePlugin, 5 | } from 'vitepress-plugin-group-icons' 6 | 7 | 8 | // https://vitepress.dev/reference/site-config 9 | export default defineConfig({ 10 | title: "OS in 1,000 Lines", 11 | description: "Write your first operating system from scratch, in 1K LoC.", 12 | cleanUrls: true, 13 | markdown: { 14 | config(md) { 15 | md.use(groupIconMdPlugin) 16 | }, 17 | }, 18 | vite: { 19 | plugins: [ 20 | groupIconVitePlugin() 21 | ], 22 | }, 23 | locales: { 24 | ko: { 25 | label: '한국어', 26 | lang: 'ko', 27 | themeConfig: { 28 | sidebar: [ 29 | { 30 | text: '목차', 31 | items: [ 32 | { link: '/ko/', text: '00. 들어가며' }, 33 | { link: '/ko/01-setting-up-development-environment', text: '01. 환경설정' }, 34 | { link: '/ko/02-assembly', text: '02. RISC-V 입문' }, 35 | { link: '/ko/03-overview', text: '03. 기능 및 구조 미리보기' }, 36 | { link: '/ko/04-boot', text: '04. 부트' }, 37 | { link: '/ko/05-hello-world', text: '05. Hello World!' }, 38 | { link: '/ko/06-libc', text: '06. C 표준 라이브러리' }, 39 | { link: '/ko/07-kernel-panic', text: '07. 커널 패닉' }, 40 | { link: '/ko/08-exception', text: '08. 예외' }, 41 | { link: '/ko/09-memory-allocation', text: '09. 메모리 할당' }, 42 | { link: '/ko/10-process', text: '10. 프로세스' }, 43 | { link: '/ko/11-page-table', text: '11. 페이지 테이블' }, 44 | { link: '/ko/12-application', text: '12. 애플리케이션' }, 45 | { link: '/ko/13-user-mode', text: '13. 유저 모드' }, 46 | { link: '/ko/14-system-call', text: '14. 시스템 콜' }, 47 | { link: '/ko/15-virtio-blk', text: '15. 디스크 I/O' }, 48 | { link: '/ko/16-file-system', text: '16. 파일 시스템' }, 49 | { link: '/ko/17-outro', text: '17. 끝내며' }, 50 | ] 51 | }, 52 | { 53 | text: 'Links', 54 | items: [ 55 | { link: 'https://github.com/nuta/operating-system-in-1000-lines', text: 'GitHub repository' }, 56 | { link: 'https://www.hanmoto.com/bd/isbn/9784798068718', text: 'Design and Implementation of Microkernels' }, 57 | { link: '/ja', text: '日本語版' }, 58 | { link: '/en', text: 'English version' }, 59 | ] 60 | }, 61 | ], 62 | socialLinks: [ 63 | { icon: 'github', link: 'https://github.com/nuta/operating-system-in-1000-lines' } 64 | ] 65 | } 66 | }, 67 | en: { 68 | label: 'English', 69 | lang: 'en', 70 | themeConfig: { 71 | sidebar: [ 72 | { 73 | text: 'Table of Contents', 74 | items: [ 75 | { link: '/en/', text: '00. Intro' }, 76 | { link: '/en/01-setting-up-development-environment', text: '01. Getting Started' }, 77 | { link: '/en/02-assembly', text: '02. RISC-V 101' }, 78 | { link: '/en/03-overview', text: '03. Overview' }, 79 | { link: '/en/04-boot', text: '04. Boot' }, 80 | { link: '/en/05-hello-world', text: '05. Hello World!' }, 81 | { link: '/en/06-libc', text: '06. C Standard Library' }, 82 | { link: '/en/07-kernel-panic', text: '07. Kernel Panic' }, 83 | { link: '/en/08-exception', text: '08. Exception' }, 84 | { link: '/en/09-memory-allocation', text: '09. Memory Allocation' }, 85 | { link: '/en/10-process', text: '10. Process' }, 86 | { link: '/en/11-page-table', text: '11. Page Table' }, 87 | { link: '/en/12-application', text: '12. Application' }, 88 | { link: '/en/13-user-mode', text: '13. User Mode' }, 89 | { link: '/en/14-system-call', text: '14. System Call' }, 90 | { link: '/en/15-virtio-blk', text: '15. Disk I/O' }, 91 | { link: '/en/16-file-system', text: '16. File System' }, 92 | { link: '/en/17-outro', text: '17. Outro' }, 93 | ] 94 | }, 95 | { 96 | text: 'Links', 97 | items: [ 98 | { link: 'https://github.com/nuta/operating-system-in-1000-lines', text: 'GitHub repository' }, 99 | { link: 'https://www.hanmoto.com/bd/isbn/9784798068718', text: 'Design and Implementation of Microkernels' }, 100 | { link: '/ja', text: '日本語版' }, 101 | { link: '/zh', text: '简体中文版' }, 102 | { link: '/ko', text: '한국어판' }, 103 | ] 104 | }, 105 | ], 106 | socialLinks: [ 107 | { icon: 'github', link: 'https://github.com/nuta/operating-system-in-1000-lines' } 108 | ] 109 | } 110 | }, 111 | ja: { 112 | label: '日本語', 113 | lang: 'ja', 114 | themeConfig: { 115 | sidebar: [ 116 | { 117 | text: '目次', 118 | items: [ 119 | { link: '/ja/', text: '00. はじめに' }, 120 | { link: '/ja/01-setting-up-development-environment', text: '01. 開発環境' }, 121 | { link: '/ja/02-assembly', text: '02. RISC-V入門' }, 122 | { link: '/ja/03-overview', text: '03. OSの全体像' }, 123 | { link: '/ja/04-boot', text: '04. ブート' }, 124 | { link: '/ja/05-hello-world', text: '05. Hello World!' }, 125 | { link: '/ja/06-libc', text: '06. C標準ライブラリ' }, 126 | { link: '/ja/07-kernel-panic', text: '07. カーネルパニック' }, 127 | { link: '/ja/08-exception', text: '08. 例外処理' }, 128 | { link: '/ja/09-memory-allocation', text: '09. メモリ割り当て' }, 129 | { link: '/ja/10-process', text: '10. プロセス' }, 130 | { link: '/ja/11-page-table', text: '11. ページテーブル' }, 131 | { link: '/ja/12-application', text: '12. アプリケーション' }, 132 | { link: '/ja/13-user-mode', text: '13. ユーザーモード' }, 133 | { link: '/ja/14-system-call', text: '14. システムコール' }, 134 | { link: '/ja/15-virtio-blk', text: '15. ディスク読み書き' }, 135 | { link: '/ja/16-file-system', text: '16. ファイルシステム' }, 136 | { link: '/ja/17-outro', text: '17. おわりに' }, 137 | ] 138 | }, 139 | { 140 | text: 'リンク', 141 | items: [ 142 | { link: '/en', text: 'English version' }, 143 | { link: '/zh', text: '简体中文版' }, 144 | { link: 'https://github.com/nuta/operating-system-in-1000-lines', text: 'GitHubリポジトリ' }, 145 | { link: 'https://www.hanmoto.com/bd/isbn/9784798068718', text: 'マイクロカーネル本' }, 146 | { link: '/ko', text: '한국어판' }, 147 | ] 148 | }, 149 | ], 150 | socialLinks: [ 151 | { icon: 'github', link: 'https://github.com/nuta/operating-system-in-1000-lines' } 152 | ] 153 | } 154 | }, 155 | zh: { 156 | label: '简体中文', 157 | lang: 'zh', 158 | themeConfig: { 159 | sidebar: [ 160 | { 161 | text: '目录', 162 | items: [ 163 | { link: '/zh/', text: '00. 简介' }, 164 | { link: '/zh/01-setting-up-development-environment', text: '01. 入门' }, 165 | { link: '/zh/02-assembly', text: '02. RISC-V 101' }, 166 | { link: '/zh/03-overview', text: '03. 总览' }, 167 | { link: '/zh/04-boot', text: '04. 引导' }, 168 | { link: '/zh/05-hello-world', text: '05. Hello World!' }, 169 | { link: '/zh/06-libc', text: '06. C 标准库' }, 170 | { link: '/zh/07-kernel-panic', text: '07. 内核恐慌(Kernel Panic)' }, 171 | { link: '/zh/08-exception', text: '08. 异常' }, 172 | { link: '/zh/09-memory-allocation', text: '09. 内存分配' }, 173 | { link: '/zh/10-process', text: '10. 进程' }, 174 | { link: '/zh/11-page-table', text: '11. 页表' }, 175 | { link: '/zh/12-application', text: '12. 应用程序' }, 176 | { link: '/zh/13-user-mode', text: '13. 用户模式' }, 177 | { link: '/zh/14-system-call', text: '14. 系统调用' }, 178 | { link: '/zh/15-virtio-blk', text: '15. 磁盘 I/O' }, 179 | { link: '/zh/16-file-system', text: '16. 文件系统' }, 180 | { link: '/zh/17-outro', text: '17. 结语' }, 181 | ] 182 | }, 183 | { 184 | text: 'Links', 185 | items: [ 186 | { link: 'https://github.com/nuta/operating-system-in-1000-lines', text: 'GitHub repository' }, 187 | { link: '/en', text: 'English version' }, 188 | { link: '/ja', text: '日本語版' }, 189 | ] 190 | }, 191 | ], 192 | socialLinks: [ 193 | { icon: 'github', link: 'https://github.com/nuta/operating-system-in-1000-lines' } 194 | ] 195 | } 196 | } 197 | }, 198 | }) 199 | -------------------------------------------------------------------------------- /website/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from 'vitepress/theme' 2 | import 'virtual:group-icons.css' 3 | 4 | export default Theme 5 | -------------------------------------------------------------------------------- /website/en/01-setting-up-development-environment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | # Getting Started 6 | 7 | This book assumes you're using a UNIX or UNIX like OS such as macOS or Ubuntu. If you're on Windows, install Windows Subsystem for Linux (WSL2) and follow the Ubuntu instructions. 8 | 9 | ## Install development tools 10 | 11 | ### macOS 12 | 13 | Install [Homebrew](https://brew.sh) and run this command to get all tools you need: 14 | 15 | ``` 16 | brew install llvm lld qemu 17 | ``` 18 | 19 | Also, you need to add LLVM binutils to your `PATH`: 20 | 21 | ``` 22 | $ export PATH="$PATH:$(brew --prefix)/opt/llvm/bin" 23 | $ which llvm-objcopy 24 | /opt/homebrew/opt/llvm/bin/llvm-objcopy 25 | ``` 26 | 27 | ### Ubuntu 28 | 29 | Install packages with `apt`: 30 | 31 | ``` 32 | sudo apt update && sudo apt install -y clang llvm lld qemu-system-riscv32 curl 33 | ``` 34 | 35 | Also, download OpenSBI (think of it as BIOS/UEFI for PCs): 36 | 37 | ``` 38 | curl -LO https://github.com/qemu/qemu/raw/v8.0.4/pc-bios/opensbi-riscv32-generic-fw_dynamic.bin 39 | ``` 40 | 41 | > [!WARNING] 42 | > 43 | > When you run QEMU, make sure `opensbi-riscv32-generic-fw_dynamic.bin` is in your current directory. If it's not, you'll see this error: 44 | > 45 | > ``` 46 | > qemu-system-riscv32: Unable to load the RISC-V firmware "opensbi-riscv32-generic-fw_dynamic.bin" 47 | > ``` 48 | 49 | ### Other OS users 50 | 51 | If you are using other OSes, get the following tools: 52 | 53 | - `bash`: The command-line shell. Usually it's pre-installed. 54 | - `tar`: Usually it's pre-installed. Prefer GNU version, not BSD. 55 | - `clang`: C compiler. Make sure it supports 32-bit RISC-V CPU (see below). 56 | - `lld`: LLVM linker, which bundles complied object files into an executable. 57 | - `llvm-objcopy`: Object file editor. It comes with the LLVM package (typically `llvm` package). 58 | - `llvm-objdump`: A disassembler. Same as `llvm-objcopy`. 59 | - `llvm-readelf`: An ELF file reader. Same as `llvm-objcopy`. 60 | - `qemu-system-riscv32`: 32-bit RISC-V CPU emulator. It's part of the QEMU package (typically `qemu` package). 61 | 62 | > [!TIP] 63 | > 64 | > To check if your `clang` supports 32-bit RISC-V CPU, run this command: 65 | > 66 | > ``` 67 | > $ clang -print-targets | grep riscv32 68 | > riscv32 - 32-bit RISC-V 69 | > ``` 70 | > 71 | > You should see `riscv32`. Note pre-installed clang on macOS won't show this. That's why you need to install another `clang` in Homebrew's `llvm` package. 72 | 73 | ## Setting up a Git repository (optional) 74 | 75 | If you're using a Git repository, use the following `.gitignore` file: 76 | 77 | ```gitignore [.gitignore] 78 | /disk/* 79 | !/disk/.gitkeep 80 | *.map 81 | *.tar 82 | *.o 83 | *.elf 84 | *.bin 85 | *.log 86 | *.pcap 87 | ``` 88 | 89 | You're all set! Let's start building your first operating system! 90 | -------------------------------------------------------------------------------- /website/en/03-overview.md: -------------------------------------------------------------------------------- 1 | # What we will implement 2 | 3 | Before starting to build an OS, let's quickly get an overview of the features we will implement. 4 | 5 | ## Features in 1K LoC OS 6 | 7 | In this book, we will implement the following major features: 8 | 9 | - **Multitasking**: Switch between processes to allow multiple applications to share the CPU. 10 | - **Exception handler**: Handle events requiring OS intervention, such as illegal instructions. 11 | - **Paging**: Provide an isolated memory address space for each application. 12 | - **System calls**: Allow applications to call kernel features. 13 | - **Device drivers**: Abstract hardware functionalities, such as disk read/write. 14 | - **File system**: Manage files on disk. 15 | - **Command-line shell**: User interface for humans. 16 | 17 | ## Features not implemented 18 | 19 | The following major features are not implemented in this book: 20 | 21 | - **Interrupt handling**: Instead, we will use a polling method (periodically check for new data on devices), also known as busy waiting. 22 | - **Timer processing**: Preemptive multitasking is not implemented. We'll use cooperative multitasking, where each process voluntarily yields the CPU. 23 | - **Inter-process communication**: Features such as pipe, UNIX domain socket, and shared memory are not implemented. 24 | - **Multi-processor support**: Only single processor is supported. 25 | 26 | ## Source code structure 27 | 28 | We'll build from scratch incrementally, and the final file structure will look like this: 29 | 30 | ``` 31 | ├── disk/ - File system contents 32 | ├── common.c - Kernel/user common library: printf, memset, ... 33 | ├── common.h - Kernel/user common library: definitions of structs and constants 34 | ├── kernel.c - Kernel: process management, system calls, device drivers, file system 35 | ├── kernel.h - Kernel: definitions of structs and constants 36 | ├── kernel.ld - Kernel: linker script (memory layout definition) 37 | ├── shell.c - Command-line shell 38 | ├── user.c - User library: functions for system calls 39 | ├── user.h - User library: definitions of structs and constants 40 | ├── user.ld - User: linker script (memory layout definition) 41 | └── run.sh - Build script 42 | ``` 43 | 44 | > [!TIP] 45 | > 46 | > In this book, "user land" is sometimes abbreviated as "user". Consider it as "applications", and do not confuse it with "user account"! 47 | -------------------------------------------------------------------------------- /website/en/06-libc.md: -------------------------------------------------------------------------------- 1 | # C Standard Library 2 | 3 | In this chapter, let's implement basic types and memory operations, as well as string manipulation functions. In this book, for the purpose of learning, we'll create these from scratch instead of using C standard library. 4 | 5 | > [!TIP] 6 | > 7 | > The concepts introduced in this chapter are very common in C programming, so ChatGPT would provide solid answers. If you struggle with implementation or understanding any part, feel free to try asking it or ping me. 8 | 9 | ## Basic types 10 | 11 | First, let's define some basic types and convenient macros in `common.h`: 12 | 13 | 14 | ```c [common.h] {1-15,21-24} 15 | typedef int bool; 16 | typedef unsigned char uint8_t; 17 | typedef unsigned short uint16_t; 18 | typedef unsigned int uint32_t; 19 | typedef unsigned long long uint64_t; 20 | typedef uint32_t size_t; 21 | typedef uint32_t paddr_t; 22 | typedef uint32_t vaddr_t; 23 | 24 | #define true 1 25 | #define false 0 26 | #define NULL ((void *) 0) 27 | #define align_up(value, align) __builtin_align_up(value, align) 28 | #define is_aligned(value, align) __builtin_is_aligned(value, align) 29 | #define offsetof(type, member) __builtin_offsetof(type, member) 30 | #define va_list __builtin_va_list 31 | #define va_start __builtin_va_start 32 | #define va_end __builtin_va_end 33 | #define va_arg __builtin_va_arg 34 | 35 | void *memset(void *buf, char c, size_t n); 36 | void *memcpy(void *dst, const void *src, size_t n); 37 | char *strcpy(char *dst, const char *src); 38 | int strcmp(const char *s1, const char *s2); 39 | void printf(const char *fmt, ...); 40 | ``` 41 | 42 | Most of these are available in the standard library, but we've added a few useful ones: 43 | 44 | - `paddr_t`: A type representing physical memory addresses. 45 | - `vaddr_t`: A type representing virtual memory addresses. Equivalent to `uintptr_t` in the standard library. 46 | - `align_up`: Rounds up `value` to the nearest multiple of `align`. `align` must be a power of 2. 47 | - `is_aligned`: Checks if `value` is a multiple of `align`. `align` must be a power of 2. 48 | - `offsetof`: Returns the offset of a member within a structure (how many bytes from the start of the structure). 49 | 50 | `align_up` and `is_aligned` are useful when dealing with memory alignment. For example, `align_up(0x1234, 0x1000)` returns `0x2000`. Also, `is_aligned(0x2000, 0x1000)` returns true, but `is_aligned(0x2f00, 0x1000)` is false. 51 | 52 | The functions starting with `__builtin_` used in each macro are Clang-specific extensions (built-in functions). See [Clang built-in functions and macros](https://clang.llvm.org/docs/LanguageExtensions.html). 53 | 54 | > [!TIP] 55 | > 56 | > These macros can also be implemented in C without built-in functions. The pure C implementation of `offsetof` is particularly interesting ;) 57 | 58 | ## Memory operations 59 | 60 | Next, we implement the following memory operation functions. 61 | 62 | The `memcpy` function copies `n` bytes from `src` to `dst`: 63 | 64 | ```c [common.c] 65 | void *memcpy(void *dst, const void *src, size_t n) { 66 | uint8_t *d = (uint8_t *) dst; 67 | const uint8_t *s = (const uint8_t *) src; 68 | while (n--) 69 | *d++ = *s++; 70 | return dst; 71 | } 72 | ``` 73 | 74 | The `memset` function fills the first `n` bytes of `buf` with `c`. This function has already been implemented in Chapter 4 for initializing the bss section. Let's move it from `kernel.c` to `common.c`: 75 | 76 | ```c [common.c] 77 | void *memset(void *buf, char c, size_t n) { 78 | uint8_t *p = (uint8_t *) buf; 79 | while (n--) 80 | *p++ = c; 81 | return buf; 82 | } 83 | ``` 84 | 85 | > [!TIP] 86 | > 87 | > `*p++ = c;` does pointer dereferencing and pointer manipulation in a single statement. For clarity, it's equivalent to: 88 | > 89 | > ```c 90 | > *p = c; // Dereference the pointer 91 | > p = p + 1; // Advance the pointer after the assignment 92 | > ``` 93 | > 94 | > This is an idiom in C. 95 | 96 | ## String operations 97 | 98 | Let's start with `strcpy`. This function copies the string from `src` to `dst`: 99 | 100 | ```c [common.c] 101 | char *strcpy(char *dst, const char *src) { 102 | char *d = dst; 103 | while (*src) 104 | *d++ = *src++; 105 | *d = '\0'; 106 | return dst; 107 | } 108 | ``` 109 | 110 | > [!WARNING] 111 | > 112 | > The `strcpy` function continues copying even if `src` is longer than the memory area of `dst`. This can easily lead to bugs and vulnerabilities, so it's generally recommended to use alternative functions instead of `strcpy`. Never use it in production! 113 | > 114 | > For simplicity, we'll use `strcpy` in this book, but if you have the capacity, try implementing and using an alternative function (`strcpy_s`) instead. 115 | 116 | Next function is the `strcmp` function. It compares `s1` and `s2` and returns: 117 | 118 | | Condition | Result | 119 | | --------- | ------ | 120 | | `s1` == `s2` | 0 | 121 | | `s1` > `s2` | Positive value | 122 | | `s1` < `s2` | Negative value | 123 | 124 | ```c [common.c] 125 | int strcmp(const char *s1, const char *s2) { 126 | while (*s1 && *s2) { 127 | if (*s1 != *s2) 128 | break; 129 | s1++; 130 | s2++; 131 | } 132 | 133 | return *(unsigned char *)s1 - *(unsigned char *)s2; 134 | } 135 | ``` 136 | 137 | > [!TIP] 138 | > 139 | > The casting to `unsigned char *` when comparing is done to conform to the [POSIX specification](https://www.man7.org/linux/man-pages/man3/strcmp.3.html#:~:text=both%20interpreted%20as%20type%20unsigned%20char). 140 | 141 | The `strcmp` function is often used to check if two strings are identical. It's a bit counter-intuitive, but the strings are identical when `!strcmp(s1, s2)` is true (i.e., when the function returns zero): 142 | 143 | ```c 144 | if (!strcmp(s1, s2)) 145 | printf("s1 == s2\n"); 146 | else 147 | printf("s1 != s2\n"); 148 | ``` 149 | -------------------------------------------------------------------------------- /website/en/07-kernel-panic.md: -------------------------------------------------------------------------------- 1 | # Kernel Panic 2 | 3 | A kernel panic occurs when the kernel encounters an unrecoverable error, similar to the concept of `panic` in Go or Rust. Have you ever seen a blue screen on Windows? Let's implement the same concept in our kernel to handle fatal errors. 4 | 5 | The following `PANIC` macro is the implementation of kernel panic: 6 | 7 | ```c [kernel.h] 8 | #define PANIC(fmt, ...) \ 9 | do { \ 10 | printf("PANIC: %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 11 | while (1) {} \ 12 | } while (0) 13 | ``` 14 | 15 | It prints where the panic occurred, it enters an infinite loop to halt processing. We define it as a macro here. The reason for this is to correctly display the source file name (`__FILE__`) and line number (`__LINE__`). If we defined this as a function, `__FILE__` and `__LINE__` would show the file name and line number where `PANIC` is defined, not where it's called. 16 | 17 | This macro also uses two idioms: 18 | 19 | The first idiom is the `do-while` statement. Since it's `while (0)`, this loop is only executed once. This is a common way to define macros consisting of multiple statements. Simply enclosing with `{ ...}` can lead to unintended behavior when combined with statements like `if` (see [this clear example](https://www.jpcert.or.jp/sc-rules/c-pre10-c.html)). Also, note the backslash (`\`) at the end of each line. Although the macro is defined over multiple lines, newline characters are ignored when expanded. 20 | 21 | The second idiom is `##__VA_ARGS__`. This is a useful compiler extension for defining macros that accept a variable number of arguments (reference: [GCC documentation](https://gcc.gnu.org/onlinedocs/gcc/Variadic-Macros.html)). `##` removes the preceding `,` when the variable arguments are empty. This allows compilation to succeed even when there's only one argument, like `PANIC("booted!")`. 22 | 23 | ## Let's try it 24 | 25 | Let's try using `PANIC`. You can use it like `printf`: 26 | 27 | ```c [kernel.c] {4-5} 28 | void kernel_main(void) { 29 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 30 | 31 | PANIC("booted!"); 32 | printf("unreachable here!\n"); 33 | } 34 | ``` 35 | 36 | Try in QEMU and confirm that the correct file name and line number are displayed, and that the processing after `PANIC` is not executed (i.e., `"unreachable here!"` is not displayed): 37 | 38 | ``` 39 | $ ./run.sh 40 | PANIC: kernel.c:46: booted! 41 | ``` 42 | 43 | Blue screen in Windows and kernel panics in Linux are very scary, but in your own kernel, don't you think it is a nice feature to have? It's a "crash gracefully" mechanism, with a human-readable clue. 44 | -------------------------------------------------------------------------------- /website/en/09-memory-allocation.md: -------------------------------------------------------------------------------- 1 | # Memory Allocation 2 | 3 | In this chapter, we'll implement a simple memory allocator. 4 | 5 | ## Revisiting the linker script 6 | 7 | Before implementing a memory allocator, let's define the memory regions to be managed by the allocator: 8 | 9 | ```ld [kernel.ld] {5-8} 10 | . = ALIGN(4); 11 | . += 128 * 1024; /* 128KB */ 12 | __stack_top = .; 13 | 14 | . = ALIGN(4096); 15 | __free_ram = .; 16 | . += 64 * 1024 * 1024; /* 64MB */ 17 | __free_ram_end = .; 18 | } 19 | ``` 20 | 21 | This adds two new symbols: `__free_ram` and `__free_ram_end`. This defines a memory area after the stack space. The size of the space (64MB) is an arbitrary value and `. = ALIGN(4096)` ensures that it's aligned to a 4KB boundary. 22 | 23 | By defining this in the linker script instead of hardcoding addresses, the linker can determine the position to avoid overlapping with the kernel's static data. 24 | 25 | > [!TIP] 26 | > 27 | > Practical operating systems on x86-64 determine available memory regions by obtaining information from hardware at boot time (for example, UEFI's `GetMemoryMap`). 28 | 29 | ## The world's simplest memory allocation algorithm 30 | 31 | Let's implement a function to allocate memory dynamically. Instead of allocating in bytes like `malloc`, it allocates in a larger unit called *"pages"*. 1 page is typically 4KB (4096 bytes). 32 | 33 | > [!TIP] 34 | > 35 | > 4KB = 4096 = 0x1000 (hexadecimal). Thus, page-aligned addresses look nicely aligned in hexadecimal. 36 | 37 | The following `alloc_pages` function dynamically allocates `n` pages of memory and returns the starting address: 38 | 39 | ```c [kernel.c] 40 | extern char __free_ram[], __free_ram_end[]; 41 | 42 | paddr_t alloc_pages(uint32_t n) { 43 | static paddr_t next_paddr = (paddr_t) __free_ram; 44 | paddr_t paddr = next_paddr; 45 | next_paddr += n * PAGE_SIZE; 46 | 47 | if (next_paddr > (paddr_t) __free_ram_end) 48 | PANIC("out of memory"); 49 | 50 | memset((void *) paddr, 0, n * PAGE_SIZE); 51 | return paddr; 52 | } 53 | ``` 54 | 55 | `PAGE_SIZE` represents the size of one page. Define it in `common.h`: 56 | 57 | ```c [common.h] 58 | #define PAGE_SIZE 4096 59 | ``` 60 | 61 | You will find the following key points: 62 | 63 | - `next_paddr` is defined as a `static` variable. This means, unlike local variables, its value is retained between function calls. That is, it behaves like a global variable. 64 | - `next_paddr` points to the start address of the "next area to be allocated" (free area). When allocating, `next_paddr` is advanced by the size being allocated. 65 | - `next_paddr` initially holds the address of `__free_ram`. This means memory is allocated sequentially starting from `__free_ram`. 66 | - `__free_ram` is placed on a 4KB boundary due to `ALIGN(4096)` in the linker script. Therefore, the `alloc_pages` function always returns an address aligned to 4KB. 67 | - If it tries to allocate beyond `__free_ram_end`, in other words, if it runs out of memory, a kernel panic occurs. 68 | - The `memset` function ensures that the allocated memory area is always filled with zeroes. This is to avoid hard-to-debug issues caused by uninitialized memory. 69 | 70 | Isn't it simple? However, there is a big problem with this memory allocation algorithm: allocated memory cannot be freed! That said, it's good enough for our simple hobby OS. 71 | 72 | > [!TIP] 73 | > 74 | > This algorithm is known as **Bump allocator** or **Linear allocator**, and it's actually used in scenarios where deallocation is not necessary. It's an attractive allocation algorithm that can be implemented in just a few lines and is very fast. 75 | > 76 | > When implementing deallocation, it's common to use a bitmap-based algorithm or use an algorithm called the buddy system. 77 | 78 | ## Let's try memory allocation 79 | 80 | Let's test the memory allocation function we've implemented. Add some code to `kernel_main`: 81 | 82 | ```c [kernel.c] {4-7} 83 | void kernel_main(void) { 84 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 85 | 86 | paddr_t paddr0 = alloc_pages(2); 87 | paddr_t paddr1 = alloc_pages(1); 88 | printf("alloc_pages test: paddr0=%x\n", paddr0); 89 | printf("alloc_pages test: paddr1=%x\n", paddr1); 90 | 91 | PANIC("booted!"); 92 | } 93 | ``` 94 | 95 | Verify that the first address (`paddr0`) matches the address of `__free_ram`, and that the next address (`paddr1`) matches an address 8KB after `paddr0`: 96 | 97 | ``` 98 | $ ./run.sh 99 | Hello World! 100 | alloc_pages test: paddr0=80221000 101 | alloc_pages test: paddr1=80223000 102 | ``` 103 | 104 | ``` 105 | $ llvm-nm kernel.elf | grep __free_ram 106 | 80221000 R __free_ram 107 | 84221000 R __free_ram_end 108 | ``` 109 | -------------------------------------------------------------------------------- /website/en/12-application.md: -------------------------------------------------------------------------------- 1 | # Application 2 | 3 | In this chapter, we'll prepare the first application executable to run on our kernel. 4 | 5 | ## Memory layout 6 | 7 | In the previous chapter, we implemented isolated virtual address spaces using the paging mechanism. Let's consider where to place the application in the address space. 8 | 9 | Create a new linker script (`user.ld`) that defines where to place the application in memory: 10 | 11 | ```ld [user.ld] 12 | ENTRY(start) 13 | 14 | SECTIONS { 15 | . = 0x1000000; 16 | 17 | /* machine code */ 18 | .text :{ 19 | KEEP(*(.text.start)); 20 | *(.text .text.*); 21 | } 22 | 23 | /* read-only data */ 24 | .rodata : ALIGN(4) { 25 | *(.rodata .rodata.*); 26 | } 27 | 28 | /* data with initial values */ 29 | .data : ALIGN(4) { 30 | *(.data .data.*); 31 | } 32 | 33 | /* data that should be zero-filled at startup */ 34 | .bss : ALIGN(4) { 35 | *(.bss .bss.* .sbss .sbss.*); 36 | 37 | . = ALIGN(16); 38 | . += 64 * 1024; /* 64KB */ 39 | __stack_top = .; 40 | 41 | ASSERT(. < 0x1800000, "too large executable"); 42 | } 43 | } 44 | ``` 45 | 46 | It looks pretty much the same as the kernel's linker script, isn't it? The key difference is the base address (`0x1000000`) so that the application doesn't overlap with the kernel's address space. 47 | 48 | `ASSERT` is an assertion which aborts the linker if the condition in the first argument is not met. Here, it ensures that the end of the `.bss` section, which is the end of the application memory, does not exceed `0x1800000`. This is to ensure that the executable file doesn't accidentally become too large. 49 | 50 | ## Userland library 51 | 52 | Next, let's create a library for userland programs. For simplicity, we'll start with a minimal feature set to start the application: 53 | 54 | ```c [user.c] 55 | #include "user.h" 56 | 57 | extern char __stack_top[]; 58 | 59 | __attribute__((noreturn)) void exit(void) { 60 | for (;;); 61 | } 62 | 63 | void putchar(char ch) { 64 | /* TODO */ 65 | } 66 | 67 | __attribute__((section(".text.start"))) 68 | __attribute__((naked)) 69 | void start(void) { 70 | __asm__ __volatile__( 71 | "mv sp, %[stack_top] \n" 72 | "call main \n" 73 | "call exit \n" 74 | :: [stack_top] "r" (__stack_top) 75 | ); 76 | } 77 | ``` 78 | 79 | The execution of the application starts from the `start` function. Similar to the kernel's boot process, it sets up the stack pointer and calls the application's `main` function. 80 | 81 | We prepare the `exit` function to terminate the application. However, for now, we'll just have it perform an infinite loop. 82 | 83 | Also, we define the `putchar` function that the `printf` function in `common.c` refers to. We'll implement this later. 84 | 85 | Unlike the kernel's initialization process, we don't clear the `.bss` section with zeros. This is because the kernel guarantees that it has already filled it with zeros (in the `alloc_pages` function). 86 | 87 | > [!TIP] 88 | > 89 | > Allocated memory regions are already filled with zeros in typical operating systems too. Otherwise, the memory may contain sensitive information (e.g. credentials) from other processes, and it could lead to a critical security issue. 90 | 91 | Lastly, prepare a header file (`user.h`) for the userland library: 92 | 93 | ```c [user.h] 94 | #pragma once 95 | #include "common.h" 96 | 97 | __attribute__((noreturn)) void exit(void); 98 | void putchar(char ch); 99 | ``` 100 | 101 | ## First application 102 | 103 | It's time to create the first application! Unfortunately, we still don't have a way to display characters, we can't start with a "Hello, World!" program. Instead, we'll create a simple infinite loop: 104 | 105 | ```c [shell.c] 106 | #include "user.h" 107 | 108 | void main(void) { 109 | for (;;); 110 | } 111 | ``` 112 | 113 | ## Building the application 114 | 115 | Applications will be built separately from the kernel. Let's create a new script (`run.sh`) to build the application: 116 | 117 | ```bash [run.sh] {1,3-6,10} 118 | OBJCOPY=/opt/homebrew/opt/llvm/bin/llvm-objcopy 119 | 120 | # Build the shell (application) 121 | $CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf shell.c user.c common.c 122 | $OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin 123 | $OBJCOPY -Ibinary -Oelf32-littleriscv shell.bin shell.bin.o 124 | 125 | # Build the kernel 126 | $CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf \ 127 | kernel.c common.c shell.bin.o 128 | ``` 129 | 130 | The first `$CC` call is very similar to the kernel build script. Compile C files and link them with the `user.ld` linker script. 131 | 132 | The first `$OBJCOPY` command converts the executable file (in ELF format) to raw binary format. A raw binary is the actual content that will be expanded in memory from the base address (in this case, `0x1000000`). The OS can prepare the application in memory simply by copying the contents of the raw binary. Common OSes use formats like ELF, where memory contents and their mapping information are separate, but in this book, we'll use raw binary for simplicity. 133 | 134 | The second `$OBJCOPY` command converts the raw binary execution image into a format that can be embedded in C language. Let's take a look at what's inside using the `llvm-nm` command: 135 | 136 | ``` 137 | $ llvm-nm shell.bin.o 138 | 00000000 D _binary_shell_bin_start 139 | 00010260 D _binary_shell_bin_end 140 | 00010260 A _binary_shell_bin_size 141 | ``` 142 | 143 | The prefix `_binary_` is followed by the file name, and then `start`, `end`, and `size`. These are symbols that indicate the beginning, end, and size of the execution image, respectively. In practice, they are used as follows: 144 | 145 | ```c 146 | extern char _binary_shell_bin_start[]; 147 | extern char _binary_shell_bin_size[]; 148 | 149 | void main(void) { 150 | uint8_t *shell_bin = (uint8_t *) _binary_shell_bin_start; 151 | printf("shell_bin size = %d\n", (int) _binary_shell_bin_size); 152 | printf("shell_bin[0] = %x (%d bytes)\n", shell_bin[0]); 153 | } 154 | ``` 155 | 156 | This program outputs the file size of `shell.bin` and the first byte of its contents. In other words, you can treat the `_binary_shell_bin_start` variable as if it contains the file contents, like: 157 | 158 | ```c 159 | char _binary_shell_bin_start[] = ""; 160 | ``` 161 | 162 | `_binary_shell_bin_size` variable contains the file size. However, it's used in a slightly unusual way. Let's check with `llvm-nm` again: 163 | 164 | ``` 165 | $ llvm-nm shell.bin.o | grep _binary_shell_bin_size 166 | 00010454 A _binary_shell_bin_size 167 | 168 | $ ls -al shell.bin ← note: do not confuse with shell.bin.o! 169 | -rwxr-xr-x 1 seiya staff 66644 Oct 24 13:35 shell.bin 170 | 171 | $ python3 -c 'print(0x10454)' 172 | 66644 173 | ``` 174 | 175 | The first column in the `llvm-nm` output is the *address* of the symbol. This `10454` hexadecimal number matches the file size, but this is not a coincidence. Generally, the values of each address in a `.o` file are determined by the linker. However, `_binary_shell_bin_size` is special. 176 | 177 | The `A` in the second column indicates that the address of `_binary_shell_bin_size` is a type of symbol (absolute) that should not be changed by the linker. That is, it embeds the file size as an address. 178 | 179 | By defining it as an array of an arbitrary type like `char _binary_shell_bin_size[]`, `_binary_shell_bin_size` will be treated as a pointer storing its *address*. However, since we're embedding the file size as an address here, casting it will result in the file size. This is a common trick (or a dirty hack) that exploits the object file format. 180 | 181 | Lastly, we've added `shell.bin.o` to the `clang` arguments in the kernel compiling. It embeds the first application's executable into the kernel image. 182 | 183 | ## Disassemble the executable 184 | 185 | In disassembly, we can see that the `.text.start` section is placed at the beginning of the executable file. The `start` function should be placed at `0x1000000` as follows: 186 | 187 | ``` 188 | $ llvm-objdump -d shell.elf 189 | 190 | shell.elf: file format elf32-littleriscv 191 | 192 | Disassembly of section .text: 193 | 194 | 01000000 : 195 | 1000000: 37 05 01 01 lui a0, 4112 196 | 1000004: 13 05 05 26 addi a0, a0, 608 197 | 1000008: 2a 81 mv sp, a0 198 | 100000a: 19 20 jal 0x1000010
199 | 100000c: 29 20 jal 0x1000016 200 | 100000e: 00 00 unimp 201 | 202 | 01000010
: 203 | 1000010: 01 a0 j 0x1000010
204 | 1000012: 00 00 unimp 205 | 206 | 01000016 : 207 | 1000016: 01 a0 j 0x1000016 208 | ``` 209 | -------------------------------------------------------------------------------- /website/en/17-outro.md: -------------------------------------------------------------------------------- 1 | # Outro 2 | 3 | Congratulations! You've completed the book. You've learned how to implement a simple OS kernel from scratch. You've learned about the basic concepts of operating systems, such as booting CPU, context switching, page table, user mode, system call, disk I/O, and file system. 4 | 5 | Although it's less than 1000 lines, it must have been quite challenging. This is because you built the core of the core of the core of a kernel. 6 | 7 | For those who are not satisfied yet and want to continue with something, here are some next steps: 8 | 9 | ## Add new features 10 | 11 | In this book, we implemented the basic features of a kernel. However, there are still many features that can be implemented. For example, it would be interesting to implement the following features: 12 | 13 | - A proper memory allocator that allows freeing memory. 14 | - Interrupt handling. Do not busy-wait for disk I/O. 15 | - A full-fledged file system. Implementing ext2 would be a good start. 16 | - Network communication (TCP/IP). It's not hard to implement UDP/IP (TCP is somewhat advanced). Virtio-net is very similar to virtio-blk! 17 | 18 | ## Read other OS implementations 19 | 20 | The most recommended next step is to read the implementation of existing OSes. Comparing your implementation with others is very educational. 21 | 22 | My favorite is this [RISC-V version of xv6](https://github.com/mit-pdos/xv6-riscv). This is a UNIX-like OS for educational purposes, and it comes with an [explanatory book (in English)](https://pdos.csail.mit.edu/6.828/2022/). It's recommended for those who want to learn about UNIX-specific features like `fork(2)`. 23 | 24 | Another one is my project [Starina](https://starina.dev), a microkernel-based OS written in Rust. This is still very experimental, but would be interesting for those who want to learn about microkernel architecture and how Rust shines in OS development. 25 | 26 | ## Feedback is very welcome! 27 | 28 | If you have any questions or feedback, please feel free to ask on [GitHub](https://github.com/nuta/operating-system-in-1000-lines/issues), or [send me an email](https://seiya.me) if you prefer. Happy your endless OS programming! 29 | -------------------------------------------------------------------------------- /website/en/index.md: -------------------------------------------------------------------------------- 1 | # Operating System in 1,000 Lines 2 | 3 | Hey there! In this book, we're going to build a small operating system from scratch, step by step. 4 | 5 | You might get intimidated when you hear OS or kernel development, the basic functions of an OS (especially the kernel) are surprisingly simple. Even Linux, which is often cited as a huge open-source software, was only 8,413 lines in version 0.01. Today's Linux kernel is overwhelmingly large, but it started with a tiny codebase, just like your hobby project. 6 | 7 | We'll implement basic context switching, paging, user mode, a command-line shell, a disk device driver, and file read/write operations in C. Sounds like a lot, however, it's only 1,000 lines of code! 8 | 9 | One thing you should remember is, it's not as easy as it sounds. The tricky part of creating your own OS is debugging. You can't do `printf` debugging until you implement it. You'll need to learn different debugging techniques and skills you've never needed in application development. Especially when starting "from scratch", you'll encounter challenging parts like boot process and paging. But don't worry! We'll also learn "how to debug an OS" too! 10 | 11 | The tougher the debugging, the more satisfying it is when it works. Let's dive into exciting world of OS development! 12 | 13 | - You can download the implementation examples from [GitHub](https://github.com/nuta/operating-system-in-1000-lines). 14 | - This book is available under the [CC BY 4.0 license](https://creativecommons.jp/faq). The implementation examples and source code in the text are under the [MIT license](https://opensource.org/licenses/MIT). 15 | - The prerequisites are familiarity with the C language and with a UNIX-like environment. If you've run `gcc hello.c && ./a.out`, you're good to go! 16 | - This book was originally written as an appendix of my book *[Design and Implementation of Microkernels](https://www.shuwasystem.co.jp/book/9784798068718.html)* (written in Japanese). 17 | 18 | Happy OS hacking! 19 | -------------------------------------------------------------------------------- /website/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Operating System in 1,000 Lines" 7 | 8 | features: 9 | - title: English 10 | link: /en 11 | - title: 日本語 12 | link: /ja 13 | - title: 简体中文 14 | link: /zh 15 | - title: 한국어 16 | link: /ko 17 | --- 18 | 19 | -------------------------------------------------------------------------------- /website/ja/01-setting-up-development-environment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 開発環境 3 | --- 4 | 5 | # 開発環境 6 | 7 | 本書では、macOSとUbuntuといったUNIX系OSを想定しています。Windowsをお使いの場合は、Windows Subsystem for Linux (WSL2) をインストールしたのち、Ubuntuの手順に従ってください。 8 | 9 | ## ソフトウェアのインストール 10 | 11 | ### macOS 12 | 13 | [Homebrew](https://brew.sh/index_ja) をインストールしたのち、次のコマンドで必要なパッケージをインストールします。 14 | 15 | ``` 16 | brew install llvm lld qemu 17 | ``` 18 | 19 | また、LLVMのユーティリティを使えるように `PATH` を設定します。 20 | 21 | ``` 22 | $ export PATH="$PATH:$(brew --prefix)/opt/llvm/bin" 23 | $ which llvm-objcopy 24 | /opt/homebrew/opt/llvm/bin/llvm-objcopy 25 | ``` 26 | 27 | ### Ubuntu 28 | 29 | 次のコマンドで必要なパッケージをインストールします。他のLinuxディストリビューションをお使いの場合は、適宜読み替えてください。 30 | 31 | ``` 32 | sudo apt update && sudo apt install -y clang llvm lld qemu-system-riscv32 curl 33 | ``` 34 | 35 | 加えてOpenSBI (PCでいうBIOS/UEFI) を作業用ディレクトリにダウンロードしておきます。 36 | 37 | ``` 38 | cd <開発用ディレクトリ> 39 | curl -LO https://github.com/qemu/qemu/raw/v8.0.4/pc-bios/opensbi-riscv32-generic-fw_dynamic.bin 40 | ``` 41 | 42 | > [!WARNING] 43 | > 44 | > QEMUを実行する際に、 `opensbi-riscv32-generic-fw_dynamic.bin` がカレントディレクトリにある必要があります。別の場所にある場合、次の「ファイルが見当たらない」エラーが出ます。 45 | > 46 | > ``` 47 | > qemu-system-riscv32: Unable to load the RISC-V firmware "opensbi-riscv32-generic-fw_dynamic.bin" 48 | > ``` 49 | 50 | ### その他のOS 51 | 52 | どうしても他のOSを使いたい場合は、次のコマンドを頑張ってインストールしてください。 53 | 54 | - `bash`: コマンドラインシェル。UNIX系OSには基本的に最初から入っている。 55 | - `tar`: tarアーカイブ操作ツール。UNIX系OSには基本的に最初から入っている。GNU版の`tar`がおすすめ。 56 | - `clang`: Cコンパイラ。32ビットRISC-V CPUに対応していること (下記参照)。 57 | > `lld`: LLVMリンカー。`clang`でコンパイルしたファイルたちを一つの実行ファイルにまとめる。 58 | - `llvm-objcopy`: オブジェクトファイル編集ツール。よくLLVMパッケージに入っている。(GNU binutilsの`objcopy`でも代用可)。 59 | - `llvm-objdump`: 逆アセンブラ。`llvm-objcopy`と同様。 60 | - `llvm-readelf`: ELFファイル解析ツール。`llvm-objcopy`と同様。 61 | - `qemu-system-riscv32`: 32ビットRISC-V CPUのエミュレータ。QEMUパッケージに入っている。 62 | 63 | > [!TIP] 64 | > 65 | > お使いのclangが32ビットRISC-Vに対応しているかは、次のコマンドで確認できます。 66 | > 67 | > ``` 68 | > $ clang -print-targets | grep riscv32 69 | > riscv32 - 32-bit RISC-V 70 | > ``` 71 | > 72 | > `riscv32`が表示されればOKです。表示されない代表例としては、macOS標準のclangがあります。上記の手順では、Homebrewの全部入りclang (`llvm`パッケージ) を代わりにインストールしています。 73 | 74 | ## Gitリポジトリの用意 (任意) 75 | 76 | もしGitリポジトリ下で作っていく場合は、次の`.gitignore`をあらかじめ用意しておくと便利です。 77 | 78 | ```gitignore [.gitignore] 79 | /disk/* 80 | !/disk/.gitkeep 81 | *.map 82 | *.tar 83 | *.o 84 | *.elf 85 | *.bin 86 | *.log 87 | *.pcap 88 | ``` 89 | -------------------------------------------------------------------------------- /website/ja/02-assembly.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: RISC-V入門 3 | --- 4 | 5 | # RISC-V入門 6 | 7 | アプリケーションを開発する際に「どのOSで動かすか」を意識するのと同じように、OSもその下のレイヤであるハードウェア (特にCPU) で動かすかを考える必要があります。本書では、RISC-V (リスク・ファイブ) を次の理由から選択しました。 8 | 9 | - 仕様がシンプルで初学者向きである。 10 | - 近年よく話題に上がるCPU (命令セットアーキテクチャ) である。 11 | - 仕様書でところどころ述べられている「なぜこの設計にしたのか」の説明が面白く、勉強になる。 12 | 13 | なお、本書では32ビットのRISC-Vを利用します。64ビットでも大体同じように実装できるのですが、ビット幅が広い分複雑になるのと、アドレスが長く読むのが億劫なので、最初は32ビットがおすすめです。 14 | 15 | ## virtマシン 16 | 17 | コンピュータはCPUだけではなく、メモリをはじめとする様々なデバイスから構成されています。例えば、iPhoneとRaspberry Piは同じArmのCPUを利用していますが、別物だと考えるのが自然です。 18 | 19 | 本書では、RISC-Vベースの中でもQEMU virtマシン ([ドキュメント](https://www.qemu.org/docs/master/system/riscv/virt.html)) に次の理由から対応することにしました。 20 | 21 | - 名前の通り実在しない仮想的なコンピュータとはいえ、実機への対応と極めて近しい体験ができる。 22 | - QEMU上で動くので、実機を買わなくてもすぐに無料で試せる。また、販売終了などで実機を手に入れるのが難しくなることもない。 23 | - デバッグに困ったら、QEMUのソースコードを読んだり、QEMU自体にデバッガを繋いだりして何がおかしいのかを調べることができる。 24 | 25 | ## RISC-Vアセンブリ入門の手引き 26 | 27 | アセンブリを手っ取り早く学ぶ方法は「C言語のコードがどのようなアセンブリに変わるのか観察する」ことです。あとは、使われている命令の機能をひとつずつ辿っていくと良いでしょう。特に次の項目が基本的な知識となります。入門書で書くのもなんですが、ChatGPTに聞くと良いでしょう。 28 | 29 | - レジスタとは何か 30 | - 四則演算 31 | - メモリアクセス 32 | - 分岐命令 33 | - 関数呼び出し 34 | - スタックの仕組み 35 | 36 | アセンブリを学ぶときに重宝するのが、[Compiler Explorer](https://godbolt.org/) というオンラインコンパイラです。ここでは、C言語のコードを入力すると、コンパイルした結果の逆アセンブリを見ることができます。どの機械語がC言語のコードのどの部分に対応しているかを色でわかりやすくしてくれます。 37 | 38 | また、コンパイラオプションに `-O0` (最適化オフ) や `-O2` (最適化レベル2) などの最適化オプションを指定して、コンパイラがどのようなアセンブリを出力するのかを確認するのも勉強になるでしょう。 39 | 40 | > [!WARNING] 41 | > 42 | > デフォルトではx86-64 CPUのアセンブリが出力されるようになっています。右のペインでコンパイラを`RISC-V rv32gc clang (trunk)`を指定すると32ビットRISC-Vのアセンブリを出力するようになります。 43 | 44 | ## CPUの動作モード 45 | 46 | CPUでは動作モードによって実行できる命令や挙動が異なります。RISC-Vでは、次の3つの動作モードがあります。 47 | 48 | | モード | 概要 | 49 | | ------ | ---------------------------------- | 50 | | M-mode | OpenSBIが動作するモード。 | 51 | | S-mode | カーネルが動作するモード。 | 52 | | U-mode | アプリケーションが動作するモード。 | 53 | 54 | ## 特権命令 55 | 56 | CPUの命令の中には、特権命令と呼ばれるアプリケーションが実行できない類があります。本書では、CPUの動作設定を変更する特権命令がいくつか登場します。以下の命令が本書で登場する特権命令です。 57 | 58 | | 命令とオペランド | 概要 | 擬似コード | 59 | | ------------------- | --------------------------------------------------------------------- | -------------------------------- | 60 | | `csrr rd, csr` | CSRの読み出し | `rd = csr;` | 61 | | `csrw csr, rs` | CSRの書き込み | `csr = rs;` | 62 | | `csrrw rd, csr, rs` | CSRの読み出しと書き込みを一度に行う | `tmp = csr; csr = rs; rd = tmp;` | 63 | | `sret` | トラップハンドラからの復帰 (プログラムカウンタ・動作モードの復元など) | | 64 | | `sfence.vma` | Translation Lookaside Buffer (TLB) のクリア | | 65 | 66 | ここで登場するCSR (Control and Status Register) とは、CPUの動作設定を格納するレジスタです。各CSRの説明は本書中にありますが、一覧を見たい場合は [RISC-Vの仕様書 (PDF) ](https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf)を参照してください。 67 | 68 | ## インラインアセンブリ 69 | 70 | 本文中では、次のような特殊なC言語の記法が登場します。 71 | 72 | ```c 73 | uint32_t value; 74 | __asm__ __volatile__("csrr %0, sepc" : "=r"(value)); 75 | ``` 76 | 77 | これは「インラインアセンブリ」と呼ばれる、C言語の中にアセンブリを埋め込む記法です。別途アセンブリのファイル (`.S`拡張子) にアセンブリのみを書いてコンパイルする方法もありますが、インラインアセンブリを使うことで次のようなメリットがあります。 78 | 79 | - C言語の変数をアセンブリの中で使える。また、アセンブリの結果をC言語の変数に代入できる。 80 | - レジスタ割り当てをC言語のコンパイラに任せられる。アセンブリ中で内容を変更するレジスタの保存・復元を自分で書かなくて良い。 81 | 82 | ### インラインアセンブリの書き方 83 | 84 | インラインアセンブリは、次のような形式で書きます。 85 | 86 | ```c 87 | __asm__ __volatile__("アセンブリ" : 出力オペランド : 入力オペランド : アセンブリ中で破壊するレジスタ); 88 | ``` 89 | 90 | - アセンブリは文字列リテラルで書きます。各命令は `オペコード オペランド1, オペランド2, ...` という構造になっています。オペコードはアセンブリの命令名、オペランドは命令の引数を指します。 91 | - アセンブリは基本的に1行に1命令記述します。一般的なプログラミング言語で例えると、関数呼び出しが連なっているようなイメージです。複数の命令を書く場合は、 `"addi x1, x2, 3\naddi x3, x4, 5"` のように、改行文字を挟んで命令を書きます。 92 | - 出力オペランドでは、アセンブリの処理結果をどこに取り出すかを宣言します。 `"制約" (C言語中の変数名)` という形式で記述します。制約には大抵 `=r` を指定し、`=` はアセンブリによって変更されること、`r` はいずれかの汎用レジスタを使うことを表します。 93 | - 入力オペランドでは、アセンブリ中で使いたい値を宣言します。出力オペランドと同じように、 `"制約" (C言語での式)` という形式で記述します。制約には大抵 `r` を指定し、いずれかの汎用レジスタに値をセットすることを表します。 94 | - 最後にアセンブリ中で中身を破壊するレジスタを指定します。指定し忘れると、C言語のコンパイラがそのレジスタの内容を保存・復元せず、ローカル変数が予期しない値になるバグにつながります。 95 | - 出力・入力オペランドは、出力オペランドから順番に `%0`、`%1`、`%2` という形でアセンブリ中からアクセスできます。 96 | - `__volatile__` は「アセンブリの内容を最適化をしてはいけない (そのまま出力せよ)」とコンパイラに指示するものです。 97 | 98 | ### インラインアセンブリの例 99 | 100 | ```c 101 | uint32_t value; 102 | __asm__ __volatile__("csrr %0, sepc" : "=r"(value)); 103 | ``` 104 | 105 | 上記のインラインアセンブリは、`csrr` 命令で `sepc` レジスタの値を読み出し、`value` 変数に代入します。`%0` が `value` 変数に対応しています。 106 | 107 | ```c 108 | __asm__ __volatile__("csrw sscratch, %0" : : "r"(123)); 109 | ``` 110 | 111 | 上記のインラインアセンブリは、`csrw` 命令で `sscratch` レジスタに `123` を書き込みます。`%0` が `123` が入ったレジスタ (`r` 制約) に対応し、次のように展開されます。 112 | 113 | ``` 114 | li a0, 123 // a0 レジスタに 123 をセット 115 | csrw sscratch, a0 // sscratch レジスタに a0 レジスタの値を書き込み 116 | ``` 117 | 118 | インラインアセンブリには `csrw` 命令しか記述していませんが、`"r"` 制約を満たすために `li` 命令がコンパイラによって自動的に挿入されています。 119 | 120 | > [!TIP] 121 | > 122 | > インラインアセンブリは、C言語の仕様にはないコンパイラの独自拡張機能です。詳細な使い方は[GCCのドキュメント](https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html)で確認できます。ただし、CPUアーキテクチャによって制約の書き方が違っていたり、機能が多く複雑だったりと理解に時間を要する機能です。 123 | > 124 | > 初学者におすすめなのは実例に多く触れることです。例えば、[HinaOSのインラインアセンブリ集](https://github.com/nuta/microkernel-book/blob/52d66bd58cd95424f009e2df8bc1184f6ffd9395/kernel/riscv32/asm.h)が参考になるでしょう。 125 | -------------------------------------------------------------------------------- /website/ja/03-overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: OSの全体像 3 | --- 4 | 5 | # OSの全体像 6 | 7 | OSを作り始める前に、どんな機能を作るのか、どのようなソースコードの構成になるのかをざっくりと把握しておきましょう。 8 | 9 | ## 本書で実装する機能 10 | 11 | 本書で作るOSは、大きく分けて次の機能を持ちます。 12 | 13 | - **マルチタスク**: プロセスの切り替えを行い、複数のプログラムが同時に動作している (ように見せかける) 機能 14 | - **例外ハンドラ**: 実行時エラーなどのCPUがOSの介入を必要とするイベントを処理する機能 15 | - **ページング**: 各プロセスごとの独立したメモリ空間を実現 16 | - **システムコール**: アプリケーションからOSの機能を呼び出す機能 17 | - **デバイスドライバ**: ディスクの読み書きなど、ハードウェアを操作する機能 18 | - **ファイルシステム**: ディスク上のファイルを管理する機能 19 | - **コマンドラインシェル**: 利用者がコマンドを入力してOSの機能を呼び出せる機能 20 | 21 | ## 本書で実装しない機能 22 | 23 | 逆に何が欠けているのかというと、主に次のような主要機能が省かれています。 24 | 25 | - **割り込み処理**: 「デバイスに新しいデータがないか定期的にチェックする」方式 (ポーリング) で実装されています。 26 | - **タイマー処理**: プリエンプティブマルチタスクは実装されません。各プロセスが自発的にCPUを譲る、協調的マルチタスクで実装されています。 27 | - **プロセス間通信**: プロセス間でデータをやり取りする機能は実装されません。 28 | - **マルチプロセッサ対応**: シングルプロセッサのみ対応です。 29 | 30 | もちろん、これらの機能は後から実装可能です。本書の実装を済ませた後に、[HinaOS](https://github.com/nuta/microkernel-book)などを参考にしながら実装してみると良いでしょう。 31 | 32 | ## ソースコードの構成 33 | 34 | ゼロからゆっくり作っていきますが、最終的には次のようなファイル構成になります。 35 | 36 | ``` 37 | ├── disk/ - ファイルシステムの中身 38 | ├── common.c - カーネル・ユーザー共通ライブラリ: printf関数やmemset関数など 39 | ├── common.h - カーネル・ユーザー共通ライブラリ: 各種構造体・定数などの定義 40 | ├── kernel.c - カーネル: プロセス管理、システムコール、デバイスドライバ、ファイルシステム 41 | ├── kernel.h - カーネル: 各種構造体・定数などの定義 42 | ├── kernel.ld - カーネル: リンカスクリプト (メモリレイアウトの定義) 43 | ├── shell.c - コマンドラインシェル 44 | ├── user.c - ユーザー用ライブラリ: システムコールの呼び出し関数など 45 | ├── user.h - ユーザー用ライブラリ: 各種構造体・定数などの定義 46 | ├── user.ld - ユーザー: リンカスクリプト (メモリレイアウトの定義) 47 | └── run.sh - ビルドスクリプト 48 | ``` 49 | 50 | > [!TIP] 51 | > 52 | > 本書中では「ユーザーランド」を略して「ユーザー」と表記していることがあります。ざっくり「アプリケーション側」という意味で捉えると分かりやすいでしょう。 53 | -------------------------------------------------------------------------------- /website/ja/06-libc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: C標準ライブラリ 3 | --- 4 | 5 | # C標準ライブラリ 6 | 7 | Hello Worldを済ませたところで、基本的な型やメモリ操作、文字列操作関数を実装しましょう。一般的にはC言語の標準ライブラリ (例: `stdint.h` や `string.h`) を利用しますが、今回は勉強のためにゼロから作ります。 8 | 9 | > [!TIP] 10 | > 11 | > 本章で紹介するものはC言語でごく一般的なものなので、ChatGPTに聞くとしっかりと答えてくれる領域です。実装や理解に手こずる部分があった時には試してみてください。便利な時代になりましたね。 12 | 13 | ## 基本的な型 14 | 15 | まずは基本的な型といくつかのマクロを定義します。 16 | 17 | ```c [common.h] {1-15,21-24} 18 | typedef int bool; 19 | typedef unsigned char uint8_t; 20 | typedef unsigned short uint16_t; 21 | typedef unsigned int uint32_t; 22 | typedef unsigned long long uint64_t; 23 | typedef uint32_t size_t; 24 | typedef uint32_t paddr_t; 25 | typedef uint32_t vaddr_t; 26 | 27 | #define true 1 28 | #define false 0 29 | #define NULL ((void *) 0) 30 | #define align_up(value, align) __builtin_align_up(value, align) 31 | #define is_aligned(value, align) __builtin_is_aligned(value, align) 32 | #define offsetof(type, member) __builtin_offsetof(type, member) 33 | #define va_list __builtin_va_list 34 | #define va_start __builtin_va_start 35 | #define va_end __builtin_va_end 36 | #define va_arg __builtin_va_arg 37 | 38 | void *memset(void *buf, char c, size_t n); 39 | void *memcpy(void *dst, const void *src, size_t n); 40 | char *strcpy(char *dst, const char *src); 41 | int strcmp(const char *s1, const char *s2); 42 | void printf(const char *fmt, ...); 43 | ``` 44 | 45 | ほとんどは標準ライブラリにあるものですが、いくつか便利なものを追加しています。 46 | 47 | - `paddr_t`: 物理メモリアドレスを表す型。 48 | - `vaddr_t`: 仮想メモリアドレスを表す型。標準ライブラリでいう`uintptr_t`。 49 | - `align_up`: `value`を`align`の倍数に切り上げる。`align`は2のべき乗である必要がある。 50 | - `is_aligned`: `value`が`align`の倍数かどうかを判定する。`align`は2のべき乗である必要がある。 51 | - `offsetof`: 構造体のメンバのオフセット (メンバが構造体の先頭から何バイト目にあるか) を返す。 52 | 53 | `align_up`と`is_aligned`は、メモリアラインメントを気にする際に便利です。例えば、`align_up(0x1234, 0x1000)`は`0x2000`を返します。また、`is_aligned(0x2000, 0x1000)`は真となります。 54 | 55 | 各マクロで使われている`__builtin_`から始まる関数はClangの独自拡張 (ビルトイン関数) です。これらの他にも、[さまざまなビルトイン関数・マクロ](https://clang.llvm.org/docs/LanguageExtensions.html) があります。 56 | 57 | > [!TIP] 58 | > 59 | > なお、これらのマクロはビルトイン関数を使わなくても標準的なCのコードで実装することもできます。特に`offsetof`の実装手法は面白いので、興味のある方は検索してみてください。 60 | 61 | ## メモリ操作 62 | 63 | メモリ操作関数を実装しましょう。 64 | 65 | `memcpy`関数は`src`から`n`バイト分を`dst`にコピーします。 66 | 67 | ```c [common.c] 68 | void *memcpy(void *dst, const void *src, size_t n) { 69 | uint8_t *d = (uint8_t *) dst; 70 | const uint8_t *s = (const uint8_t *) src; 71 | while (n--) 72 | *d++ = *s++; 73 | return dst; 74 | } 75 | ``` 76 | 77 | `memset`関数は`buf`の先頭から`n`バイト分を`c`で埋めます。この関数は、bssセクションの初期化のために4章で実装済みです。`kernel.c`から`common.c`に移動させましょう。 78 | 79 | ```c [common.c] 80 | void *memset(void *buf, char c, size_t n) { 81 | uint8_t *p = (uint8_t *) buf; 82 | while (n--) 83 | *p++ = c; 84 | return buf; 85 | } 86 | ``` 87 | 88 | `*p++ = c;`のように、ポインタの間接参照とポインタの操作を一度にしている箇所がいくつかあります。わかりやすく分解すると次のようになります。C言語ではよく使われる表現です。 89 | 90 | ```c 91 | *p = c; //ポインタの間接参照を行う 92 | p = p + 1; // 代入を済ませた後にポインタを進める 93 | ``` 94 | 95 | ## 文字列操作 96 | 97 | まずは、`strcpy`関数です。この関数は`src`の文字列を`dst`にコピーします。 98 | 99 | ```c [common.c] 100 | char *strcpy(char *dst, const char *src) { 101 | char *d = dst; 102 | while (*src) 103 | *d++ = *src++; 104 | *d = '\0'; 105 | return dst; 106 | } 107 | ``` 108 | 109 | > [!WARNING] 110 | > 111 | > `strcpy`関数は`dst`のメモリ領域より`src`の方が長い時でも、`dst`のメモリ領域を越えてコピーを行います。バグや脆弱性に繋がりやすいため、一般的には`strcpy`ではなく代替の関数を使うことが推奨されています。 112 | > 113 | > 本書では簡単のため`strcpy`を使いますが、余力があれば代替の関数 (`strcpy_s`) を実装して代わりに使ってみてください。 114 | 115 | 次に`strcmp`関数です。`s1`と`s2`を比較します。`s1`と`s2`が等しい場合は0を、`s1`の方が大きい場合は正の値を、`s2`の方が大きい場合は負の値を返します。 116 | 117 | ```c [common.c] 118 | int strcmp(const char *s1, const char *s2) { 119 | while (*s1 && *s2) { 120 | if (*s1 != *s2) 121 | break; 122 | s1++; 123 | s2++; 124 | } 125 | 126 | return *(unsigned char *)s1 - *(unsigned char *)s2; 127 | } 128 | ``` 129 | 130 | 比較する際に `unsigned char *` にキャストしているのは、比較する際は符号なし整数を使うという[POSIXの仕様](https://www.man7.org/linux/man-pages/man3/strcmp.3.html#:~:text=both%20interpreted%20as%20type%20unsigned%20char)に合わせるためです。 131 | 132 | `strcmp`関数はよく文字列が同一であるかを判定したい時に使います。若干ややこしいですが、`!strcmp(s1, s2)` の場合 (ゼロが返ってきた場合に) に文字列が同一になります。 133 | 134 | ```c 135 | if (!strcmp(s1, s2)) 136 | printf("s1 == s2\n"); 137 | else 138 | printf("s1 != s2\n"); 139 | ``` 140 | -------------------------------------------------------------------------------- /website/ja/07-kernel-panic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: カーネルパニック 3 | --- 4 | 5 | # カーネルパニック 6 | 7 | カーネルパニックは、カーネルが続行不能なエラーに遭遇したときに発生するもので、GoやRustでいう`panic`に似た概念です。次の`PANIC`マクロがカーネルパニックの実装です。 8 | 9 | ```c [kernel.h] 10 | #define PANIC(fmt, ...) \ 11 | do { \ 12 | printf("PANIC: %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 13 | while (1) {} \ 14 | } while (0) 15 | ``` 16 | 17 | エラー内容を表示した後に無限ループに入って処理を停止します。ここではマクロとして定義しています。なぜかというと、ソースファイル名 (`__FILE__`) と行番号 (`__LINE__`) を正しく表示するためです。これを関数として定義すると、`__FILE__` と `__LINE__` は、`PANIC`の呼び出し元ではなく、`PANIC`が定義されているファイル名と行番号になってしまいます。 18 | 19 | このマクロでは、2つの[イディオム]()が登場しています。 20 | 21 | 1つ目は `do-while` 文です。`while (0)`であることから、このループは必ず1回しか実行されません。これは、複数の文からなるマクロを定義したいときに頻出する書き方です。単に `{ ...}` で括ってしまうと、`if` 文などと組み合わせたときに意図しない動作に繋がる ([分かりやすい例](https://www.jpcert.or.jp/sc-rules/c-pre10-c.html)) ことがあります。また、行末のバックスラッシュ (`\`) にも注意してください。マクロは複数行で定義されていますが、展開時には改行が無視されます。 22 | 23 | 2つ目のイディオムは `##__VA_ARGS__` です。これは可変長引数を受け取るマクロを定義するときに便利なコンパイラ拡張機能 (参考: [GCCのドキュメント](https://gcc.gnu.org/onlinedocs/gcc/Variadic-Macros.html)) です。`##` が付いていることで、可変長引数が空のときに直前の `,` を削除してくれます。これにより、`PANIC("booted!")` のように引数が1つのときにもコンパイルが通るようになります。 24 | 25 | マクロの書き方を理解したところで、`PANIC`を使ってみましょう。使い方は `printf` と同じです。 26 | 27 | ```c [kernel.c] {4-5} 28 | void kernel_main(void) { 29 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 30 | 31 | PANIC("booted!"); 32 | printf("unreachable here!\n"); 33 | } 34 | ``` 35 | 36 | 実際に動かしてみて、正しいファイル名と行番号、そして`PANIC`以後の処理が実行されないこと (`"unreachable here!"`が表示されないこと) を確認してください。 37 | 38 | ``` 39 | $ ./run.sh 40 | PANIC: kernel.c:46: booted! 41 | ``` 42 | -------------------------------------------------------------------------------- /website/ja/08-exception.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 例外処理 3 | --- 4 | 5 | # 例外処理 6 | 7 | 例外は、不正なメモリアクセス (例: ヌルポインタ参照) のような「プログラムの実行が続行不能な状態」になったときに、あらかじめOSによって設定されるプログラム (例外ハンドラ) に処理を切り替える仕組みです。 8 | 9 | 例外の発生は、QEMUのデバッグログ (`-d`オプション) で確認できますが、長ったらしいログを読むのは面倒なのと、例外が発生してQEMUがリセットしても初学者にはそれが分かりづらいので、例外発生箇所を出力してカーネルパニックする例外ハンドラを序盤に実装しておくのがおすすめです。JavaScriptに親しみのある方なら「とりあえずUnhandled Rejectionのハンドラを追加する感じ」と言えばしっくりくるのではないでしょうか。 10 | 11 | ## 例外処理の流れ 12 | 13 | 例外が発生すると、RISC-Vでは次のような流れで処理が進みます。 14 | 15 | 1. CPUは`medeleg`レジスタを確認して、例外をどの動作モードで処理するかを決定する。本書では、主要な例外がU-Mode (ユーザーランド)、S-Mode (カーネル) のどちらかで発生した場合は、S-Modeで処理するようにOpenSBIによって設定されている。 16 | 2. 例外発生時のCPUの状態を各CSRに保存する。 17 | 3. `stvec` レジスタの値をプログラムカウンタにセットして、カーネルの例外処理プログラム (例外ハンドラ) にジャンプする。 18 | 4. 例外ハンドラは、カーネルが好きに使って良い `sscratch`レジスタを上手く使って、汎用レジスタの値 (つまり例外発生時の実行状態) を保存し、例外の種類に応じた処理を行う。 19 | 5. 例外処理を済ませると、保存していた実行状態を復元し、`sret`命令を呼び出して例外発生箇所から実行を再開する。 20 | 21 | ステップ2で更新されるCSRは、主に次の通りです。カーネルの例外ハンドラはこれらの情報を元に、必要な処理を判断したり、例外発生時の状態を保存・復元したりします。 22 | 23 | | レジスタ名 | 内容 | 24 | | ---------- | ----------------------------------------------------------------------------------------- | 25 | | `scause` | 例外の種類。これを読んでカーネルは例外の種類を判別する。 | 26 | | `stval` | 例外の付加情報 (例: 例外の原因となったメモリアドレス)。具体的な内容は、例外の種類による。 | 27 | | `sepc` | 例外発生箇所のプログラムカウンタ | 28 | | `sstatus` | 例外発生時の動作モード (U-Mode/S-Mode) | 29 | 30 | カーネルの例外ハンドラの実装で一番気をつけなければいけないのは例外発生時の状態を正しく保存・復元することです。例えば、a0レジスタを間違って上書きしてしまうと「何もしていないのにローカル変数の値が変」といったデバッグの難しい問題に繋がってしまいます。 31 | 32 | ## 例外ハンドラの実装 33 | 34 | 準備が整ったところで、例外を受け取ってみましょう。まずは最初に実行される箇所です。`stvec`レジスタに、この`kernel_entry`関数の先頭アドレスを後ほどセットします。 35 | 36 | ```c [kernel.c] 37 | __attribute__((naked)) 38 | __attribute__((aligned(4))) 39 | void kernel_entry(void) { 40 | __asm__ __volatile__( 41 | "csrw sscratch, sp\n" 42 | "addi sp, sp, -4 * 31\n" 43 | "sw ra, 4 * 0(sp)\n" 44 | "sw gp, 4 * 1(sp)\n" 45 | "sw tp, 4 * 2(sp)\n" 46 | "sw t0, 4 * 3(sp)\n" 47 | "sw t1, 4 * 4(sp)\n" 48 | "sw t2, 4 * 5(sp)\n" 49 | "sw t3, 4 * 6(sp)\n" 50 | "sw t4, 4 * 7(sp)\n" 51 | "sw t5, 4 * 8(sp)\n" 52 | "sw t6, 4 * 9(sp)\n" 53 | "sw a0, 4 * 10(sp)\n" 54 | "sw a1, 4 * 11(sp)\n" 55 | "sw a2, 4 * 12(sp)\n" 56 | "sw a3, 4 * 13(sp)\n" 57 | "sw a4, 4 * 14(sp)\n" 58 | "sw a5, 4 * 15(sp)\n" 59 | "sw a6, 4 * 16(sp)\n" 60 | "sw a7, 4 * 17(sp)\n" 61 | "sw s0, 4 * 18(sp)\n" 62 | "sw s1, 4 * 19(sp)\n" 63 | "sw s2, 4 * 20(sp)\n" 64 | "sw s3, 4 * 21(sp)\n" 65 | "sw s4, 4 * 22(sp)\n" 66 | "sw s5, 4 * 23(sp)\n" 67 | "sw s6, 4 * 24(sp)\n" 68 | "sw s7, 4 * 25(sp)\n" 69 | "sw s8, 4 * 26(sp)\n" 70 | "sw s9, 4 * 27(sp)\n" 71 | "sw s10, 4 * 28(sp)\n" 72 | "sw s11, 4 * 29(sp)\n" 73 | 74 | "csrr a0, sscratch\n" 75 | "sw a0, 4 * 30(sp)\n" 76 | 77 | "mv a0, sp\n" 78 | "call handle_trap\n" 79 | 80 | "lw ra, 4 * 0(sp)\n" 81 | "lw gp, 4 * 1(sp)\n" 82 | "lw tp, 4 * 2(sp)\n" 83 | "lw t0, 4 * 3(sp)\n" 84 | "lw t1, 4 * 4(sp)\n" 85 | "lw t2, 4 * 5(sp)\n" 86 | "lw t3, 4 * 6(sp)\n" 87 | "lw t4, 4 * 7(sp)\n" 88 | "lw t5, 4 * 8(sp)\n" 89 | "lw t6, 4 * 9(sp)\n" 90 | "lw a0, 4 * 10(sp)\n" 91 | "lw a1, 4 * 11(sp)\n" 92 | "lw a2, 4 * 12(sp)\n" 93 | "lw a3, 4 * 13(sp)\n" 94 | "lw a4, 4 * 14(sp)\n" 95 | "lw a5, 4 * 15(sp)\n" 96 | "lw a6, 4 * 16(sp)\n" 97 | "lw a7, 4 * 17(sp)\n" 98 | "lw s0, 4 * 18(sp)\n" 99 | "lw s1, 4 * 19(sp)\n" 100 | "lw s2, 4 * 20(sp)\n" 101 | "lw s3, 4 * 21(sp)\n" 102 | "lw s4, 4 * 22(sp)\n" 103 | "lw s5, 4 * 23(sp)\n" 104 | "lw s6, 4 * 24(sp)\n" 105 | "lw s7, 4 * 25(sp)\n" 106 | "lw s8, 4 * 26(sp)\n" 107 | "lw s9, 4 * 27(sp)\n" 108 | "lw s10, 4 * 28(sp)\n" 109 | "lw s11, 4 * 29(sp)\n" 110 | "lw sp, 4 * 30(sp)\n" 111 | "sret\n" 112 | ); 113 | } 114 | ``` 115 | 116 | 実装のキーポイントは次のとおりです。 117 | 118 | - `sscratch`レジスタに、例外発生時のスタックポインタを保存しておき、あとで復元している。このように、一時退避用として`sscratch`レジスタを使うことができる。 119 | - 浮動小数点レジスタはカーネル内で使われないので、ここでは保存する必要がない。一般的にスレッドの切り替え時に保存・退避が行われる。 120 | - `a0`レジスタにスタックポインタをセットして、`handle_trap`関数を呼び出している。このとき、スタックポインタが指し示すアドレスには、後述する`trap_frame`構造体と同じ構造でレジスタの値が保存されている。 121 | - `__attribute__((aligned(4)))` をつけることで、関数の先頭アドレスを4バイト境界にアラインする。これは、`stvec`レジスタは例外ハンドラのアドレスだけでなく、下位2ビットにはモードを表すフラグを持っているため。 122 | 123 | この関数で呼ばれているのが、次の`handle_trap`関数です。 124 | 125 | ```c [kernel.c] 126 | void handle_trap(struct trap_frame *f) { 127 | uint32_t scause = READ_CSR(scause); 128 | uint32_t stval = READ_CSR(stval); 129 | uint32_t user_pc = READ_CSR(sepc); 130 | 131 | PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc); 132 | } 133 | ``` 134 | 135 | 例外の発生事由 (`scause`) と例外発生時のプログラムカウンタ (`sepc`) を取得し、デバッグ用にカーネルパニックを発生させています。ここで使われている各種マクロは、`kernel.h`で次のように定義しましょう。 136 | 137 | ```c [kernel.h] 138 | #include "common.h" 139 | 140 | struct trap_frame { 141 | uint32_t ra; 142 | uint32_t gp; 143 | uint32_t tp; 144 | uint32_t t0; 145 | uint32_t t1; 146 | uint32_t t2; 147 | uint32_t t3; 148 | uint32_t t4; 149 | uint32_t t5; 150 | uint32_t t6; 151 | uint32_t a0; 152 | uint32_t a1; 153 | uint32_t a2; 154 | uint32_t a3; 155 | uint32_t a4; 156 | uint32_t a5; 157 | uint32_t a6; 158 | uint32_t a7; 159 | uint32_t s0; 160 | uint32_t s1; 161 | uint32_t s2; 162 | uint32_t s3; 163 | uint32_t s4; 164 | uint32_t s5; 165 | uint32_t s6; 166 | uint32_t s7; 167 | uint32_t s8; 168 | uint32_t s9; 169 | uint32_t s10; 170 | uint32_t s11; 171 | uint32_t sp; 172 | } __attribute__((packed)); 173 | 174 | #define READ_CSR(reg) \ 175 | ({ \ 176 | unsigned long __tmp; \ 177 | __asm__ __volatile__("csrr %0, " #reg : "=r"(__tmp)); \ 178 | __tmp; \ 179 | }) 180 | 181 | #define WRITE_CSR(reg, value) \ 182 | do { \ 183 | uint32_t __tmp = (value); \ 184 | __asm__ __volatile__("csrw " #reg ", %0" ::"r"(__tmp)); \ 185 | } while (0) 186 | ``` 187 | 188 | `trap_frame`構造体はスタックに積まれている元の実行状態の構造を表しています。`READ_CSR`マクロと`WRITE_CSR`マクロは、CSRレジスタの読み書きを行うための便利なマクロです。 189 | 190 | 最後に必要なのは例外ハンドラがどこにあるのかをCPUに教えてあげることです。次のように`main`関数で`stvec`レジスタに例外ハンドラのアドレスを書き込みましょう。 191 | 192 | ```c [kernel.c] {4-5} 193 | void kernel_main(void) { 194 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 195 | 196 | WRITE_CSR(stvec, (uint32_t) kernel_entry); 197 | __asm__ __volatile__("unimp"); // 無効な命令 198 | ``` 199 | 200 | `stvec`レジスタの設定に加えて、`unimp`命令を実行します。この命令はRISC-Vの命令セットには存在しない架空の命令で、CPUが無効な機械語であると判断するようなバイナリ列を出力してくれる、少し便利なコンパイラの機能です (参考: [具体的なunimp命令の実装](https://github.com/llvm/llvm-project/commit/26403def69f72c7938889c1902d62121095b93d7#diff-1d077b8beaff531e8d78ba5bb21c368714f270f1b13ba47bb23d5ad2a5d1f01bR410-R414))。 201 | 202 | 実行してみて、例外ハンドラが呼ばれることを確認しましょう。 203 | 204 | ``` 205 | $ ./run.sh 206 | Hello World! 207 | PANIC: kernel.c:47: unexpected trap scause=00000002, stval=ffffff84, sepc=8020015e 208 | ``` 209 | 210 | 仕様書によると、`scause`の値が2の場合は「Illegal instruction」つまり無効な命令の実行を試みたことが分かります。まさしく、`unimp`命令の期待通りの動作です。 211 | 212 | また、`sepc`の値がどこを指しているかも確認してみましょう。`unimp`命令を呼び出している行であれば上手くいっています。 213 | 214 | ``` 215 | $ llvm-addr2line -e kernel.elf 8020015e 216 | /Users/seiya/os-from-scratch/kernel.c:129 217 | ``` 218 | -------------------------------------------------------------------------------- /website/ja/09-memory-allocation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: メモリ割り当て 3 | --- 4 | 5 | # メモリ割り当て 6 | 7 | ## リンカスクリプト 8 | 9 | 動的に割り当てるメモリ領域をリンカスクリプトに定義しましょう。 10 | 11 | ```ld [kernel.ld] {5-8} 12 | . = ALIGN(4); 13 | . += 128 * 1024; /* 128KB */ 14 | __stack_top = .; 15 | 16 | . = ALIGN(4096); 17 | __free_ram = .; 18 | . += 64 * 1024 * 1024; /* 64MB */ 19 | __free_ram_end = .; 20 | } 21 | ``` 22 | 23 | `__free_ram`から`__free_ram_end`までの領域を、動的割り当て可能なメモリ領域とします。64MBは筆者が適当に選んだ値です。`. = ALIGN(4096)`とすることで、4KB境界に配置されるようになります。 24 | 25 | このように、アドレスを決め打ちで定義せずにリンカスクリプト上に定義することで、カーネルの静的データと被らないようにリンカが位置を決定してくれます。 26 | 27 | 実用的なOSでは、このようにデバイスごとにメモリサイズを決め打ちで定義する場合の他に、起動時にハードウェアから利用可能なメモリ領域の情報を取得して決定することもあります (例: UEFIの`GetMemoryMap`)。 28 | 29 | ## たぶん世界一シンプルなメモリ割り当てアルゴリズム 30 | 31 | 動的割り当て領域を定義したところで、実際に動的にメモリを割り当てる関数を実装しましょう。ただし、`malloc`関数のようなバイト単位で割り当てるのではなく「ページ」という、まとまった単位で割り当てます。1ページは一般的に4KB (4096バイト) です。 32 | 33 | > [!TIP] 34 | > 35 | > 4KB = 4096 = 0x1000 です。16進数では`1000`であることを覚えておくと便利です。 36 | 37 | 次の`alloc_pages`関数は、`n`ページ分のメモリを動的に割り当てて、その先頭アドレスを返します。 38 | 39 | ```c [kernel.c] 40 | extern char __free_ram[], __free_ram_end[]; 41 | 42 | paddr_t alloc_pages(uint32_t n) { 43 | static paddr_t next_paddr = (paddr_t) __free_ram; 44 | paddr_t paddr = next_paddr; 45 | next_paddr += n * PAGE_SIZE; 46 | 47 | if (next_paddr > (paddr_t) __free_ram_end) 48 | PANIC("out of memory"); 49 | 50 | memset((void *) paddr, 0, n * PAGE_SIZE); 51 | return paddr; 52 | } 53 | ``` 54 | 55 | 新たに登場する `PAGE_SIZE` は、1ページのサイズを表します。`common.h`に定義しておきます。 56 | 57 | ```c [common.h] 58 | #define PAGE_SIZE 4096 59 | ``` 60 | 61 | この関数からは、次のような特徴を読み取ることができます。 62 | 63 | - `next_paddr`は`static`変数として定義されている。つまり、ローカル変数とは違い、関数呼び出し間で値が保持される (グローバル変数のような挙動を示す)。 64 | - `next_paddr`が「次に割り当てられる領域 (空いている領域) の先頭アドレス」を指す。割り当て時には、確保するサイズ分だけ`next_paddr`を進める。 65 | - `next_paddr`は`__free_ram`のアドレスを初期値として持つ。つまり、`__free_ram`から順にメモリを割り当てていく。 66 | - `__free_ram`はリンカスクリプトの`ALIGN(4096)`により4KB境界に配置される。つまり、`alloc_pages`関数必ず4KBでアラインされたアドレスを返す。 67 | - `__free_ram_end`を超えるアドレスに割り当てようとした場合は、カーネルパニックする。`malloc`関数が`NULL`を返すのと同じように`0`を返すのも手だが、返り値チェックし忘れのバグはデバッグが面倒なので、分かりやすさのためパニックさせる。 68 | - `memset`関数によって、割り当てたメモリ領域が必ずゼロで初期化されている。初期化し忘れのバグはデバッグが面倒なので、ここで初期化しておく。 69 | 70 | このメモリ割り当ての最大の特徴は個別のメモリページを解放できないことです。つまり、割り当てっぱなしです。ただ、自作OSを長時間動かし続けることはまずないでしょうから、今のところはメモリリークを許容しても差し支えないでしょう。 71 | 72 | > [!TIP] 73 | > 74 | > ちなみに、この割り当てアルゴリズムのことは**Bumpアロケータ**または**Linearアロケータ**と呼ばれており、解放処理が必要ない場面で実際に使われています。数行に実装できて高速に動作する、魅力的な割り当てアルゴリズムです。 75 | > 76 | > 解放処理を実装する場合は、ビットマップで空き状況を管理したり、バディシステムというアルゴリズムを使ったりすることが多いです。 77 | 78 | ## メモリ割り当てのテスト 79 | 80 | 実装したメモリ割り当て関数をテストしてみましょう。 81 | 82 | ```c [kernel.c] {4-7} 83 | void kernel_main(void) { 84 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 85 | 86 | paddr_t paddr0 = alloc_pages(2); 87 | paddr_t paddr1 = alloc_pages(1); 88 | printf("alloc_pages test: paddr0=%x\n", paddr0); 89 | printf("alloc_pages test: paddr1=%x\n", paddr1); 90 | 91 | PANIC("booted!"); 92 | } 93 | ``` 94 | 95 | 次のように、最初のアドレス (`paddr0`) が`__free_ram`のアドレスと一致し、次のアドレス (`paddr1`) が最初のアドレスから2 \* 4KB分進んだアドレス (16進数で`0x2000`足した数) と一致することを確認します。 96 | 97 | ``` 98 | $ ./run.sh 99 | Hello World! 100 | alloc_pages test: paddr0=80221000 101 | alloc_pages test: paddr1=80223000 102 | ``` 103 | 104 | ``` 105 | $ llvm-nm kernel.elf | grep __free_ram 106 | 80221000 R __free_ram 107 | 84221000 R __free_ram_end 108 | ``` 109 | -------------------------------------------------------------------------------- /website/ja/12-application.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: アプリケーション 3 | --- 4 | 5 | # アプリケーション 6 | 7 | 本章では、カーネルから一旦離れて、最初のユーザーランドのプログラムとそのビルド方法を見ていきます。 8 | 9 | ## メモリレイアウト 10 | 11 | 前章ではページングという仕組みを使ってプロセスごとの独立した仮想アドレス空間を実現しました。本章では、アプリケーションを仮想アドレス空間上のどこに配置するかを考えます。 12 | 13 | アプリケーションの実行ファイルをどこに配置するかを定義する、新しいリンカスクリプト (`user.ld`) を作成しましょう。 14 | 15 | ```ld [user.ld] 16 | ENTRY(start) 17 | 18 | SECTIONS { 19 | . = 0x1000000; 20 | 21 | .text :{ 22 | KEEP(*(.text.start)); 23 | *(.text .text.*); 24 | } 25 | 26 | .rodata : ALIGN(4) { 27 | *(.rodata .rodata.*); 28 | } 29 | 30 | .data : ALIGN(4) { 31 | *(.data .data.*); 32 | } 33 | 34 | .bss : ALIGN(4) { 35 | *(.bss .bss.* .sbss .sbss.*); 36 | 37 | . = ALIGN(16); /* https://github.com/nuta/operating-system-in-1000-lines/pull/23 */ 38 | . += 64 * 1024; /* 64KB */ 39 | __stack_top = .; 40 | 41 | ASSERT(. < 0x1800000, "too large executable"); 42 | } 43 | } 44 | ``` 45 | 46 | 筆者が適当に決めた、カーネルアドレスと被らない領域 (`0x1000000`から `0x1800000`の間) にアプリケーションの各データを配置することにします。大体カーネルのリンカスクリプトと同じではないでしょうか。 47 | 48 | ここで登場している`ASSERT`は、第一引数の条件が満たされていなければリンク処理 (本書ではclangコマンド) を失敗させるものです。ここでは、`.bss`セクションの末尾、つまりアプリケーションの末尾が `0x1800000` を超えていないことを確認しています。実行ファイルが意図せず大きすぎることのないようにするためです。 49 | 50 | ## ユーザーランド用ライブラリ 51 | 52 | 次にユーザーランド用ライブラリを作成しましょう。まずはアプリケーションの起動に必要な処理だけを書きます。 53 | 54 | ```c [user.c] 55 | #include "user.h" 56 | 57 | extern char __stack_top[]; 58 | 59 | __attribute__((noreturn)) void exit(void) { 60 | for (;;); 61 | } 62 | 63 | void putchar(char ch) { 64 | /* 後で実装する */ 65 | } 66 | 67 | __attribute__((section(".text.start"))) 68 | __attribute__((naked)) 69 | void start(void) { 70 | __asm__ __volatile__( 71 | "mv sp, %[stack_top]\n" 72 | "call main\n" 73 | "call exit\n" ::[stack_top] "r"(__stack_top)); 74 | } 75 | ``` 76 | 77 | アプリケーションの実行は`start`関数から始まります。カーネルのブート処理と同じように、スタックポインタを設定し、アプリケーションの`main`関数を呼び出します。 78 | 79 | アプリケーションを終了する`exit`関数も用意しておきます。ただし、ここでは無限ループを行うだけにとどめておきます。 80 | 81 | また、`common.c` の `printf` 関数が参照している `putchar` 関数も定義しておきます。のちほど実装します。 82 | 83 | カーネルの初期化処理と異なる点として、`.bss`セクションをゼロで埋める処理 (ゼロクリア) をしていません。これは、カーネルがゼロで埋めていることを保証してくるからです (`alloc_pages`関数)。 84 | 85 | > [!TIP] 86 | > 87 | > ゼロクリアは実用的なOSでも行われている処理で、ゼロで埋めないと以前そのメモリ領域を使っていた他のプロセスの情報が残ってしまうためです。パスワードのような機密情報が残ってしまっていたら大変です。 88 | 89 | 加えて、ユーザランド用ライブラリのヘッダファイル (`user.h`) も用意しておきましょう。 90 | 91 | ```c [user.h] 92 | #pragma once 93 | #include "common.h" 94 | 95 | __attribute__((noreturn)) void exit(void); 96 | void putchar(char ch); 97 | ``` 98 | 99 | ## 最初のアプリケーション 100 | 101 | 最初のアプリケーション (`shell.c`) は次のものを用意します。カーネルの時と同じく、文字を表示するのにも一手間必要なので、無限ループを行うだけにとどめておきます。 102 | 103 | ```c [shell.c] 104 | #include "user.h" 105 | 106 | void main(void) { 107 | for (;;); 108 | } 109 | ``` 110 | 111 | ## アプリケーションのビルド 112 | 113 | 最後にアプリケーションのビルド処理です。 114 | 115 | ```bash [run.sh] {1,3-6,10} 116 | OBJCOPY=/opt/homebrew/opt/llvm/bin/llvm-objcopy 117 | 118 | # シェルをビルド 119 | $CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf shell.c user.c common.c 120 | $OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin 121 | $OBJCOPY -Ibinary -Oelf32-littleriscv shell.bin shell.bin.o 122 | 123 | # カーネルをビルド 124 | $CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf \ 125 | kernel.c common.c shell.bin.o 126 | ``` 127 | 128 | 最初の`$CC`を呼び出している箇所はカーネルと同じで、clangがコンパイル・リンク処理を一括して行います。 129 | 130 | 1つ目の`$OBJCOPY`は、ビルドした実行ファイル (ELF形式) を生バイナリ形式 (raw binary) に変換する処理です。まず、生バイナリとは何かというと、ベースアドレス (ここでは0x1000000) から実際にメモリ上に展開される内容が入ったものです。OSは生バイナリの内容をそのままコピーするだけで、アプリケーションをメモリ上に展開できます。一般的なOSでは、ELFのような展開先の定義とメモリ上のデータが分かれた形式を使いますが、本書では簡単のために生バイナリを使います。 131 | 132 | 2つ目の`$OBJCOPY`は、生バイナリ形式の実行イメージを、C言語に埋め込める形式に変換する処理です。`llvm-nm`コマンドで何が入っているかを見てみましょう。 133 | 134 | ``` 135 | $ llvm-nm shell.bin.o 136 | 00010260 D _binary_shell_bin_end 137 | 00010260 A _binary_shell_bin_size 138 | 00000000 D _binary_shell_bin_start 139 | ``` 140 | 141 | `_binary_`という接頭辞に続いて、ファイル名、そして`start`、`end`、`size`が続いています。それぞれ、実行イメージの先頭、終端、サイズを示すシンボルです。実際には次のように利用します。 142 | 143 | ```c 144 | extern char _binary_shell_bin_start[]; 145 | extern char _binary_shell_bin_size[]; 146 | 147 | void main(void) { 148 | uint8_t *shell_bin = (uint8_t *) _binary_shell_bin_start; 149 | printf("shell_bin size = %d\n", (int) _binary_shell_bin_size); 150 | printf("shell_bin[0] = %x (%d bytes)\n", shell_bin[0]); 151 | } 152 | ``` 153 | 154 | このプログラムは、`shell.bin`のファイルサイズと、ファイル内容の1バイト目を出力します。つまり、次のように`_binary_shell_bin_start`変数にファイル内容が入っているかように扱えます。 155 | 156 | ```c 157 | char _binary_shell_bin_start[] = "shell.binのファイル内容"; 158 | ``` 159 | 160 | また、`_binary_shell_bin_size`変数には、ファイルサイズが入っています。ただし少し変わった使い方をします。もう一度`llvm-nm`で確認してみましょう。 161 | 162 | ``` 163 | $ llvm-nm shell.bin.o | grep _binary_shell_bin_size 164 | 00010454 A _binary_shell_bin_size 165 | 166 | $ ls -al shell.bin ← shell.bin.oではなくshell.binであることに注意 167 | -rwxr-xr-x 1 seiya staff 66644 Oct 24 13:35 shell.bin 168 | 169 | $ python3 -c 'print(0x10454)' 170 | 66644 171 | ``` 172 | 173 | 出力の1列目は、シンボルのアドレスです。この`10454`という16進数はファイルの大きさと一致しますが、これは偶然ではありません。一般的に、`.o`ファイルの各アドレスの値はリンカによって決定されます。しかし、`_binary_shell_bin_size`は特別なのです。 174 | 175 | 2列目の`A`は、`_binary_shell_bin_size`のアドレスがリンカによって変更されない種類のシンボル (absolute) であることを示しています。 176 | `char _binary_shell_bin_size[]`という適当な型の配列として定義することで、`_binary_shell_bin_size`はそのアドレスを格納したポインタとして扱われることになります。ただし、ここではファイルサイズをアドレスとして埋め込んでいるので、キャストするとファイルサイズになるのです。オブジェクトファイルの仕組みをうまく使った、ちょっとした小技が使われています。 177 | 178 | 最後に、カーネルのclangへの引数に、生成した `shell.bin.o` を追加しています。これで、最初に起動すべきアプリケーションの実行ファイルを、カーネルイメージに埋め込めるようになりました。 179 | 180 | ## 逆アセンブリを見てみる 181 | 182 | 逆アセンブリしてみると、リンカスクリプトに定義されている通り、`.text.start`セクションは実行ファイルの先頭に配置され、`0x1000000`に`start`関数が配置されていることがわかります。 183 | 184 | ``` 185 | $ llvm-objdump -d shell.elf 186 | 187 | shell.elf: file format elf32-littleriscv 188 | 189 | Disassembly of section .text: 190 | 191 | 01000000 : 192 | 1000000: 37 05 01 01 lui a0, 4112 193 | 1000004: 13 05 05 26 addi a0, a0, 608 194 | 1000008: 2a 81 mv sp, a0 195 | 100000a: 19 20 jal 0x1000010
196 | 100000c: 29 20 jal 0x1000016 197 | 100000e: 00 00 unimp 198 | 199 | 01000010
: 200 | 1000010: 01 a0 j 0x1000010
201 | 1000012: 00 00 unimp 202 | 203 | 01000016 : 204 | 1000016: 01 a0 j 0x1000016 205 | ``` 206 | -------------------------------------------------------------------------------- /website/ja/13-user-mode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ユーザーモード 3 | --- 4 | 5 | # ユーザーモード 6 | 7 | 本章では、前章で作ったアプリケーションの実行イメージを動かしてみます。 8 | 9 | ## 実行ファイルの展開 10 | 11 | まずは実行イメージの展開に必要な定義をいくつかしましょう。まずは、実行イメージの基点アドレス (`USER_BASE`) です。これは、`user.ld`で定義されている開始アドレスと合致する必要があります。 12 | 13 | ELF形式のような一般的な実行可能ファイルであれば、そのファイルのヘッダ (ELFの場合プログラムヘッダ) にロード先のアドレスが書かれています。しかし、本書のアプリケーションの実行イメージは生バイナリなので、このように決め打ちで用意しておく必要があります。 14 | 15 | ```c [kernel.h] 16 | #define USER_BASE 0x1000000 17 | ``` 18 | 19 | 次に、`shell.bin.o`に入っている実行イメージへのポインタとイメージサイズのシンボルを定義しておきます。 20 | 21 | ```c [kernel.c] 22 | extern char _binary_shell_bin_start[], _binary_shell_bin_size[]; 23 | ``` 24 | 25 | 次に、実行イメージをプロセスのアドレス空間にマップする処理を`create_process`関数に追加します。 26 | 27 | ```c [kernel.c] {1-3,5,11,20-35} 28 | void user_entry(void) { 29 | PANIC("not yet implemented"); // 後で実装する 30 | } 31 | 32 | struct process *create_process(const void *image, size_t image_size) { 33 | /* 省略 */ 34 | *--sp = 0; // s3 35 | *--sp = 0; // s2 36 | *--sp = 0; // s1 37 | *--sp = 0; // s0 38 | *--sp = (uint32_t) user_entry; // ra 39 | 40 | uint32_t *page_table = (uint32_t *) alloc_pages(1); 41 | 42 | // カーネルのページをマッピングする 43 | for (paddr_t paddr = (paddr_t) __kernel_base; 44 | paddr < (paddr_t) __free_ram_end; paddr += PAGE_SIZE) 45 | map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X); 46 | 47 | // ユーザーのページをマッピングする 48 | for (uint32_t off = 0; off < image_size; off += PAGE_SIZE) { 49 | paddr_t page = alloc_pages(1); 50 | 51 | // コピーするデータがページサイズより小さい場合を考慮 52 | // https://github.com/nuta/operating-system-in-1000-lines/pull/27 53 | size_t remaining = image_size - off; 54 | size_t copy_size = PAGE_SIZE <= remaining ? PAGE_SIZE : remaining; 55 | 56 | // 確保したページにデータをコピー 57 | memcpy((void *) page, image + off, copy_size); 58 | 59 | // ページテーブルにマッピング 60 | map_page(page_table, USER_BASE + off, page, 61 | PAGE_U | PAGE_R | PAGE_W | PAGE_X); 62 | } 63 | ``` 64 | 65 | `create_process`関数は、実行イメージへのポインタ (`image`) とイメージサイズ (`image_size`) を引数に取るように変更しました。指定されたサイズ分、実行イメージをページ単位でコピーして、ユーザーモードのページにマッピングしています。また、初回のコンテキストスイッチ時のジャンプ先を`user_entry`に設定しています。今のところは空っぽの関数にしておきます。 66 | 67 | > [!WARNING] 68 | > 69 | > このとき、実行イメージをコピーせずにそのままマッピングしてしまうと、同じアプリケーションのプロセスたちが同じ物理ページを共有することになります。 70 | 71 | 最後に `create_process` 関数の呼び出し側の修正と、ユーザープロセスを作成するようにします。 72 | 73 | ```c [kernel.c] {8,12} 74 | void kernel_main(void) { 75 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 76 | 77 | printf("\n\n"); 78 | 79 | WRITE_CSR(stvec, (uint32_t) kernel_entry); 80 | 81 | idle_proc = create_process(NULL, 0); 82 | idle_proc->pid = 0; // idle 83 | current_proc = idle_proc; 84 | 85 | create_process(_binary_shell_bin_start, (size_t) _binary_shell_bin_size); 86 | 87 | yield(); 88 | PANIC("switched to idle process"); 89 | } 90 | ``` 91 | 92 | 実際に動かしてみて、実行イメージが期待通りマッピングされているかQEMUモニタで確認してみましょう。 93 | 94 | > [!TIP] 95 | > 96 | > まだこの時点ではユーザーモードへの移行処理がないので、アプリケーションは動きません。まずは、実行イメージが正しく展開されているかのみを確認します。 97 | 98 | ``` 99 | (qemu) info mem 100 | vaddr paddr size attr 101 | -------- ---------------- -------- ------- 102 | 01000000 0000000080265000 00001000 rwxu--- 103 | 01001000 0000000080267000 00010000 rwxu--- 104 | ``` 105 | 106 | 仮想アドレス `0x1000000` (`USER_BASE`) に、物理アドレス `0x80265000` がマップされていることがわかります。この物理アドレスの中身を見てみましょう。物理メモリの内容を表示するには、`xp`コマンドを使います。 107 | 108 | ``` 109 | (qemu) xp /32b 0x80265000 110 | 0000000080265000: 0x37 0x05 0x01 0x01 0x13 0x05 0x05 0x26 111 | 0000000080265008: 0x2a 0x81 0x19 0x20 0x29 0x20 0x00 0x00 112 | 0000000080265010: 0x01 0xa0 0x00 0x00 0x82 0x80 0x01 0xa0 113 | 0000000080265018: 0x09 0xca 0xaa 0x86 0x7d 0x16 0x13 0x87 114 | ``` 115 | 116 | 何かしらデータが入っているようです。`shell.bin`の中身を確認してみると、確かに合致しています。 117 | 118 | ``` 119 | $ hexdump -C shell.bin | head 120 | 00000000 37 05 01 01 13 05 05 26 2a 81 19 20 29 20 00 00 |7......&*.. ) ..| 121 | 00000010 01 a0 00 00 82 80 01 a0 09 ca aa 86 7d 16 13 87 |............}...| 122 | 00000020 16 00 23 80 b6 00 ba 86 75 fa 82 80 01 ce aa 86 |..#.....u.......| 123 | 00000030 03 87 05 00 7d 16 85 05 93 87 16 00 23 80 e6 00 |....}.......#...| 124 | 00000040 be 86 7d f6 82 80 03 c6 05 00 aa 86 01 ce 85 05 |..}.............| 125 | 00000050 2a 87 23 00 c7 00 03 c6 05 00 93 06 17 00 85 05 |*.#.............| 126 | 00000060 36 87 65 fa 23 80 06 00 82 80 03 46 05 00 15 c2 |6.e.#......F....| 127 | 00000070 05 05 83 c6 05 00 33 37 d0 00 93 77 f6 0f bd 8e |......37...w....| 128 | 00000080 93 b6 16 00 f9 8e 91 c6 03 46 05 00 85 05 05 05 |.........F......| 129 | 00000090 6d f2 03 c5 05 00 93 75 f6 0f 33 85 a5 40 82 80 |m......u..3..@..| 130 | ``` 131 | 132 | 16進数だと分かりづらいので、`xp`コマンドを使ってメモリ上の機械語を逆アセンブルしてみましょう。 133 | 134 | ``` 135 | (qemu) xp /8i 0x80265000 136 | 0x80265000: 01010537 lui a0,16842752 137 | 0x80265004: 26050513 addi a0,a0,608 138 | 0x80265008: 812a mv sp,a0 139 | 0x8026500a: 2019 jal ra,6 # 0x80265010 140 | 0x8026500c: 2029 jal ra,10 # 0x80265016 141 | 0x8026500e: 0000 illegal 142 | 0x80265010: a001 j 0 # 0x80265010 143 | 0x80265012: 0000 illegal 144 | ``` 145 | 146 | 何か計算した結果をスタックポインタに設定し、2回関数を呼び出しています。`shell.elf`の逆アセンブル結果と比較してみると、確かに合致しています。上手く展開できているようです。 147 | 148 | ``` 149 | $ llvm-objdump -d shell.elf | head -n20 150 | 151 | shell.elf: file format elf32-littleriscv 152 | 153 | Disassembly of section .text: 154 | 155 | 01000000 : 156 | 1000000: 37 05 01 01 lui a0, 4112 157 | 1000004: 13 05 05 26 addi a0, a0, 608 158 | 1000008: 2a 81 mv sp, a0 159 | 100000a: 19 20 jal 0x1000010
160 | 100000c: 29 20 jal 0x1000016 161 | 100000e: 00 00 unimp 162 | 163 | 01000010
: 164 | 1000010: 01 a0 j 0x1000010
165 | 1000012: 00 00 unimp 166 | ``` 167 | 168 | ## ユーザーモードへの移行 169 | 170 | 実行イメージを展開できたので、最後の処理を実装しましょう。それは「CPUの動作モードの切り替え」です。カーネルはS-Modeと呼ばれる特権モードで動作していますが、ユーザープログラムはU-Modeと呼ばれる非特権モードで動作します。以下がその実装です。 171 | 172 | ```c [kernel.h] 173 | #define SSTATUS_SPIE (1 << 5) 174 | ``` 175 | 176 | ```c [kernel.c] 177 | // ↓ __attribute__((naked)) が追加されていることに注意 178 | __attribute__((naked)) void user_entry(void) { 179 | __asm__ __volatile__( 180 | "csrw sepc, %[sepc]\n" 181 | "csrw sstatus, %[sstatus]\n" 182 | "sret\n" 183 | : 184 | : [sepc] "r" (USER_BASE), 185 | [sstatus] "r" (SSTATUS_SPIE) 186 | ); 187 | } 188 | ``` 189 | 190 | S-ModeからU-Modeへの切り替えは、`sret`命令で行います。ただし、動作モードを切り替える前に2つ下準備をしています。 191 | 192 | - `sepc`レジスタにU-Modeに移行した際のプログラムカウンタを設定する。 193 | - `sstatus`レジスタの`SPIE`ビットを立てる。これを設定しておくと、U-Modeに入った際に割り込みが有効化され、例外と同じように`stvec`レジスタに設定しているハンドラが呼ばれるようになる。 194 | 195 | > [!TIP] 196 | > 197 | > 本書では割り込みを使わず代わりにポーリングを使うので、`SPIE`ビットを立てる必要はありません。しかし、有効化していても損はないので立てておきます。黙って割り込みを無視されるよりは分かりやすくて良いでしょう。 198 | 199 | ## 動作テスト 200 | 201 | では実際に動かしてみてみましょう。といっても、`shell.c`は無限ループするだけなので画面上では上手く動いているのか分かりません。代わりにQEMUモニタで覗いてみましょう。 202 | 203 | ``` 204 | (qemu) info registers 205 | 206 | CPU#0 207 | V = 0 208 | pc 01000010 209 | ``` 210 | 211 | レジスタダンプを見てみると、`0x1000010`をずっと実行しているようです。上手く動いている気がしますが、なんだか納得がいきません。そこで、U-Mode特有の挙動が現れるかを見てみましょう。`shell.c`に一行追加してみます。 212 | 213 | ```c [shell.c] {4} 214 | #include "user.h" 215 | 216 | void main(void) { 217 | *((volatile int *) 0x80200000) = 0x1234; 218 | for (;;); 219 | } 220 | ``` 221 | 222 | この`0x80200000`は、ページテーブル上でマップされているカーネルが利用するメモリ領域です。しかし、ページテーブルエントリの`U`ビットが立っていない「カーネル用ページ」であるため、例外 (ページフォルト) が発生するはずです。 223 | 224 | 実行してみると、期待通り例外が発生しました。 225 | 226 | ``` 227 | $ ./run.sh 228 | 229 | PANIC: kernel.c:71: unexpected trap scause=0000000f, stval=80200000, sepc=0100001a 230 | ``` 231 | 232 | `0xf = 15`番目の例外を仕様書で確認してみると「Store/AMO page fault」に対応します。期待通りの例外が発生しているようです。また、`sepc`レジスタの例外発生時のプログラムカウンタを見てみると、確かに`shell.c`に追加している行を指しています。 233 | 234 | ``` 235 | $ llvm-addr2line -e shell.elf 0x100001a 236 | /Users/seiya/dev/os-from-scratch/shell.c:4 237 | ``` 238 | 239 | 初めてのアプリケーションを実行できました! 240 | -------------------------------------------------------------------------------- /website/ja/14-system-call.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: システムコール 3 | --- 4 | 5 | # システムコール 6 | 7 | 前章では、ページフォルトをわざと起こすことでユーザーモードへの移行を確認しました。本章では、ユーザーモードで実行されているアプリケーションからカーネルの機能を呼び出す **「システムコール」** を実装します。 8 | 9 | ## システムコール呼び出し関数 (ユーザーランド側) 10 | 11 | まずはシステムコールを呼び出すユーザーランド側の実装から始めましょう。手始めに、文字を出力する `putchar` 関数をシステムコールとして実装してみます。システムコールを識別するための番号 (`SYS_PUTCHAR`) を`common.h`に定義します。 12 | 13 | ```c [common.h] 14 | #define SYS_PUTCHAR 1 15 | ``` 16 | 17 | 次にシステムコールを実際に呼び出す関数です。大体は [SBIの呼び出し](/ja/05-hello-world#初めてのsbi) の実装と同じです。 18 | 19 | ```c [user.c] 20 | int syscall(int sysno, int arg0, int arg1, int arg2) { 21 | register int a0 __asm__("a0") = arg0; 22 | register int a1 __asm__("a1") = arg1; 23 | register int a2 __asm__("a2") = arg2; 24 | register int a3 __asm__("a3") = sysno; 25 | 26 | __asm__ __volatile__("ecall" 27 | : "=r"(a0) 28 | : "r"(a0), "r"(a1), "r"(a2), "r"(a3) 29 | : "memory"); 30 | 31 | return a0; 32 | } 33 | ``` 34 | 35 | `syscall`関数は、`a3`にシステムコール番号、`a0`〜`a2`レジスタにシステムコールの引数を設定して `ecall` 命令を実行します。`ecall` 命令は、カーネルに処理を委譲するための特殊な命令です。`ecall` 命令を実行すると、例外ハンドラが呼び出され、カーネルに処理が移ります。カーネルからの戻り値は`a0`レジスタに設定されます。 36 | 37 | 最後に、次のように `putchar` 関数で `putchar`システムコールを呼び出しましょう。このシステムコールでは、第1引数として文字を渡します。第2引数以降は、未使用なので0を渡すことにします。 38 | 39 | ```c [user.c] {2} 40 | void putchar(char ch) { 41 | syscall(SYS_PUTCHAR, ch, 0, 0); 42 | } 43 | ``` 44 | 45 | ## 例外ハンドラの更新 46 | 47 | 次に、`ecall` 命令を実行したときに呼び出される例外ハンドラを更新します。 48 | 49 | ```c [kernel.h] 50 | #define SCAUSE_ECALL 8 51 | ``` 52 | 53 | ```c [kernel.c] {5-7,12} 54 | void handle_trap(struct trap_frame *f) { 55 | uint32_t scause = READ_CSR(scause); 56 | uint32_t stval = READ_CSR(stval); 57 | uint32_t user_pc = READ_CSR(sepc); 58 | if (scause == SCAUSE_ECALL) { 59 | handle_syscall(f); 60 | user_pc += 4; 61 | } else { 62 | PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc); 63 | } 64 | 65 | WRITE_CSR(sepc, user_pc); 66 | } 67 | ``` 68 | 69 | `ecall` 命令が呼ばれたのかどうかは、`scause` の値を確認することで判定できます。`handle_syscall`関数を呼び出す以外にも、`sepc`の値に4を加えています。これは、`sepc`は例外を引き起こしたプログラムカウンタ、つまり`ecall`命令を指しています。変えないままだと、`ecall`命令を無限に繰り返し実行してしまうので、命令のサイズ分 (4バイト) だけ加算することで、ユーザーモードに戻る際に次の命令から実行を再開するようにしています。 70 | 71 | ## システムコールハンドラ 72 | 73 | 例外ハンドラから呼ばれるのが次のシステムコールハンドラです。引数には、例外ハンドラで保存した「例外発生時のレジスタ」の構造体を受け取ります。 74 | 75 | ```c [kernel.c] 76 | void handle_syscall(struct trap_frame *f) { 77 | switch (f->a3) { 78 | case SYS_PUTCHAR: 79 | putchar(f->a0); 80 | break; 81 | default: 82 | PANIC("unexpected syscall a3=%x\n", f->a3); 83 | } 84 | } 85 | ``` 86 | 87 | システムコールの種類に応じて処理を分岐します。今回は、`SYS_PUTCHAR` に対応する処理を実装します。単に`a0`レジスタに入っている文字を出力するだけです。 88 | 89 | ## システムコールのテスト 90 | 91 | システムコールを一通り実装したので試してみましょう。`common.c`にある`printf`関数の実装を覚えているでしょうか。この関数は文字を表示する際に`putchar`関数を呼び出しています。たった今ユーザーランド上のライブラリで`putchar`を実装したのでそのまま使えます。 92 | 93 | ```c [shell.c] {2} 94 | void main(void) { 95 | printf("Hello World from shell!\n"); 96 | } 97 | ``` 98 | 99 | 次のようにメッセージが表示されれば成功です。 100 | 101 | ``` 102 | $ ./run.sh 103 | Hello World from shell! 104 | ``` 105 | 106 | ## 文字入力システムコール (`getchar`) 107 | 108 | 次に、文字入力を行うシステムコールを実装しましょう。SBIには「デバッグコンソールへの入力」を読む機能があります。空の場合は `-1`を返します。 109 | 110 | ```c [kernel.c] 111 | long getchar(void) { 112 | struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2); 113 | return ret.error; 114 | } 115 | ``` 116 | 117 | あとは次の通り`getchar`システムコールを実装します。 118 | 119 | ```c [common.h] 120 | #define SYS_GETCHAR 2 121 | ``` 122 | 123 | ```c [user.c] 124 | int getchar(void) { 125 | return syscall(SYS_GETCHAR, 0, 0, 0); 126 | } 127 | ``` 128 | 129 | ```c [user.h] 130 | int getchar(void); 131 | ``` 132 | 133 | ```c [kernel.c] {3-13} 134 | void handle_syscall(struct trap_frame *f) { 135 | switch (f->a3) { 136 | case SYS_GETCHAR: 137 | while (1) { 138 | long ch = getchar(); 139 | if (ch >= 0) { 140 | f->a0 = ch; 141 | break; 142 | } 143 | 144 | yield(); 145 | } 146 | break; 147 | /* 省略 */ 148 | } 149 | } 150 | ``` 151 | 152 | `getchar`システムコールの実装は、文字が入力されるまでSBIを繰り返し呼び出します。ただし、単純に繰り返すとCPUを占有してしまうので、`yield`システムコールを呼び出してCPUを他のプロセスに譲るようにしています。 153 | 154 | ## シェルを書こう 155 | 156 | 文字入力ができるようになったので、シェルを書いてみましょう。手始めに、`Hello world from shell!`と表示する`hello`コマンドを実装します。 157 | 158 | ```c [shell.c] 159 | void main(void) { 160 | while (1) { 161 | prompt: 162 | printf("> "); 163 | char cmdline[128]; 164 | for (int i = 0;; i++) { 165 | char ch = getchar(); 166 | putchar(ch); 167 | if (i == sizeof(cmdline) - 1) { 168 | printf("command line too long\n"); 169 | goto prompt; 170 | } else if (ch == '\r') { 171 | printf("\n"); 172 | cmdline[i] = '\0'; 173 | break; 174 | } else { 175 | cmdline[i] = ch; 176 | } 177 | } 178 | 179 | if (strcmp(cmdline, "hello") == 0) 180 | printf("Hello world from shell!\n"); 181 | else 182 | printf("unknown command: %s\n", cmdline); 183 | } 184 | } 185 | ``` 186 | 187 | 改行が来るまで文字を読み込んでいき、入力された文字列がコマンド名に完全一致するかをチェックする、非常に単純な実装です。デバッグコンソール上では改行が (`'\r'`) でやってくるので注意してください。 188 | 189 | 実際に動かしてみて、文字が入力されるか、そして`hello`コマンドが動くか確認してみましょう。 190 | 191 | ``` 192 | $ ./run.sh 193 | 194 | > hello 195 | Hello world from shell! 196 | ``` 197 | 198 | ## プロセスの終了 (`exit`システムコール) 199 | 200 | 最後に、プロセスを終了する`exit`システムコールを実装します。 201 | 202 | ```c [common.h] 203 | #define SYS_EXIT 3 204 | ``` 205 | 206 | ```c [user.c] {2-3} 207 | __attribute__((noreturn)) void exit(void) { 208 | syscall(SYS_EXIT, 0, 0, 0); 209 | for (;;); // 念のため 210 | } 211 | ``` 212 | 213 | ```c [kernel.h] 214 | #define PROC_EXITED 2 215 | ``` 216 | 217 | ```c [kernel.c] {3-7} 218 | void handle_syscall(struct trap_frame *f) { 219 | switch (f->a3) { 220 | case SYS_EXIT: 221 | printf("process %d exited\n", current_proc->pid); 222 | current_proc->state = PROC_EXITED; 223 | yield(); 224 | PANIC("unreachable"); 225 | /* 省略 */ 226 | } 227 | } 228 | ``` 229 | 230 | まず、プロセスの状態を`PROC_EXITED`に変更し、`yield`システムコールを呼び出してCPUを他のプロセスに譲ります。スケジューラは`PROC_RUNNABLE`のプロセスしか実行しないため、このプロセスに戻ってくることはありません。ただし念の為、`PANIC`マクロで万が一戻ってきた場合はパニックを起こします。 231 | 232 | > [!TIP] 233 | > 234 | > 分かりやすさのためにプロセスの状態を変えているだけで、プロセス管理構造体を開放していません。実用的なOSを目指したい時には、ページテーブルや割り当てられたメモリ領域など、プロセスが持つ資源を開放する必要があります。 235 | 236 | 最後に、シェルに`exit`コマンドを追加します。 237 | 238 | ```c [shell.c] {3-4} 239 | if (strcmp(cmdline, "hello") == 0) 240 | printf("Hello world from shell!\n"); 241 | else if (strcmp(cmdline, "exit") == 0) 242 | exit(); 243 | else 244 | printf("unknown command: %s\n", cmdline); 245 | ``` 246 | 247 | 実際に動かしてみましょう。 248 | 249 | ``` 250 | $ ./run.sh 251 | 252 | > exit 253 | process 2 exited 254 | PANIC: kernel.c:333: switched to idle process 255 | ``` 256 | 257 | `exit`コマンドを実行するとシェルプロセスが終了し、他に実行可能なプロセスがなくなります。そのため、スケジューラがアイドルプロセスを選ぶという流れになります。 258 | -------------------------------------------------------------------------------- /website/ja/17-outro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: おわりに 3 | --- 4 | 5 | # おわりに 6 | 7 | ここで本書は終わりです。1000行足らずではありますが、なかなか大変だったのではないでしょうか。 8 | 9 | 「まだ物足りない!まだ何か続けたい」という方のために「次にすること」をいくつか紹介します。 10 | 11 | ## HinaOSやxv6を読もう 12 | 13 | ページングや例外処理といった、アプリケーション開発では見られない独特な機能の実装を学んだ今、一番おすすめなのが「既存のOSの実装を読んでみる」ことです。自分の実装と比較して「他の人がどう実装しているのか」を学ぶのは大変勉強になります。 14 | 15 | おすすめが2つあります。1つ目は拙作の[HinaOS](https://github.com/nuta/microkernel-book)です。本書と同じくRISC-V 32ビット向けの教育用OSです。マルチプロセッサ対応のマイクロカーネルOSで、TCP/IPプロトコルスタックも実装されています。筆者が書いたOSということもあり、本書を読んだ方なら比較的読みやすいでしょう。 16 | 17 | 2つ目は[RISC-V版 xv6](https://github.com/mit-pdos/xv6-riscv)です。こちらは教育用UNIX風OSで、[解説書 (英語)](https://pdos.csail.mit.edu/6.828/2022/)もあります。`fork(2)`のようなUNIX特有の機能の仕組みを学びたい方におすすめです。 18 | 19 | ## 新しい機能を追加しよう 20 | 21 | 本書では、カーネルの基本的な機能を実装しました。しかし、まだまだ実装できる機能はたくさんあります。例えば、次のような機能を実装してみると面白いでしょう。 22 | 23 | - まともなメモリアロケータ 24 | - 割り込み 25 | - 本格的なファイルシステム 26 | - ネットワーク通信 (TCP/IP) 27 | 28 | [HinaOSの実験テーマ集](https://github.com/nuta/microkernel-book/blob/main/IDEAS.md) もアイデア出しに役立つでしょう。 29 | -------------------------------------------------------------------------------- /website/ja/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: はじめに 3 | --- 4 | 5 | # 1000行でOSを作ってみよう 6 | 7 | 本書では、小さなOSをゼロから少しずつ作っていきます。 8 | 9 | OSと聞くと腰が引けるかもしれませんが、OS (特にカーネル) の基本機能は案外単純です。巨大なオープンソースソフトウェアとしてよく挙げられるLinuxでさえ、バージョン0.01はたった8413行でした。様々な要求に応えるために次第に肥大化していっただけで、当初は大変素朴な実装になっていました。 10 | 11 | 本書ではコンテキストスイッチ、ページング、ユーザーモード、コマンドラインシェル、ディスクデバイスドライバ、ファイルの読み書きをC言語で実装します。これだけ様々な機能が詰め込まれているのに、コードは合計でたった1000行未満です。 12 | 13 | 「1000行なら1日でできそう」と思うかもしれませんが、初学者には少なくとも3日はかかるでしょう。OS自作のハマりポイントは「デバッグ」です。アプリケーション開発とは違うデバッグ手法・能力を習得する必要があります。特に「ゼロから」の自作となると、ブート処理やページングといったデバッグが難しく挫折しやすい部分が冒頭に登場します。そこで本書では「自作OSをどうデバッグすればよいのか」も一緒に学んでいきます。 14 | 15 | ただ、デバッグが辛い分、動いた時の感動もひとしおです。ぜひ本書を通じて、OS開発という楽しい世界を体験してみてください。 16 | 17 | - 実装例は [GitHub](https://github.com/nuta/operating-system-in-1000-lines) からダウンロードできます。 18 | - 本書は [CC BY 4.0ライセンス](https://creativecommons.jp/faq) の下で利用できます。実装例と本文中のソースコードは [MITライセンス](https://opensource.org/licenses/MIT) です。 19 | - 前提知識は「C言語を理解していること」と「UNIX系のコマンドラインシェルに慣れていること」です。RustやZigなど好きな言語で挑戦するのも面白いでしょう。 20 | - **本書はあくまで[『自作OSで学ぶマイクロカーネルの設計と実装』](https://www.shuwasystem.co.jp/book/9784798068718.html) (愛称: エナガ本) の補足資料です。** エナガ本で既に解説されている部分は本書では省略しています。 21 | -------------------------------------------------------------------------------- /website/ko/01-setting-up-development-environment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 환경설정 3 | --- 4 | 5 | # 개발환경 6 | 7 | 이 책은 기본적으로 UNIX 계열 운영체제(예: macOS, Ubuntu)에서의 작업을 가정합니다. Windows를 사용한다면, 먼저 Windows Subsystem for Linux(WSL2)를 설치한 뒤, 아래의 Ubuntu 절차를 따르는 것을 추천합니다. 8 | 9 | ## 개발 툴 설치 10 | 11 | ### macOS 12 | 13 | 1. [Homebrew](https://brew.sh) 를 설치합니다. 14 | 2. 터미널에서 다음 명령어를 실행하여 필요한 프로그램들을 설치합니다. 15 | 16 | ``` 17 | brew install llvm lld qemu 18 | ``` 19 | 20 | ### Ubuntu 21 | 22 | 1. apt 패키지 관리자 업데이트 후, 필요한 패키지들을 설치합니다. 23 | 24 | 25 | ``` 26 | sudo apt update && sudo apt install -y clang llvm lld qemu-system-riscv32 curl 27 | ``` 28 | 29 | 2. 추가로, OpenSBI 펌웨어를 다운로드합니다. 이는 PC의 BIOS/UEFI에 해당하는 역할을 합니다. 30 | 31 | ``` 32 | curl -LO https://github.com/qemu/qemu/raw/v8.0.4/pc-bios/opensbi-riscv32-generic-fw_dynamic.bin 33 | ``` 34 | 35 | > [!WARNING] 36 | > 37 | > QEMU를 실행할 때, opensbi-riscv32-generic-fw_dynamic.bin 파일이 현재 디렉터리에 반드시 있어야 합니다. 파일이 없으면 아래와 같은 오류가 발생합니다: 38 | > 39 | > ``` 40 | > qemu-system-riscv32: Unable to load the RISC-V firmware "opensbi-riscv32-generic-fw_dynamic.bin" 41 | > ``` 42 | 43 | ### 그 외의 운영체제 44 | 45 | 꼭 다른 OS를 써야 한다면, 다음 프로그램들을 직접 설치해야 합니다. 46 | 47 | - `bash`: 일반적인 명령어 쉘 (대부분 기본 설치되어 있음) 48 | - `tar`: tar 아카이브 툴 (대부분 기본 설치됨, BSD가 아닌 GNU버전 tar 권장) 49 | - `clang`: C 컴파일러 (반드시 32비트 RISC-V를 지원해야 함, 아래 참조) 50 | - `lld`: LLVM 링커 (여러 오브젝트 파일을 한 실행 파일로 묶음) 51 | - `llvm-objcopy`: 오브젝트 파일 편집 툴 (주로 LLVM 패키지에 포함, GNU binutils의 objcopy도 대체 가능) 52 | - `llvm-objdump`: 역어셈블러 (위 LLVM 패키지와 함께 제공) 53 | - `llvm-readelf`: ELF 파일 분석 툴 (역시 LLVM 패키지에 포함) 54 | - `qemu-system-riscv32`: 32비트 RISC-V CPU 에뮬레이터 (QEMU 패키지에 포함) 55 | 56 | 57 | > [!TIP] 58 | > 59 | > 사용 중인 `clang`이 32비트 RISC-V를 지원하는지 확인하려면 아래 명령어를 써보세요. 60 | > 61 | > ``` 62 | > $ clang -print-targets | grep riscv32 63 | > riscv32 - 32-bit RISC-V 64 | > ``` 65 | > 66 | > 여기서 riscv32 항목이 나오면 지원된다는 뜻입니다. 예를 들어, macOS 기본 clang은 32비트 RISC-V를 지원하지 않는 경우가 많기 때문에, Homebrew를 통해 설치한 llvm 패키지를 사용하는 것입니다. 67 | 68 | ## Git 레포지토리 설정 (선택사항) 69 | 70 | 프로젝트를 Git으로 관리하는 경우, 아래 .gitignore 파일을 만들어두면 여러모로 편리합니다. 71 | 72 | ```gitignore [.gitignore] 73 | /disk/* 74 | !/disk/.gitkeep 75 | *.map 76 | *.tar 77 | *.o 78 | *.elf 79 | *.bin 80 | *.log 81 | *.pcap 82 | ``` 83 | 84 | 이제 끝났습니다! 이제 바로 OS 만들기의 첫 여정을 시작해봅시다! 85 | -------------------------------------------------------------------------------- /website/ko/03-overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 기능 및 구조 미리보기 3 | --- 4 | 5 | # 기능 및 구조 미리보기 6 | 7 | OS를 본격적으로 만들기 시작하기 전에, 이 책에서 구현할 기능들을 간단히 살펴보겠습니다. 8 | 9 | ## 1,000줄 짜리 OS에서 구현할 주요 기능 10 | 11 | 다음과 같은 주요 기능들을 구현할 예정입니다: 12 | 13 | - **멀티태스킹(Multitasking)**: 여러 애플리케이션이 CPU를 공유할 수 있도록 프로세스 간 전환을 지원합니다. 14 | - **예외 처리(Exception Handler)**: 잘못된 명령어 처리 등, OS의 개입이 필요한 이벤트를 다룹니다. 15 | - **페이징(Paging)**: 각 애플리케이션에 독립적인 메모리 주소 공간을 제공합니다. 16 | - **시스템 콜(System Calls)**: 애플리케이션이 커널 기능을 호출할 수 있는 방법을 제공합니다. 17 | - **디바이스 드라이버(Device Drivers)**: 디스크 읽기/쓰기처럼 하드웨어 기능을 추상화합니다. 18 | - **파일 시스템(File System)**: 디스크 상에서 파일을 관리합니다. 19 | - **명령줄 셸(Command-line Shell)**: 사람이 사용하는 명령줄 기반 인터페이스를 제공합니다. 20 | 21 | 22 | ## 이 책에서 다루지 않는 기능 23 | 24 | 아래와 같은 기능들은 이번 책에서 구현하지 않습니다: 25 | 26 | - **인터럽트 처리(Interrupt Handling)**: 대신 주기적으로 디바이스를 확인하는 폴링(polling) 방식을 사용합니다(busy waiting). 27 | - **타이머 처리(Timer Processing)**: 선점형(preemptive) 멀티태스킹은 구현하지 않습니다. 각 프로세스가 스스로 CPU를 양보하는 협력형(cooperative) 멀티태스킹을 사용할 예정입니다. 28 | - **프로세스 간 통신(Inter-process Communication)**: 파이프, UNIX 도메인 소켓, 공유 메모리 등의 기능은 구현하지 않습니다. 29 | - **멀티프로세서 지원(Multi-processor Support)**: 단일 프로세서만 지원합니다. 30 | 31 | ## 소스 코드 구조 32 | 33 | 맨바닥부터 점진적으로 빌드해나갈 것이며, 최종적으로 아래와 같은 파일 구조가 완성됩니다: 34 | 35 | ``` 36 | ├── disk/ - 파일 시스템용 디렉터리 37 | ├── common.c - 커널/유저 공용 라이브러리(printf, memset 등) 38 | ├── common.h - 커널/유저 공용 라이브러리용 구조체 및 상수 정의 39 | ├── kernel.c - 커널(프로세스 관리, 시스템 콜, 디바이스 드라이버, 파일 시스템 등) 40 | ├── kernel.h - 커널용 구조체 및 상수 정의 41 | ├── kernel.ld - 커널용 링커 스크립트(메모리 레이아웃 정의) 42 | ├── shell.c - 명령줄 셸 43 | ├── user.c - 유저 라이브러리(시스템 콜 관련 함수) 44 | ├── user.h - 유저 라이브러리용 구조체 및 상수 정의 45 | ├── user.ld - 유저용 링커 스크립트(메모리 레이아웃 정의) 46 | └── run.sh - 빌드 스크립트 47 | ``` 48 | 49 | > [!TIP] 50 | > 51 | > 이 책에서 "user land"라는 용어는 종종 "user"로 축약해 사용됩니다. 이는 계정 단위의 “사용자(user account)”가 아니라, 애플리케이션 영역을 뜻하는 “유저 영역(user land)” 개념이니 혼동하지 마세요! 52 | -------------------------------------------------------------------------------- /website/ko/06-libc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: C 표준 라이브러리 3 | --- 4 | 5 | # C 표준 라이브러리 6 | 7 | 이 장에서는 기본 타입과 메모리 조작, 문자열 조작 함수를 직접 구현해 봅시다. 학습 목적으로, C 표준 라이브러리를 사용하지 않고 직접 만들어 볼 예정입니다. 8 | 9 | 10 | > [!TIP] 11 | > 12 | > 이 장에서 소개하는 개념은 C 프로그래밍에서 매우 흔하므로 ChatGPT가 좋은 답변을 제공할 것입니다. 구현이나 이해에 어려움을 겪는 부분이 있다면 질문해보거나 저에게 알려주세요. 13 | 14 | ## 기본 타입들 15 | 16 | 먼저 `common.h`에 기본 타입과 편리한 매크로를 정의해 봅시다: 17 | 18 | 19 | ```c [common.h] {1-15,21-24} 20 | typedef int bool; 21 | typedef unsigned char uint8_t; 22 | typedef unsigned short uint16_t; 23 | typedef unsigned int uint32_t; 24 | typedef unsigned long long uint64_t; 25 | typedef uint32_t size_t; 26 | typedef uint32_t paddr_t; 27 | typedef uint32_t vaddr_t; 28 | 29 | #define true 1 30 | #define false 0 31 | #define NULL ((void *) 0) 32 | #define align_up(value, align) __builtin_align_up(value, align) 33 | #define is_aligned(value, align) __builtin_is_aligned(value, align) 34 | #define offsetof(type, member) __builtin_offsetof(type, member) 35 | #define va_list __builtin_va_list 36 | #define va_start __builtin_va_start 37 | #define va_end __builtin_va_end 38 | #define va_arg __builtin_va_arg 39 | 40 | void *memset(void *buf, char c, size_t n); 41 | void *memcpy(void *dst, const void *src, size_t n); 42 | char *strcpy(char *dst, const char *src); 43 | int strcmp(const char *s1, const char *s2); 44 | void printf(const char *fmt, ...); 45 | ``` 46 | 47 | 대부분 C 표준 라이브러리에 이미 있는 내용이지만, 몇 가지 유용한 것들을 추가했습니다. 48 | 49 | 50 | 51 | - `paddr_t`: 물리 메모리 주소를 나타내는 타입. 52 | - `vaddr_t`: 가상 메모리 주소를 나타내는 타입. 표준 라이브러리의 `uintptr_t`에 해당합니다. 53 | - `align_up`: `value`를 `align`의 배수로 맞춰 올림합니다. 여기서 `align`은 2의 거듭제곱이어야 합니다. 54 | - `is_aligned`: `value`가 `align`의 배수인지 확인합니다. 마찬가지로 `align`은 2의 거듭제곱이어야 합니다. 55 | - `offsetof`: 구조체 내에서 특정 멤버가 시작되는 위치(바이트 단위)를 반환합니다. 56 | 57 | 58 | 예를 들어 `align_up(0x1234, 0x1000)`은 `0x2000`이 되고, `is_aligned(0x2000, 0x1000)`은 true를 반환하지만 `is_aligned(0x2f00, 0x1000)`은 false를 반환합니다. 59 | 60 | 위 매크로들은 Clang의 확장 기능인 `__builtin_` 함수를 사용했습니다. (자세한 내용은 [Clang built-in functions and macros](https://clang.llvm.org/docs/LanguageExtensions.html)를 참고하세요.) 61 | 62 | > [!TIP] 63 | > 64 | > 이 매크로들은 빌트인 함수를 사용하지 않고, 순수 C 코드로도 구현할 수 있습니다. 특히 `offsetof`의 전통적인 C 구현은 꽤 흥미롭습니다. 65 | 66 | 67 | ## 메모리 조작 함수 68 | 69 | 다음으로, 메모리 조작 함수들을 구현해 봅시다. 70 | 71 | ### memcpy 72 | 73 | `memcpy` 함수는 `src`에서 `dst`로 `n` 바이트를 복사합니다: 74 | 75 | ```c [common.c] 76 | void *memcpy(void *dst, const void *src, size_t n) { 77 | uint8_t *d = (uint8_t *) dst; 78 | const uint8_t *s = (const uint8_t *) src; 79 | while (n--) 80 | *d++ = *s++; 81 | return dst; 82 | } 83 | ``` 84 | 85 | ### memset 86 | 87 | `memset`은 `buf`의 시작 위치부터 `n`바이트를 문자 `c`로 채웁니다. 예전(4장)에는 BSS 섹션 초기화 용도로 `kernel.c`에서 잠시 썼는데, 이제 `common.c`로 옮깁니다. 88 | 89 | ```c [common.c] 90 | void *memset(void *buf, char c, size_t n) { 91 | uint8_t *p = (uint8_t *) buf; 92 | while (n--) 93 | *p++ = c; 94 | return buf; 95 | } 96 | ``` 97 | 98 | > [!TIP] 99 | > 100 | > *p++ = c; 구문은 포인터 역참조와 포인터 이동이 한 줄에서 동시에 이루어집니다. 더 명확히 쓰면 다음과 같습니다: 101 | > 102 | > ```c 103 | > *p = c; // Dereference the pointer 104 | > p = p + 1; // Advance the pointer after the assignment 105 | > ``` 106 | 107 | 108 | ## 문자열 조작 함수 109 | 110 | ### strcpy 111 | 112 | 이제 `strcpy` 함수도 살펴봅시다. 이 함수는 `src` 문자열을 `dst`로 복사합니다. 113 | 114 | ```c [common.c] 115 | char *strcpy(char *dst, const char *src) { 116 | char *d = dst; 117 | while (*src) 118 | *d++ = *src++; 119 | *d = '\0'; 120 | return dst; 121 | } 122 | ``` 123 | 124 | > [!WARNING] 125 | > 126 | > `strcpy`는 `src`가 `dst`보다 길어도 무조건 계속 복사하기 때문에, 매우 위험할 수 있습니다. 버그나 보안 취약점의 원인이 되기 쉽죠. 실제로는 `strcpy` 대신 `strcpy_s` 같은 안전한 함수를 쓰는 것이 권장됩니다. 127 | > 128 | > 여기서는 단순히 학습을 위해 strcpy를 사용합니다만, 실무에서는 strcpy는 피해야 합니다! 129 | 130 | 131 | ### strcmp 132 | 133 | 다음은 `strcmp` 함수입니다. s1과 s2를 비교하여, 134 | 135 | - 같으면 0, 136 | - s1이 더 크면 양수, 137 | - s1이 더 작으면 음수를 반환합니다. 138 | 139 | | Condition | Result | 140 | | --------- |-------| 141 | | `s1` == `s2` | 0 | 142 | | `s1` > `s2` | 양수 | 143 | | `s1` < `s2` | 음수 | 144 | 145 | ```c [common.c] 146 | int strcmp(const char *s1, const char *s2) { 147 | while (*s1 && *s2) { 148 | if (*s1 != *s2) 149 | break; 150 | s1++; 151 | s2++; 152 | } 153 | 154 | return *(unsigned char *)s1 - *(unsigned char *)s2; 155 | } 156 | ``` 157 | 158 | > [!TIP] 159 | > 160 | > `*(unsigned char *)s1`로 캐스팅하는 이유는 [POSIX 명세](https://www.man7.org/linux/man-pages/man3/strcmp.3.html#:~:text=both%20interpreted%20as%20type%20unsigned%20char)에 따르면, 문자를 비교할 때 `unsigned char`로 해석하도록 권장하기 때문입니다. 161 | 162 | 예를 들어, 아래처럼 두 문자열이 같은지 비교할 때 사용합니다. `strcmp(s1, s2)`가 `0`을 반환하면 두 문자열이 동일하다는 뜻입니다. 163 | 164 | 165 | ```c 166 | if (!strcmp(s1, s2)) 167 | printf("s1 == s2\n"); 168 | else 169 | printf("s1 != s2\n"); 170 | ``` 171 | -------------------------------------------------------------------------------- /website/ko/07-kernel-panic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 커널 패닉 (Kernel Panic) 3 | --- 4 | 5 | # 커널 패닉 (Kernel Panic) 6 | 7 | 커널 패닉은 커널에서 복구 불가능한 오류를 만났을 때 발생합니다. Go나 Rust에서의 `panic`과 비슷한 개념입니다. 윈도우에서 가끔 보이는 “블루 스크린”을 떠올려보면 이해가 쉽죠. 우리의 OS에도 크리티컬한 오류가 발생했을 때 커널이 알아서 멈추도록 구현해봅시다. 8 | 9 | 다음 `PANIC` 매크로가 그 역할을 합니다. 10 | 11 | ```c [kernel.h] 12 | #define PANIC(fmt, ...) \ 13 | do { \ 14 | printf("PANIC: %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 15 | while (1) {} \ 16 | } while (0) 17 | ``` 18 | 19 | 이 매크로는 패닉이 발생한 소스 파일과 줄 번호를 출력한 뒤, 무한 루프에 빠져서 더 이상의 처리를 중단합니다. 매크로로 정의한 이유는 `__FILE__`과 `__LINE__`이 제대로 호출 위치의 파일명과 줄 번호를 표시하도록 하기 위해서입니다. 만약 함수를 썼다면, 이 매개변수들은 함수가 정의된 위치(매크로 정의가 들어 있는 파일/라인)로 잘못 표시될 것입니다. 20 | 21 | 이 매크로에서는 아래 두 가지 유용한 기법을 사용하고 있습니다: 22 | 23 | 1. **do-while(0) 구조** : while (0)이므로 이 루프는 한 번만 실행됩니다. 여러 문장으로 이루어진 매크로를 do { ... } while (0)로 묶으면, 다른 구문(예: if 문)과 함께 사용할 때 의도치 않은 동작이 발생하는 것을 피할 수 있습니다. 그리고 각 줄 끝의 역슬래시(`\`)에 주의하세요. 매크로가 여러 줄에 걸쳐서 정의되어 있어도, 매크로 전개 시에는 줄바꿈이 무시됩니다. 24 | 25 | 2. **`##__VA_ARGS__` 문법** : 매개변수가 가변적인 매크로를 정의할 때 유용한 확장 기능입니다. (참고: [GCC 문서](https://gcc.gnu.org/onlinedocs/gcc/Variadic-Macros.html)) `##`는 가변 인자가 비어 있을 경우, 불필요한 쉼표(,)를 제거해줍니다. 예를 들어 `PANIC("booted!")`처럼 인자가 하나뿐이어도 오류 없이 잘 컴파일되도록 해줍니다. 26 | 27 | 28 | ## 예시 29 | 30 | `PANIC`을 한 번 사용해봅시다. 그냥, `printf`처럼 쓰면 됩니다: 31 | 32 | ```c [kernel.c] {4-5} 33 | void kernel_main(void) { 34 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 35 | 36 | PANIC("booted!"); 37 | printf("unreachable here!\n"); 38 | } 39 | ``` 40 | 41 | `QEMU`에서 실행했을 때, 커널 패닉이 실제로 발생하고 아래처럼 정확한 파일명과 줄 번호가 찍히는지, 그리고 `PANIC` 뒤의 코드는 전혀 실행되지 않는지(즉, `"unreachable here!"`가 출력되지 않는지) 확인해보세요. 42 | 43 | 44 | ``` 45 | $ ./run.sh 46 | PANIC: kernel.c:46: booted! 47 | ``` 48 | 49 | 윈도우의 블루 스크린이나 리눅스의 커널 패닉처럼 한편으론 무서운 기능이지만, 우리가 만드는 커널 안에서는 사용자에게 원인을 알려주고 멈출 수 있는 “우아한 크래시” 메커니즘이라고 할 수 있습니다. 이를 통해 인간이 이해할 수 있는 오류 메시지를 띄우고 시스템이 안전하게 멈추도록 하는 것이죠. 50 | 51 | -------------------------------------------------------------------------------- /website/ko/08-exception.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 예외 (Exception) 3 | --- 4 | 5 | # 예외 (Exception) 6 | 7 | `Exception`은 CPU가 잘못된 메모리 접근(일명 페이지 폴트), 유효하지 않은 명령(Illegal Instructions), 그리고 시스템 콜 같은 다양한 이벤트가 발생했을 때 커널이 개입하도록 해주는 CPU 기능입니다. 8 | 9 | 비유하자면, C++나 Java의 `try-catch`와 유사한 하드웨어 지원 메커니즘입니다. CPU는 커널이 개입해야 할 상황이 발생하기 전까지는 계속 프로그램을 실행하다가, 문제가 발생하면 예외가 발생해 커널이 개입하게 됩니다. `try-catch`와 다른 점은, 예외가 발생한 지점에서 커널이 개입했다가, 처리 후에 다시 “아무 일 없었던 것처럼” 같은 지점부터 프로그램을 재개할 수 있다는 것입니다. 꽤 멋진 기능이죠? 10 | 11 | `Exception`은 커널 모드에서도 발생할 수 있으며, 대부분 치명적인 커널 버그로 이어집니다. QEMU가 갑자기 리셋되거나 커널이 이상 동작한다면, 예외가 발생했을 가능성이 큽니다. 예외 핸들러를 일찍 구현해두면, 이런 경우 커널 패닉을 발생시켜 좀 더 우아하게 디버깅할 수 있습니다. 웹 개발에서 자바스크립트의 “unhandled rejection” 핸들러를 제일 먼저 달아두는 것과 비슷한 개념입니다. 12 | 13 | 14 | 15 | ## 예외가 처리되는 과정 16 | 17 | RISC-V에서 예외는 다음과 같은 단계를 거쳐 처리됩니다: 18 | 19 | 1. CPU는 `medeleg` 레지스터를 확인하여 어떤 모드(운영 모드)에서 예외를 처리할지 결정합니다. 여기서는 OpenSBI가 이미 U-Mode와 S-Mode 예외를 S-Mode 핸들러에서 처리하도록 설정해두었습니다. 20 | 2. CPU는 예외가 발생한 시점의 상태(각종 레지스터 값)를 여러 CSR(제어/상태 레지스터)들에 저장합니다(아래 표 참조). 21 | 3. `stvec` 레지스터에 저장된 값이 프로그램 카운터로 설정되면서, 커널의 예외 핸들러로 점프합니다. 22 | 4. 예외 핸들러는 일반 레지스터(프로그램 상태)를 별도로 저장한 뒤, 예외를 처리합니다. 23 | 5. 처리 후, 저장해둔 실행 상태를 복원하고 `sret` 명령어를 실행해 예외가 발생했던 지점으로 돌아가 프로그램을 재개합니다. 24 | 6. 2번 단계에서 업데이트되는 주요 CSR은 아래와 같습니다. 커널은 이 정보들을 기반으로 예외를 처리합니다: 25 | 26 | | 레지스터 | 내용 | 27 | |----------|------------------------------------------------------| 28 | | `scause` | 예외 유형. 커널은 이를 읽어 어떤 종류의 예외인지 판단합니다. | 29 | | `stval` | 예외에 대한 부가 정보(예: 문제를 일으킨 메모리 주소). 예외 종류에 따라 다르게 사용됩니다. | 30 | | `sepc` | 예외가 발생했을 때의 프로그램 카운터(PC) 값. | 31 | | `sstatus` | 예외가 발생했을 때의 운영 모드(U-Mode/S-Mode 등). | 32 | 33 | ## 예외 핸들러 (Exception Handler) 구현 34 | 35 | 이제 첫 번째 예외 핸들러를 구현해봅시다! 아래 코드는 `stvec` 레지스터에 등록할 예외 핸들러 진입점(entry point) 예시입니다: 36 | 37 | ```c [kernel.c] 38 | __attribute__((naked)) 39 | __attribute__((aligned(4))) 40 | void kernel_entry(void) { 41 | __asm__ __volatile__( 42 | "csrw sscratch, sp\n" 43 | "addi sp, sp, -4 * 31\n" 44 | "sw ra, 4 * 0(sp)\n" 45 | "sw gp, 4 * 1(sp)\n" 46 | "sw tp, 4 * 2(sp)\n" 47 | "sw t0, 4 * 3(sp)\n" 48 | "sw t1, 4 * 4(sp)\n" 49 | "sw t2, 4 * 5(sp)\n" 50 | "sw t3, 4 * 6(sp)\n" 51 | "sw t4, 4 * 7(sp)\n" 52 | "sw t5, 4 * 8(sp)\n" 53 | "sw t6, 4 * 9(sp)\n" 54 | "sw a0, 4 * 10(sp)\n" 55 | "sw a1, 4 * 11(sp)\n" 56 | "sw a2, 4 * 12(sp)\n" 57 | "sw a3, 4 * 13(sp)\n" 58 | "sw a4, 4 * 14(sp)\n" 59 | "sw a5, 4 * 15(sp)\n" 60 | "sw a6, 4 * 16(sp)\n" 61 | "sw a7, 4 * 17(sp)\n" 62 | "sw s0, 4 * 18(sp)\n" 63 | "sw s1, 4 * 19(sp)\n" 64 | "sw s2, 4 * 20(sp)\n" 65 | "sw s3, 4 * 21(sp)\n" 66 | "sw s4, 4 * 22(sp)\n" 67 | "sw s5, 4 * 23(sp)\n" 68 | "sw s6, 4 * 24(sp)\n" 69 | "sw s7, 4 * 25(sp)\n" 70 | "sw s8, 4 * 26(sp)\n" 71 | "sw s9, 4 * 27(sp)\n" 72 | "sw s10, 4 * 28(sp)\n" 73 | "sw s11, 4 * 29(sp)\n" 74 | 75 | "csrr a0, sscratch\n" 76 | "sw a0, 4 * 30(sp)\n" 77 | 78 | "mv a0, sp\n" 79 | "call handle_trap\n" 80 | 81 | "lw ra, 4 * 0(sp)\n" 82 | "lw gp, 4 * 1(sp)\n" 83 | "lw tp, 4 * 2(sp)\n" 84 | "lw t0, 4 * 3(sp)\n" 85 | "lw t1, 4 * 4(sp)\n" 86 | "lw t2, 4 * 5(sp)\n" 87 | "lw t3, 4 * 6(sp)\n" 88 | "lw t4, 4 * 7(sp)\n" 89 | "lw t5, 4 * 8(sp)\n" 90 | "lw t6, 4 * 9(sp)\n" 91 | "lw a0, 4 * 10(sp)\n" 92 | "lw a1, 4 * 11(sp)\n" 93 | "lw a2, 4 * 12(sp)\n" 94 | "lw a3, 4 * 13(sp)\n" 95 | "lw a4, 4 * 14(sp)\n" 96 | "lw a5, 4 * 15(sp)\n" 97 | "lw a6, 4 * 16(sp)\n" 98 | "lw a7, 4 * 17(sp)\n" 99 | "lw s0, 4 * 18(sp)\n" 100 | "lw s1, 4 * 19(sp)\n" 101 | "lw s2, 4 * 20(sp)\n" 102 | "lw s3, 4 * 21(sp)\n" 103 | "lw s4, 4 * 22(sp)\n" 104 | "lw s5, 4 * 23(sp)\n" 105 | "lw s6, 4 * 24(sp)\n" 106 | "lw s7, 4 * 25(sp)\n" 107 | "lw s8, 4 * 26(sp)\n" 108 | "lw s9, 4 * 27(sp)\n" 109 | "lw s10, 4 * 28(sp)\n" 110 | "lw s11, 4 * 29(sp)\n" 111 | "lw sp, 4 * 30(sp)\n" 112 | "sret\n" 113 | ); 114 | } 115 | ``` 116 | 117 | 주요 포인트 118 | - `sscratch` 레지스터를 임시 저장소로 이용해 예외 발생 시점의 스택 포인터를 저장해두고, 나중에 복구합니다. 119 | - 커널에서는 부동소수점(FPU) 레지스터를 사용하지 않으므로(일반적으로 쓰레드 스위칭 시에만 저장/복원), 여기서는 저장하지 않았습니다. 120 | - 스택 포인터(`sp`) 값을 `a0` 레지스터에 넘겨 `handle_trap` 함수를 C 코드로 호출합니다. 이때 sp가 가리키는 곳에는 조금 뒤에 소개할 `trap_frame` 구조체와 동일한 형태로 레지스터들이 저장되어 있습니다. 121 | - `__attribute__((aligned(4)))`는 함수 시작 주소를 4바이트 경계에 맞추기 위함입니다. `stvec` 레지스터는 예외 핸들러 주소뿐 아니라 하위 2비트를 모드 정보 플래그로 사용하기 때문에, 핸들러 주소가 4바이트 정렬이 되어 있어야 합니다. 122 | 123 | > [!NOTE] 124 | > 125 | > 예외 핸들러 진입점(entry point)은 커널에서 가장 까다롭고 실수하기 쉬운 부분 중 하나입니다. 코드를 자세히 보면, 원래의 일반 레지스터 값을 전부 스택에 저장하되, `sp`는 `sscratch`를 통해 우회적으로 저장하고 있음을 알 수 있습니다. 126 | > 127 | > 만약 `a0` 레지스터를 잘못 덮어써버리면, “지역 변수 값이 이유 없이 바뀐다” 같은 디버깅하기 어려운 문제들을 일으킬 수 있습니다. 금요일 밤을 야근에 쏟고 싶지 않다면, 프로그램 상태를 잘 저장해두세요! 128 | 129 | 130 | 위 진입점 코드에서는 `handle_trap` 함수를 호출해, 예외 처리를 C 언어로 진행합니다: 131 | 132 | 133 | ```c [kernel.c] 134 | void handle_trap(struct trap_frame *f) { 135 | uint32_t scause = READ_CSR(scause); 136 | uint32_t stval = READ_CSR(stval); 137 | uint32_t user_pc = READ_CSR(sepc); 138 | 139 | PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc); 140 | } 141 | ``` 142 | 143 | `scause`가 어떤 이유로 예외가 발생했는지 알려주고, `stval`은 예외 부가정보(예: 잘못된 메모리 주소 등), `sepc`는 예외가 일어난 시점의 PC를 알려줍니다. 여기서는 디버깅을 위해 커널 패닉을 발생시킵니다. 144 | 145 | 사용된 매크로들은 `kernel.h`에서 다음과 같이 정의합니다: 146 | 147 | ```c [kernel.h] 148 | #include "common.h" 149 | 150 | struct trap_frame { 151 | uint32_t ra; 152 | uint32_t gp; 153 | uint32_t tp; 154 | uint32_t t0; 155 | uint32_t t1; 156 | uint32_t t2; 157 | uint32_t t3; 158 | uint32_t t4; 159 | uint32_t t5; 160 | uint32_t t6; 161 | uint32_t a0; 162 | uint32_t a1; 163 | uint32_t a2; 164 | uint32_t a3; 165 | uint32_t a4; 166 | uint32_t a5; 167 | uint32_t a6; 168 | uint32_t a7; 169 | uint32_t s0; 170 | uint32_t s1; 171 | uint32_t s2; 172 | uint32_t s3; 173 | uint32_t s4; 174 | uint32_t s5; 175 | uint32_t s6; 176 | uint32_t s7; 177 | uint32_t s8; 178 | uint32_t s9; 179 | uint32_t s10; 180 | uint32_t s11; 181 | uint32_t sp; 182 | } __attribute__((packed)); 183 | 184 | #define READ_CSR(reg) \ 185 | ({ \ 186 | unsigned long __tmp; \ 187 | __asm__ __volatile__("csrr %0, " #reg : "=r"(__tmp)); \ 188 | __tmp; \ 189 | }) 190 | 191 | #define WRITE_CSR(reg, value) \ 192 | do { \ 193 | uint32_t __tmp = (value); \ 194 | __asm__ __volatile__("csrw " #reg ", %0" ::"r"(__tmp)); \ 195 | } while (0) 196 | ``` 197 | 198 | `trap_frame` 구조체는 `kernel_entry`에서 저장한 프로그램 상태를 나타냅니다. `READ_CSR`와 `WRITE_CSR` 매크로는 CSR 레지스터를 읽고 쓰는 편리한 매크로입니다. 199 | 200 | 마지막으로 CPU에게 예외 핸들러 위치를 알려주려면, `kernel_main` 함수에서 `stvec` 레지스터를 설정하면 됩니다: 201 | 202 | 203 | ```c [kernel.c] {4-5} 204 | void kernel_main(void) { 205 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 206 | 207 | WRITE_CSR(stvec, (uint32_t) kernel_entry); // new 208 | __asm__ __volatile__("unimp"); // new 209 | ``` 210 | 211 | `stvec`를 설정한 뒤, `unimp` 명령어(illegal instruction 로 간주됨)를 실행해 일부러 예외를 일으키는 코드입니다. 212 | 213 | 214 | > [!NOTE] 215 | > 216 | > **`unimp` 는 “의사(pseudo) 명령어”**. 217 | > 218 | > [RISC-V Assembly Programmer's Manual](https://github.com/riscv-non-isa/riscv-asm-manual/blob/main/src/asm-manual.adoc#instruction-aliases), 에 따르면, 어셈블러는 `unimp`를 다음과 같은 명령어로 변환합니다: 219 | > 220 | > ``` 221 | > csrrw x0, cycle, x0 222 | > ``` 223 | > 224 | > `cycle` 레지스터는 읽기 전용(read-only) 레지스터이므로, 이를 쓰기 시도(`csrrw`)하는 것은 illegal instruction 예외로 이어집니다. 225 | 226 | 227 | 228 | ## 실행 해보기 229 | 230 | 이제 실행해보고, 예외 핸들러가 호출되는지 확인해봅시다: 231 | 232 | ``` 233 | $ ./run.sh 234 | Hello World! 235 | PANIC: kernel.c:47: unexpected trap scause=00000002, stval=ffffff84, sepc=8020015e 236 | ``` 237 | 238 | `scause`가 2면 “Illegal instruction” 예외에 해당합니다. 이는 우리가 `unimp`로 의도했던 동작과 일치합니다! 239 | 240 | 또한 `sepc`가 어디를 가리키는지 확인해봅시다. `unimp` 명령어가 호출된 라인 번호에 해당한다면, 예외가 정상적으로 동작하고 있는 것입니다: 241 | 242 | ``` 243 | $ llvm-addr2line -e kernel.elf 8020015e 244 | /Users/seiya/os-from-scratch/kernel.c:129 245 | ``` 246 | 247 | 해당 주소가 kernel.c에서 unimp를 실행한 줄을 가리키면 성공입니다! 248 | -------------------------------------------------------------------------------- /website/ko/09-memory-allocation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 메모리 할당 3 | --- 4 | 5 | # 메모리 할당 6 | 7 | 이 장에서는 간단한 메모리 할당기를 구현합니다. 8 | 9 | ## 링커 스크립트(Linker Script) 다시 살펴보기 10 | 11 | 메모리 할당기를 구현하기 전에, 어떤 메모리 영역을 할당할 것인지 링커 스크립트에서 정의해둡시다: 12 | 13 | 14 | ```ld [kernel.ld] {5-8} 15 | . = ALIGN(4); 16 | . += 128 * 1024; /* 128KB */ 17 | __stack_top = .; 18 | 19 | . = ALIGN(4096); 20 | __free_ram = .; 21 | . += 64 * 1024 * 1024; /* 64MB */ 22 | __free_ram_end = .; 23 | } 24 | ``` 25 | 26 | 여기서 새로 등장하는 심볼 `__free_ram`과 `__free_ram_end`는 스택 공간 다음에 위치하는 메모리 영역을 나타냅니다. 64MB라는 크기는 임의로 정한 값이고, `. = ALIGN(4096)`를 통해 이 영역을 4KB(페이지 크기) 경계로 맞추도록 했습니다. 27 | 28 | 하드코딩된 주소 대신 링커 스크립트에서 정의함으로써, 커널의 정적 데이터와 겹치지 않도록 링커가 위치를 자동으로 조정할 수 있습니다. 29 | 30 | > [!TIP] 31 | > 32 | > 실제 x86-64 운영체제들은 부팅 시 하드웨어(UEFI 등)를 통해 메모리 맵 정보(GetMemoryMap)를 받아서 사용 가능한 메모리 영역을 동적으로 파악하는 방식을 사용합니다. 33 | 34 | 35 | ## 세상에서 가장 간단한 메모리 할당 알고리즘 36 | 37 | 메모리를 동적으로 할당하는 함수를 구현해봅시다. 여기서는 C의 `malloc`처럼 `“바이트 단위”`로 할당하는 대신, 더 큰 단위인 `“페이지(page)”` 단위로 할당합니다. 일반적으로 한 페이지는 4KB(4096바이트)입니다. 38 | 39 | 40 | > [!TIP] 41 | > 42 | > 4KB는 4096이며, 16진수로는 0x1000입니다. 페이지 경계로 맞춰진 주소를 16진수로 보면 깔쌈하게 떨어지는 것을 확인할 수 있습니다. 43 | 44 | 다음 `alloc_pages` 함수는 `n`개의 페이지를 할당한 뒤, 그 시작 주소를 반환합니다: 45 | 46 | ```c [kernel.c] 47 | extern char __free_ram[], __free_ram_end[]; 48 | 49 | paddr_t alloc_pages(uint32_t n) { 50 | static paddr_t next_paddr = (paddr_t) __free_ram; 51 | paddr_t paddr = next_paddr; 52 | next_paddr += n * PAGE_SIZE; 53 | 54 | if (next_paddr > (paddr_t) __free_ram_end) 55 | PANIC("out of memory"); 56 | 57 | memset((void *) paddr, 0, n * PAGE_SIZE); 58 | return paddr; 59 | } 60 | ``` 61 | 62 | 여기서 `PAGE_SIZE`는 페이지 하나의 크기를 의미하며, `common.h`에 다음과 같이 정의해줍니다: 63 | 64 | 65 | ```c [common.h] 66 | #define PAGE_SIZE 4096 67 | ``` 68 | 69 | 주요 포인트 70 | 71 | - `next_paddr`은 `static` 변수로 선언되었기 때문에, 로컬 변수와 달리 함수 호출이 끝나도 값을 계속 유지합니다(글로벌 변수처럼 동작). 72 | - `next_paddr`는 “새로 할당할 메모리 영역의 시작 주소”를 가리키고, 할당이 이루어지면 그만큼 주소를 증가시켜 다음 할당을 준비합니다. 73 | - `next_paddr`의 초깃값은 __free_ram 주소입니다. 즉, 메모리는 `__free_ram` 이후부터 순차적으로 할당됩니다. 74 | - 링커 스크립트에서 `ALIGN(4096)`로 맞춰주었으므로, `alloc_pages`는 항상 4KB 정렬된 주소를 반환합니다. 75 | - 만약 `__free_ram_end`를 넘어가서 더 이상 할당할 메모리가 없다면, 커널 패닉을 일으킵니다. 76 | - `memset`을 통해 할당된 메모리 영역을 0으로 초기화합니다. 미초기화된 메모리에서 생기는 디버깅 어려움을 예방하기 위함입니다. 77 | 78 | 간단하지 않나요? 이 알고리즘은 아주 단순하지만, **반환(메모리 해제)** 이 불가능하다는 문제가 있습니다. 그래도 우리의 취미용 OS에는 이 정도면 충분합니다. 79 | 80 | > [!TIP] 81 | > 82 | > 이렇게 단순한 할당 방식을 흔히 **Bump Allocator** 또는 **Linear Allocator**라고 부르며, 해제가 필요 없는 상황에서 실제로도 쓰이는 유용한 알고리즘입니다. 구현이 매우 간단하고 빠릅니다. 83 | > 84 | > 메모리를 해제하는 기능까지 구현하려면, 비트맵(bitmap) 방식이나 버디 시스템(buddy system) 같은 좀 더 복잡한 알고리즘을 사용합니다. 85 | 86 | ## 메모리 할당 테스트하기 87 | 88 | 구현한 메모리 할당 함수를 테스트해봅시다. `kernel_main`에 다음 코드를 추가합니다: 89 | 90 | ```c [kernel.c] {4-7} 91 | void kernel_main(void) { 92 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 93 | 94 | paddr_t paddr0 = alloc_pages(2); 95 | paddr_t paddr1 = alloc_pages(1); 96 | printf("alloc_pages test: paddr0=%x\n", paddr0); 97 | printf("alloc_pages test: paddr1=%x\n", paddr1); 98 | 99 | PANIC("booted!"); 100 | } 101 | ``` 102 | 103 | 이제 첫 번째 할당 주소(`paddr0`)가 `__free_ram` 주소와 같은지, 그리고 두 번째 주소(`paddr1`)가 `paddr0`로부터 8KB(2페이지 = 8KB) 뒤인지를 확인해봅시다: 104 | 105 | 106 | ``` 107 | $ ./run.sh 108 | Hello World! 109 | alloc_pages test: paddr0=80221000 110 | alloc_pages test: paddr1=80223000 111 | ``` 112 | 113 | 그리고 실제 심볼 주소를 확인해보면: 114 | 115 | ``` 116 | $ llvm-nm kernel.elf | grep __free_ram 117 | 80221000 R __free_ram 118 | 84221000 R __free_ram_end 119 | ``` 120 | 121 | `__free_ram`이 `80221000`에서 시작하고, `paddr0`도 `80221000이`므로 정상적으로 동작하는 것을 알 수 있습니다! -------------------------------------------------------------------------------- /website/ko/12-application.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 애플리케이션 3 | --- 4 | 5 | # 애플리케이션 6 | 7 | 이 장에서는 우리가 만든 커널 위에서 동작할 첫 번째 애플리케이션 실행 파일을 준비해보겠습니다. 8 | 9 | ## 메모리 레이아웃 10 | 11 | 이전 장에서 페이징 메커니즘을 이용해 격리된 가상 주소 공간을 구현했습니다. 이제 애플리케이션을 주소 공간의 어느 위치에 배치할지 고민해 봅시다. 12 | 13 | 다음과 같이 애플리케이션용 링커 스크립트(`user.ld`)를 새로 만들어주세요: 14 | 15 | 16 | ```ld [user.ld] 17 | ENTRY(start) 18 | 19 | SECTIONS { 20 | . = 0x1000000; 21 | 22 | /* machine code */ 23 | .text :{ 24 | KEEP(*(.text.start)); 25 | *(.text .text.*); 26 | } 27 | 28 | /* read-only data */ 29 | .rodata : ALIGN(4) { 30 | *(.rodata .rodata.*); 31 | } 32 | 33 | /* data with initial values */ 34 | .data : ALIGN(4) { 35 | *(.data .data.*); 36 | } 37 | 38 | /* data that should be zero-filled at startup */ 39 | .bss : ALIGN(4) { 40 | *(.bss .bss.* .sbss .sbss.*); 41 | 42 | . = ALIGN(16); 43 | . += 64 * 1024; /* 64KB */ 44 | __stack_top = .; 45 | 46 | ASSERT(. < 0x1800000, "too large executable"); 47 | } 48 | } 49 | ``` 50 | 51 | 커널의 링커 스크립트와 상당히 비슷해 보이지만, 애플리케이션이 커널 주소 공간과 겹치지 않도록 베이스 주소(여기서는 `0x1000000`)가 다릅니다. 52 | 53 | `ASSERT`는 첫 번째 인자로 주어진 조건이 충족되지 않을 경우 링커를 중단시키는 역할을 합니다. 여기서는 `.bss` 섹션 끝(즉, 애플리케이션 메모리 끝)이 `0x1800000`을 초과하지 않도록 제한하고 있습니다. 이를 통해 실행 파일이 너무 크게 만들어지는 실수를 방지합니다. 54 | 55 | 56 | ## 유저랜드 라이브러리 57 | 58 | 다음으로, 사용자 프로그램(유저랜드)에서 사용할 라이브러리를 만들어 봅시다. 여기서는 간단하게 애플리케이션 시작에 필요한 최소 기능만 구현합니다. 59 | 60 | ```c [user.c] 61 | #include "user.h" 62 | 63 | extern char __stack_top[]; 64 | 65 | __attribute__((noreturn)) void exit(void) { 66 | for (;;); 67 | } 68 | 69 | void putchar(char c) { 70 | /* TODO */ 71 | } 72 | 73 | __attribute__((section(".text.start"))) 74 | __attribute__((naked)) 75 | void start(void) { 76 | __asm__ __volatile__( 77 | "mv sp, %[stack_top] \n" 78 | "call main \n" 79 | "call exit \n" 80 | :: [stack_top] "r" (__stack_top) 81 | ); 82 | } 83 | ``` 84 | 85 | 애플리케이션의 실행은 `start` 함수부터 시작합니다. 커널 부트 과정과 비슷하게, 스택 포인터를 설정한 뒤 애플리케이션의 `main` 함수를 호출합니다. 86 | 87 | `exit` 함수는 애플리케이션을 종료시킬 때 사용됩니다. 여기서는 단순히 무한 루프만 돌도록 구현했습니다. 88 | 89 | 또한, `putchar` 함수는 `common.c`의 `printf` 함수가 참조하고 있으므로, 여기에 정의만 해 두고 실제 구현은 나중에 진행합니다. 90 | 91 | 커널 초기화 과정과 달리 `.bss` 섹션을 0으로 초기화하는 코드를 넣지 않았습니다. 이는 이미 커널에서 `alloc_pages` 함수를 통해 .bss가 0으로 채워지도록 보장했기 때문입니다. 92 | 93 | 94 | > [!TIP] 95 | > 96 | > 대부분의 운영체제에서도, 사용자 프로그램에 할당된 메모리는 이미 0으로 초기화된 상태입니다. 그렇지 않으면, 다른 프로세스에서 사용하던 민감 정보(예: 인증 정보)가 남아있을 수 있고, 이는 심각한 보안 문제가 될 수 있습니다. 97 | 98 | 마지막으로, 유저랜드 라이브러리용 헤더 파일(user.h)을 준비합니다: 99 | 100 | ```c [user.h] 101 | #pragma once 102 | #include "common.h" 103 | 104 | __attribute__((noreturn)) void exit(void); 105 | void putchar(char ch); 106 | ``` 107 | 108 | ## 첫 번째 애플리케이션 109 | 110 | 이제 첫 번째 애플리케이션을 만들어 봅시다! 아직 문자를 화면에 표시하는 방법이 없으므로, "Hello, World!" 대신 단순 무한 루프를 도는 코드를 작성해 보겠습니다. 111 | 112 | ```c [shell.c] 113 | #include "user.h" 114 | 115 | void main(void) { 116 | for (;;); 117 | } 118 | ``` 119 | 120 | ## 애플리케이션 빌드하기 121 | 122 | 애플리케이션은 커널과 별도로 빌드합니다. 새로운 스크립트(`run.sh`)를 만들어 다음과 같이 작성해 봅시다: 123 | 124 | ```bash [run.sh] {1,3-6,10} 125 | OBJCOPY=/opt/homebrew/opt/llvm/bin/llvm-objcopy 126 | 127 | # Build the shell (application) 128 | $CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf shell.c user.c common.c 129 | $OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin 130 | $OBJCOPY -Ibinary -Oelf32-littleriscv shell.bin shell.bin.o 131 | 132 | # Build the kernel 133 | $CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf \ 134 | kernel.c common.c shell.bin.o 135 | ``` 136 | 137 | 처음 `$CC` 명령은 커널 빌드와 비슷합니다. C 파일들을 컴파일하고, `user.ld` 링커 스크립트를 사용해 링킹합니다. 138 | 139 | 첫 번째 `$OBJCOPY` 명령은 `ELF` 형식의 실행 파일(`shell.elf`)을 실제 메모리 내용만 포함하는 바이너리(`shell.bin`)로 변환합니다. 우리는 단순히 이 바이너리 파일을 메모리에 로드해 애플리케이션을 실행할 예정입니다. 일반적인 OS에서는 ELF 같은 형식을 사용해, 메모리 매핑 정보와 실제 메모리 내용을 분리해서 다루지만, 여기서는 단순화를 위해 바이너리만 사용합니다. 140 | 141 | 두 번째 `$OBJCOPY` 명령은 이 바이너리(`shell.bin`)를 C 언어에 임베드할 수 있는 오브젝트(`shell.bin.o`)로 변환합니다. 이 파일 안에 어떤 심볼이 들어있는지 `llvm-nm` 명령으로 확인해 봅시다: 142 | 143 | ``` 144 | $ llvm-nm shell.bin.o 145 | 00000000 D _binary_shell_bin_start 146 | 00010260 D _binary_shell_bin_end 147 | 00010260 A _binary_shell_bin_size 148 | ``` 149 | 150 | `_binary_`라는 접두사 뒤에 파일 이름이 오고, 그 다음에 `start`, `end`, `size`가 붙습니다. 이 심볼들은 각각 바이너리 내용의 시작, 끝, 크기를 의미합니다. 보통은 다음과 같이 사용할 수 있습니다: 151 | 152 | ```c 153 | extern char _binary_shell_bin_start[]; 154 | extern char _binary_shell_bin_size[]; 155 | 156 | void main(void) { 157 | uint8_t *shell_bin = (uint8_t *) _binary_shell_bin_start; 158 | printf("shell_bin size = %d\n", (int) _binary_shell_bin_size); 159 | printf("shell_bin[0] = %x (%d bytes)\n", shell_bin[0]); 160 | } 161 | ``` 162 | 163 | 이 프로그램은 `shell.bin`의 파일 크기와 파일 내용의 첫 바이트를 출력합니다. 다시 말해, `_binary_shell_bin_start` 변수를 파일 내용이 들어 있는 배열처럼 간주할 수 있습니다. 164 | 165 | 예를 들면, 다음과 같이 생각할 수 있습니다: 166 | 167 | 168 | ```c 169 | char _binary_shell_bin_start[] = ""; 170 | ``` 171 | 172 | 그리고 `_binary_shell_bin_size`에는 파일 크기가 들어 있습니다. 다만, 조금 독특한 방식으로 처리됩니다. 다시 `llvm-nm`을 확인해 봅시다. 173 | 174 | ``` 175 | $ llvm-nm shell.bin.o | grep _binary_shell_bin_size 176 | 00010454 A _binary_shell_bin_size 177 | 178 | $ ls -al shell.bin ← note: do not confuse with shell.bin.o! 179 | -rwxr-xr-x 1 seiya staff 66644 Oct 24 13:35 shell.bin 180 | 181 | $ python3 -c 'print(0x10454)' 182 | 66644 183 | ``` 184 | 185 | `llvm-nm` 출력의 첫 번째 열은 심볼의 주소를 나타냅니다. 여기서 `10454`(16진수)는 실제 파일 크기와 일치합니다. A(두 번째 열)는 이 심볼이 링커에 의해 주소가 재배치되지 않는 '절대(Absolute)' 심볼이라는 뜻입니다. 즉, 파일 크기를 '주소' 형태로 박아놓은 것입니다. 186 | 187 | `char _binary_shell_bin_size[]` 같은 식으로 정의하면, 일반 포인터처럼 보일 수 있지만 실제로는 그 값이 '파일 크기'를 담은 주소로 간주되어, 캐스팅하면 파일 크기를 얻게 됩니다. 188 | 189 | 마지막으로, 커널 컴파일 시 `shell.bin.o`를 함께 링크하면, 첫 번째 애플리케이션의 실행 파일이 커널 이미지 내부에 임베드됩니다. 190 | 191 | 192 | ## 실행 파일 디스어셈블 193 | 194 | 디스어셈블을 해보면 `.text.start` 섹션이 실행 파일의 맨 앞에 배치되어 있고, `start` 함수가 실제 주소 `0x1000000`에 있는 것을 볼 수 있습니다: 195 | 196 | ``` 197 | $ llvm-objdump -d shell.elf 198 | 199 | shell.elf: file format elf32-littleriscv 200 | 201 | Disassembly of section .text: 202 | 203 | 01000000 : 204 | 1000000: 37 05 01 01 lui a0, 4112 205 | 1000004: 13 05 05 26 addi a0, a0, 608 206 | 1000008: 2a 81 mv sp, a0 207 | 100000a: 19 20 jal 0x1000010
208 | 100000c: 29 20 jal 0x1000016 209 | 100000e: 00 00 unimp 210 | 211 | 01000010
: 212 | 1000010: 01 a0 j 0x1000010
213 | 1000012: 00 00 unimp 214 | 215 | 01000016 : 216 | 1000016: 01 a0 j 0x1000016 217 | ``` 218 | -------------------------------------------------------------------------------- /website/ko/13-user-mode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 유저 모드 3 | --- 4 | 5 | # 유저 모드 6 | 7 | 이전 장에서 작성한 애플리케이션을 실행해보겠습니다. 8 | 9 | 10 | ## 실행 파일 추출하기 11 | 12 | ELF와 같은 실행 파일 형식에서는 로드 주소가 파일 헤더(ELF의 경우 프로그램 헤더)에 저장됩니다. 하지만, 우리의 애플리케이션 실행 이미지는 원시 바이너리(raw binary)이기 때문에, 다음과 같이 고정된 값으로 준비해주어야 합니다: 13 | 14 | 15 | ```c [kernel.h] 16 | // 애플리케이션 이미지의 기본 가상 주소입니다. 이는 `user.ld`에 정의된 시작 주소와 일치해야 합니다. 17 | #define USER_BASE 0x1000000 18 | ``` 19 | 20 | 다음으로, `shell.bin.o`에 포함된 원시 바이너리를 사용하기 위해 심볼들을 정의합니다: 21 | 22 | ```c [kernel.c] 23 | extern char _binary_shell_bin_start[], _binary_shell_bin_size[]; 24 | ``` 25 | 26 | 또한, 애플리케이션을 시작하기 위해 `create_process` 함수를 다음과 같이 수정합니다: 27 | 28 | 29 | ```c [kernel.c] {1-3,5,11,20-33} 30 | void user_entry(void) { 31 | PANIC("not yet implemented"); 32 | } 33 | 34 | struct process *create_process(const void *image, size_t image_size) { 35 | /* omitted */ 36 | *--sp = 0; // s3 37 | *--sp = 0; // s2 38 | *--sp = 0; // s1 39 | *--sp = 0; // s0 40 | *--sp = (uint32_t) user_entry; // ra (changed!) 41 | 42 | uint32_t *page_table = (uint32_t *) alloc_pages(1); 43 | 44 | // Map kernel pages. 45 | for (paddr_t paddr = (paddr_t) __kernel_base; 46 | paddr < (paddr_t) __free_ram_end; paddr += PAGE_SIZE) 47 | map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X); 48 | 49 | // Map user pages. 50 | for (uint32_t off = 0; off < image_size; off += PAGE_SIZE) { 51 | paddr_t page = alloc_pages(1); 52 | 53 | // Handle the case where the data to be copied is smaller than the 54 | // page size. 55 | size_t remaining = image_size - off; 56 | size_t copy_size = PAGE_SIZE <= remaining ? PAGE_SIZE : remaining; 57 | 58 | // Fill and map the page. 59 | memcpy((void *) page, image + off, copy_size); 60 | map_page(page_table, USER_BASE + off, page, 61 | PAGE_U | PAGE_R | PAGE_W | PAGE_X); 62 | } 63 | ``` 64 | 65 | `create_process` 함수를 실행 이미지의 포인터(`image`)와 이미지 크기(`image_size`)를 인자로 받도록 수정했습니다. 이 함수는 지정된 크기만큼 실행 이미지를 페이지 단위로 복사하여 프로세스의 페이지 테이블에 매핑합니다. 또한, 첫 번째 컨텍스트 스위치 시 점프할 주소를 user_entry로 설정합니다. 현재는 이 함수를 빈 함수로 유지합니다. 66 | 67 | > [!WARNING] 68 | > 69 | > 실행 이미지를 복사하지 않고 직접 매핑하면, 동일한 애플리케이션의 프로세스들이 동일한 물리 페이지를 공유하게 됩니다. 이는 메모리 격리를 파괴합니다! 70 | 71 | 마지막으로, `create_process` 함수를 호출하는 부분을 수정하여 사용자 프로세스를 생성하도록 합니다: 72 | 73 | 74 | ```c [kernel.c] {8,12} 75 | void kernel_main(void) { 76 | memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); 77 | 78 | printf("\n\n"); 79 | 80 | WRITE_CSR(stvec, (uint32_t) kernel_entry); 81 | 82 | idle_proc = create_process(NULL, 0); // updated! 83 | idle_proc->pid = 0; // idle 84 | current_proc = idle_proc; 85 | 86 | // new! 87 | create_process(_binary_shell_bin_start, (size_t) _binary_shell_bin_size); 88 | 89 | yield(); 90 | PANIC("switched to idle process"); 91 | } 92 | ``` 93 | 94 | 이제 QEMU 모니터를 통해 실행 이미지가 예상대로 매핑되었는지 확인해봅시다: 95 | 96 | 97 | ``` 98 | (qemu) info mem 99 | vaddr paddr size attr 100 | -------- ---------------- -------- ------- 101 | 01000000 0000000080265000 00001000 rwxu--- 102 | 01001000 0000000080267000 00010000 rwxu--- 103 | ``` 104 | 105 | 물리 주소 `0x80265000`이 가상 주소 `0x1000000` (`USER_BASE`)에 매핑되어 있는 것을 확인할 수 있습니다. 이제 이 물리 주소의 내용을 살펴봅시다. 물리 메모리의 내용을 표시하려면 `xp` 명령어를 사용합니다: 106 | 107 | ``` 108 | (qemu) xp /32b 0x80265000 109 | 0000000080265000: 0x37 0x05 0x01 0x01 0x13 0x05 0x05 0x26 110 | 0000000080265008: 0x2a 0x81 0x19 0x20 0x29 0x20 0x00 0x00 111 | 0000000080265010: 0x01 0xa0 0x00 0x00 0x82 0x80 0x01 0xa0 112 | 0000000080265018: 0x09 0xca 0xaa 0x86 0x7d 0x16 0x13 0x87 113 | ``` 114 | 115 | 일부 데이터가 있는 것 같습니다. `shell.bin`의 내용을 확인하여 실제로 일치하는지 검증해봅시다: 116 | 117 | 118 | ``` 119 | $ hexdump -C shell.bin | head 120 | 00000000 37 05 01 01 13 05 05 26 2a 81 19 20 29 20 00 00 |7......&*.. ) ..| 121 | 00000010 01 a0 00 00 82 80 01 a0 09 ca aa 86 7d 16 13 87 |............}...| 122 | 00000020 16 00 23 80 b6 00 ba 86 75 fa 82 80 01 ce aa 86 |..#.....u.......| 123 | 00000030 03 87 05 00 7d 16 85 05 93 87 16 00 23 80 e6 00 |....}.......#...| 124 | 00000040 be 86 7d f6 82 80 03 c6 05 00 aa 86 01 ce 85 05 |..}.............| 125 | 00000050 2a 87 23 00 c7 00 03 c6 05 00 93 06 17 00 85 05 |*.#.............| 126 | 00000060 36 87 65 fa 23 80 06 00 82 80 03 46 05 00 15 c2 |6.e.#......F....| 127 | 00000070 05 05 83 c6 05 00 33 37 d0 00 93 77 f6 0f bd 8e |......37...w....| 128 | 00000080 93 b6 16 00 f9 8e 91 c6 03 46 05 00 85 05 05 05 |.........F......| 129 | 00000090 6d f2 03 c5 05 00 93 75 f6 0f 33 85 a5 40 82 80 |m......u..3..@..| 130 | ``` 131 | 132 | 16진수로만 보면 이해하기 어렵습니다. 기계어를 역어셈블하여 예상한 명령어들과 일치하는지 확인해봅시다: 133 | 134 | ``` 135 | (qemu) xp /8i 0x80265000 136 | 0x80265000: 01010537 lui a0,16842752 137 | 0x80265004: 26050513 addi a0,a0,608 138 | 0x80265008: 812a mv sp,a0 139 | 0x8026500a: 2019 jal ra,6 # 0x80265010 140 | 0x8026500c: 2029 jal ra,10 # 0x80265016 141 | 0x8026500e: 0000 illegal 142 | 0x80265010: a001 j 0 # 0x80265010 143 | 0x80265012: 0000 illegal 144 | ``` 145 | 146 | 이 코드는 초기 스택 포인터 값을 계산/설정하고, 두 개의 다른 함수를 호출합니다. 이를 `shell.elf`의 역어셈블 결과와 비교하면 실제로 일치함을 확인할 수 있습니다: 147 | 148 | 149 | ``` 150 | $ llvm-objdump -d shell.elf | head -n20 151 | 152 | shell.elf: file format elf32-littleriscv 153 | 154 | Disassembly of section .text: 155 | 156 | 01000000 : 157 | 1000000: 37 05 01 01 lui a0, 4112 158 | 1000004: 13 05 05 26 addi a0, a0, 608 159 | 1000008: 2a 81 mv sp, a0 160 | 100000a: 19 20 jal 0x1000010
161 | 100000c: 29 20 jal 0x1000016 162 | 100000e: 00 00 unimp 163 | 164 | 01000010
: 165 | 1000010: 01 a0 j 0x1000010
166 | 1000012: 00 00 unimp 167 | ``` 168 | 169 | ## 사용자 모드로 전환하기 170 | 171 | 애플리케이션을 실행하기 위해, 우리는 **user mode** 또는 RISC-V에서는 **U-Mode** 라 불리는 CPU 모드를 사용합니다. U-Mode로 전환하는 것은 의외로 간단합니다. 방법은 다음과 같습니다: 172 | 173 | 174 | ```c [kernel.h] 175 | #define SSTATUS_SPIE (1 << 5) 176 | ``` 177 | 178 | ```c [kernel.c] 179 | // ↓ __attribute__((naked)) is very important! 180 | __attribute__((naked)) void user_entry(void) { 181 | __asm__ __volatile__( 182 | "csrw sepc, %[sepc] \n" 183 | "csrw sstatus, %[sstatus] \n" 184 | "sret \n" 185 | : 186 | : [sepc] "r" (USER_BASE), 187 | [sstatus] "r" (SSTATUS_SPIE) 188 | ); 189 | } 190 | ``` 191 | 192 | S-Mode에서 U-Mode로의 전환은 `sret` 명령어를 사용하여 이루어집니다. 다만, 모드를 변경하기 전에 두 개의 CSR에 값을 기록합니다: 193 | 194 | 195 | - `sepc` 레지스터: U-Mode로 전환 시 실행할 프로그램 카운터를 설정합니다. 즉, sret 명령어가 점프할 위치입니다. 196 | - `sstatus` 레지스터의 `SPIE` 비트: 이 비트를 설정하면 U-Mode로 진입할 때 하드웨어 인터럽트가 활성화되며, `stvec` 레지스터에 설정된 핸들러가 호출됩니다. 197 | 198 | 199 | > [!TIP] 200 | > 201 | > 이 책에서는 하드웨어 인터럽트를 사용하지 않고 폴링을 사용하기 때문에 SPIE 비트를 설정할 필요는 없습니다. 하지만 인터럽트를 묵살하기보다는 명확하게 설정하는 것이 좋습니다. 202 | 203 | 204 | ## 사용자 모드 실행하기 205 | 206 | 이제 실행해봅시다! 다만, `shell.c`가 단순히 무한 루프를 돌기 때문에 화면상에서 제대로 동작하는지 확인하기 어렵습니다. 대신, QEMU 모니터를 통해 확인해봅시다: 207 | 208 | ``` 209 | (qemu) info registers 210 | 211 | CPU#0 212 | V = 0 213 | pc 01000010 214 | ``` 215 | 216 | CPU가 계속해서 `0x1000010` 주소를 실행하고 있는 것 같습니다. 제대로 작동하는 것처럼 보이지만, 뭔가 아쉬운 점이 있습니다. 그래서 U-Mode에서만 나타나는 동작을 관찰해보기로 합시다. shell.c에 한 줄을 추가합니다: 217 | 218 | ```c [shell.c] {4} 219 | #include "user.h" 220 | 221 | void main(void) { 222 | *((volatile int *) 0x80200000) = 0x1234; // new! 223 | for (;;); 224 | } 225 | ``` 226 | 227 | 이 `0x80200000은` 커널에서 사용하는 메모리 영역으로, 페이지 테이블에 매핑되어 있습니다. 그러나 이 주소는 페이지 테이블 항목에서 `U` 비트가 설정되지 않은 커널 페이지이므로 예외(페이지 폴트)가 발생해야 하며, 커널이 패닉 상태에 빠져야 합니다. 한번 실행해봅시다: 228 | 229 | 230 | ``` 231 | $ ./run.sh 232 | 233 | PANIC: kernel.c:71: unexpected trap scause=0000000f, stval=80200000, sepc=0100001a 234 | ``` 235 | 236 | 15번째 예외(`scause = 0xf = 15`)는 "Store/AMO 페이지 폴트"에 해당합니다. 예상했던 예외가 발생한 것 같습니다! 또한, `sepc`에 저장된 프로그램 카운터는 우리가 shell.c에 추가한 라인을 가리키고 있습니다: 237 | 238 | ``` 239 | $ llvm-addr2line -e shell.elf 0x100001a 240 | /Users/seiya/dev/os-from-scratch/shell.c:4 241 | ``` 242 | 243 | 축하합니다! 첫 번째 애플리케이션을 성공적으로 실행했습니다! 사용자 모드를 구현하는 것이 이렇게 간단하다는 것이 놀랍지 않나요? 커널은 애플리케이션과 매우 유사하며, 단지 몇 가지 추가 권한만 가지고 있을 뿐입니다. 244 | -------------------------------------------------------------------------------- /website/ko/14-system-call.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 시스템 콜 3 | --- 4 | 5 | # 시스템 콜 6 | 7 | 이 장에서는 어플리케이션이 커널 기능을 호출할 수 있도록 하는 **"시스템 콜"** 을 구현해 보겠습니다. 사용자 영역에서 Hello World를 출력해볼 시간입니다! 8 | 9 | ## 사용자 라이브러리 (User library) 10 | 11 | 시스템 콜을 호출하는 방식은 우리가 이전에 본 [SBI 콜](/ko/05-hello-world#say-hello-to-sbi) 구현과 매우 유사합니다: 12 | 13 | 14 | ```c [user.c] 15 | int syscall(int sysno, int arg0, int arg1, int arg2) { 16 | register int a0 __asm__("a0") = arg0; 17 | register int a1 __asm__("a1") = arg1; 18 | register int a2 __asm__("a2") = arg2; 19 | register int a3 __asm__("a3") = sysno; 20 | 21 | __asm__ __volatile__("ecall" 22 | : "=r"(a0) 23 | : "r"(a0), "r"(a1), "r"(a2), "r"(a3) 24 | : "memory"); 25 | 26 | return a0; 27 | } 28 | ``` 29 | 30 | `syscall` 함수는 `a3` 레지스터에 시스템 콜 번호를, `a0`부터 `a2` 레지스터에는 시스템 콜 인자를 저장한 후 ecall 명령어를 실행합니다. 31 | `ecall` 명령어는 커널로 처리를 위임하기 위해 사용되는 특별한 명령어입니다. 32 | `ecall` 명령어가 실행되면 예외 핸들러가 호출되어 제어권이 커널로 넘어갑니다. 33 | 커널에서 반환하는 값은 `a0` 레지스터에 설정됩니다. 34 | 35 | 우리가 처음 구현할 시스템 콜은 문자 출력 함수 `putchar`입니다. 이 시스템 콜은 첫 번째 인자로 문자를 받고, 두 번째 이후의 사용하지 않는 인자들은 0으로 설정됩니다: 36 | 37 | ```c [common.h] 38 | #define SYS_PUTCHAR 1 39 | ``` 40 | 41 | ```c [user.c] {2} 42 | void putchar(char ch) { 43 | syscall(SYS_PUTCHAR, ch, 0, 0); 44 | } 45 | ``` 46 | 47 | ## 커널에서 ecall 명령어 처리 48 | 49 | 이제 예외 트랩 핸들러를 업데이트하여 `ecall` 명령어를 처리하도록 합시다: 50 | 51 | ```c [kernel.h] 52 | #define SCAUSE_ECALL 8 53 | ``` 54 | 55 | ```c [kernel.c] {5-7,12} 56 | void handle_trap(struct trap_frame *f) { 57 | uint32_t scause = READ_CSR(scause); 58 | uint32_t stval = READ_CSR(stval); 59 | uint32_t user_pc = READ_CSR(sepc); 60 | if (scause == SCAUSE_ECALL) { 61 | handle_syscall(f); 62 | user_pc += 4; 63 | } else { 64 | PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc); 65 | } 66 | 67 | WRITE_CSR(sepc, user_pc); 68 | } 69 | ``` 70 | 71 | `scause`의 값을 확인하면 `ecall` 명령어가 호출되었는지 판단할 수 있습니다. 또한, `handle_syscall` 함수를 호출한 후 `sepc`에 `4`(즉, `ecall` 명령어의 크기)를 더해줍니다. 이는 `sepc가` 예외를 발생시킨 명령어(즉, `ecall`)를 가리키기 때문인데, 만약 변경하지 않으면 커널이 동일한 위치로 돌아가 `ecall` 명령어를 반복 실행하게 됩니다. 72 | 73 | ## 시스템 콜 핸들러 74 | 75 | 76 | 아래의 시스템 콜 핸들러는 트랩 핸들러에서 호출됩니다. 이 함수는 예외가 발생했을 때 저장된 "레지스터 값들을 담은 구조체"를 인자로 받습니다: 77 | 78 | ```c [kernel.c] 79 | void handle_syscall(struct trap_frame *f) { 80 | switch (f->a3) { 81 | case SYS_PUTCHAR: 82 | putchar(f->a0); 83 | break; 84 | default: 85 | PANIC("unexpected syscall a3=%x\n", f->a3); 86 | } 87 | } 88 | ``` 89 | 90 | 핸들러는 `a3` 레지스터의 값을 확인하여 시스템 콜의 종류를 판별합니다. 현재는 오직 하나의 시스템 콜, `SYS_PUTCHAR만` 구현되어 있으며, 이는 `a0` 레지스터에 저장된 문자를 출력합니다. 91 | 92 | ## 시스템 콜 테스트 93 | 94 | 이제 시스템 콜을 구현했으니 테스트해 보겠습니다! `common.c`에 구현된 `printf` 함수를 기억하시나요? 이 함수는 내부적으로 `putchar` 함수를 호출하여 문자를 출력합니다. 이미 사용자 라이브러리에서 `putchar`를 구현했으므로 그대로 사용할 수 있습니다: 95 | 96 | ```c [shell.c] {2} 97 | void main(void) { 98 | printf("Hello World from shell!\n"); 99 | } 100 | ``` 101 | 102 | 실행 결과는 다음과 같이 화면에 출력됩니다: 103 | 104 | ``` 105 | $ ./run.sh 106 | Hello World from shell! 107 | ``` 108 | 109 | 축하합니다! 시스템 콜 구현에 성공했습니다. 하지만 여기서 멈추지 않고 다른 시스템 콜도 구현해보겠습니다! 110 | 111 | ## 키보드 입력 받기 (getchar 시스템 콜) 112 | 113 | 이제 셸(shell)을 구현하려면 키보드로부터 문자를 입력받을 수 있어야 합니다. 114 | 115 | SBI는 "디버그 콘솔의 입력"을 읽어들이는 인터페이스를 제공합니다. 입력이 없을 경우 `-1`을 반환합니다: 116 | 117 | 118 | ```c [kernel.c] 119 | long getchar(void) { 120 | struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2); 121 | return ret.error; 122 | } 123 | ``` 124 | 125 | `getchar` 시스템 콜은 다음과 같이 구현됩니다: 126 | 127 | ```c [common.h] 128 | #define SYS_GETCHAR 2 129 | ``` 130 | 131 | ```c [user.c] 132 | int getchar(void) { 133 | return syscall(SYS_GETCHAR, 0, 0, 0); 134 | } 135 | ``` 136 | 137 | ```c [user.h] 138 | int getchar(void); 139 | ``` 140 | 141 | ```c [kernel.c] {3-13} 142 | void handle_syscall(struct trap_frame *f) { 143 | switch (f->a3) { 144 | case SYS_GETCHAR: 145 | while (1) { 146 | long ch = getchar(); 147 | if (ch >= 0) { 148 | f->a0 = ch; 149 | break; 150 | } 151 | 152 | yield(); 153 | } 154 | break; 155 | /* omitted */ 156 | } 157 | } 158 | ``` 159 | 160 | 이 시스템 콜의 구현에서는 SBI를 반복 호출하여 문자가 입력될 때까지 대기합니다. 하지만 단순히 반복 호출하게 되면 다른 프로세스들이 실행될 수 없으므로, `yield` 시스템 콜을 호출하여 CPU를 다른 프로세스에 양보합니다. 161 | 162 | > [!NOTE] 163 | > 164 | > 엄밀히 말하면, SBI는 키보드의 문자를 읽는 것이 아니라 시리얼 포트의 문자를 읽습니다. 키보드(QEMU의 표준 입력)가 시리얼 포트에 연결되어 있기 때문에 동작하는 것입니다. 165 | 166 | 167 | 168 | ## 셸 구현하기 169 | 170 | 이제 `hello` 명령어를 지원하는 간단한 셸을 작성해보겠습니다. 이 명령어를 입력하면 `Hello world from shell!`이 출력됩니다: 171 | 172 | ```c [shell.c] 173 | void main(void) { 174 | while (1) { 175 | prompt: 176 | printf("> "); 177 | char cmdline[128]; 178 | for (int i = 0;; i++) { 179 | char ch = getchar(); 180 | putchar(ch); 181 | if (i == sizeof(cmdline) - 1) { 182 | printf("command line too long\n"); 183 | goto prompt; 184 | } else if (ch == '\r') { 185 | printf("\n"); 186 | cmdline[i] = '\0'; 187 | break; 188 | } else { 189 | cmdline[i] = ch; 190 | } 191 | } 192 | 193 | if (strcmp(cmdline, "hello") == 0) 194 | printf("Hello world from shell!\n"); 195 | else 196 | printf("unknown command: %s\n", cmdline); 197 | } 198 | } 199 | ``` 200 | 201 | 셸은 줄바꿈 문자가 입력될 때까지 문자를 읽어들이고, 입력된 문자열이 명령어와 일치하는지 확인합니다. 202 | 203 | > [!WARNING] 204 | > 205 | > 디버그 콘솔에서는 줄바꿈 문자가 `'\r'`입니다. 206 | 207 | 예를 들어, `hello` 명령어를 입력해보면: 208 | 209 | ``` 210 | $ ./run.sh 211 | 212 | > hello 213 | Hello world from shell! 214 | ``` 215 | 216 | 이렇게 나오게 됩니다. 이제 여러분의 OS는 점점 실제 OS처럼 보이기 시작합니다. 정말 빠르게 진도를 나가고 있네요! 217 | 218 | ## 프로세스 종료 (exit 시스템 콜) 219 | 220 | 마지막으로, 프로세스를 종료시키는 `exit` 시스템 콜을 구현해 보겠습니다: 221 | 222 | ```c [common.h] 223 | #define SYS_EXIT 3 224 | ``` 225 | 226 | ```c [user.c] {2-3} 227 | __attribute__((noreturn)) void exit(void) { 228 | syscall(SYS_EXIT, 0, 0, 0); 229 | for (;;); // Just in case! 230 | } 231 | ``` 232 | 233 | ```c [kernel.h] 234 | #define PROC_EXITED 2 235 | ``` 236 | 237 | ```c [kernel.c] {3-7} 238 | void handle_syscall(struct trap_frame *f) { 239 | switch (f->a3) { 240 | case SYS_EXIT: 241 | printf("process %d exited\n", current_proc->pid); 242 | current_proc->state = PROC_EXITED; 243 | yield(); 244 | PANIC("unreachable"); 245 | /* omitted */ 246 | } 247 | } 248 | ``` 249 | 250 | 해당 시스템 콜은 프로세스의 상태를 `PROC_EXITED`로 변경한 후, `yield`를 호출하여 CPU를 다른 프로세스에 양보합니다. 스케줄러는 `PROC_RUNNABLE` 상태의 프로세스만 실행하기 때문에, 이 프로세스는 다시 실행되지 않습니다. 하지만 혹시라도 돌아올 경우를 대비하여 `PANIC` 매크로를 추가해 두었습니다. 251 | 252 | > [!TIP] 253 | > 254 | > 단순화를 위해, 여기서는 프로세스를 종료할 때 단순히 상태만 `PROC_EXITED`로 변경합니다. 실제 OS를 구축하려면, 페이지 테이블이나 할당된 메모리 영역과 같이 프로세스가 점유한 자원을 해제해야 합니다. 255 | 256 | 셸에 `exit` 명령어를 추가해보겠습니다: 257 | 258 | ```c [shell.c] {3-4} 259 | if (strcmp(cmdline, "hello") == 0) 260 | printf("Hello world from shell!\n"); 261 | else if (strcmp(cmdline, "exit") == 0) 262 | exit(); 263 | else 264 | printf("unknown command: %s\n", cmdline); 265 | ``` 266 | 267 | 이제 완료되었습니다! 실행해봅시다: 268 | 269 | 270 | ``` 271 | $ ./run.sh 272 | 273 | > exit 274 | process 2 exited 275 | PANIC: kernel.c:333: switched to idle process 276 | ``` 277 | 278 | `exit` 명령어를 실행하면 셸 프로세스가 시스템 콜을 통해 종료되고, 실행 가능한 다른 프로세스가 없으므로 스케줄러가 `idle` 프로세스를 선택하여 `PANIC`이 발생합니다. 279 | 280 | -------------------------------------------------------------------------------- /website/ko/17-outro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 끝내며 3 | --- 4 | 5 | # 끝내며 6 | 7 | 축하합니다! 이 책을 끝까지 읽으셨습니다. 여러분은 간단한 OS 커널을 처음부터 구현하는 방법을 배웠고, 부팅 CPU, 컨텍스트 스위칭, 페이지 테이블, 사용자 모드, 시스템 호출, 디스크 I/O, 파일 시스템 등 운영체제의 기본 개념들에 대해 익혔습니다. 8 | 9 | 비록 코드가 1000줄이 채 되지 않지만, 정말 도전적인 여정이었을 것입니다. 왜냐하면 여러분은 커널의 핵심 중의 핵심, 바로 커널의 코어를 직접 구현했기 때문입니다. 10 | 11 | 아직 만족스럽지 않거나 더 도전해보고 싶은 분들을 위해 다음과 같은 추가 단계들을 제안합니다: 12 | 13 | 14 | ## 새로운 기능 추가하기 15 | 16 | 이 책에서는 커널의 기본 기능들을 구현했습니다. 하지만 여전히 구현할 수 있는 기능들이 많이 있습니다. 예를 들어, 다음과 같은 기능들을 추가해보는 것도 흥미로울 것입니다: 17 | 18 | - 메모리 해제가 가능한 적절한 메모리 할당기 19 | - 인터럽트 처리: 디스크 I/O에 대해 바쁜 대기(busy-wait)를 하지 않도록 개선 20 | - 완전한 기능을 갖춘 파일 시스템: ext2와 같은 파일 시스템 구현이 좋은 출발점이 될 수 있음 21 | - 네트워크 통신(TCP/IP): UDP/IP 구현은 어렵지 않으며, TCP는 다소 복잡합니다. virtio-net은 virtio-blk와 매우 유사합니다! 22 | 23 | ## 다른 OS 구현체 읽어보기 24 | 25 | 가장 추천하는 다음 단계는 다른 OS의 구현체들을 읽어보는 것입니다. 자신의 구현과 다른 구현체들을 비교하며, 다른 사람들이 어떻게 구현했는지 배우는 과정은 매우 교육적입니다. 26 | 27 | 제가 가장 좋아하는 구현체 중 하나는 [RISC-V version of xv6](https://github.com/mit-pdos/xv6-riscv)입니다. 이 OS는 교육 목적으로 만들어진 UNIX-like 운영체제로, [explanatory book (in English)](https://pdos.csail.mit.edu/6.828/2022/)도 함께 제공됩니다. UNIX의 fork(2)와 같은 기능을 배우고자 하는 분들에게 추천합니다. 28 | 29 | 또 다른 추천 프로젝트는 제 개인 프로젝트 [Starina](https://starina.dev)입니다. Rust로 작성된 마이크로커널 기반 OS로, 아직 실험적인 단계이지만 마이크로커널 아키텍처와 OS 개발에서 Rust가 어떻게 빛을 발하는지 배우고 싶은 분들에게 흥미로운 자료가 될 것입니다. 30 | 31 | 32 | ## 피드백은 언제나 환영합니다! 33 | 34 | 질문이나 피드백이 있다면, [GitHub](https://github.com/nuta/operating-system-in-1000-lines/issues)에 남겨주시거나, 원하신다면 [이메일](https://seiya.me)로 연락해 주세요. 여러분의 OS 프로그래밍 여정에 행운이 가득하길 바랍니다! 35 | -------------------------------------------------------------------------------- /website/ko/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 들어가며 3 | --- 4 | 5 | # 1,000줄 짜리 OS를 만들어보자 6 | 7 | 처음부터 시작해서 작은 OS 하나를 차근차근 만들어봅시다. 8 | 9 | OS라고 하면 왠지 겁이 날 수도 있지만, OS(특히 커널)의 기본 기능 자체는 의외로 간단합니다. 예를 들어, 대표적인 대형 오픈소스인 리눅스조차도 0.01 버전에서는 8,413줄밖에 되지 않았습니다. 10 | 11 | 우리는 C언어로 기본적인 컨텍스트 스위칭, 페이징, 사용자 모드, 명령어 쉘, 디스크 장치 드라이버, 파일 읽기/쓰기 작업을 구현할 겁니다. 많아 보이지만, 전체 코드는 단 1,000줄 정도밖에 되지 않습니다. 12 | 13 | “1,000줄이라면 금방 만들겠지?”라고 생각하실 수도 있지만, 초심자라면 적어도 3일은 걸릴 것입니다. 자체 OS 개발이 어려운 이유 중 하나는 바로 ‘디버깅’입니다. 최초에 printf조차 구현되지 않은 상황에서는, 애플리케이션 개발과는 전혀 다른 디버깅 기법과 능력이 필요하기 때문입니다. 특히 “제로부터” 시작하면, 부트 과정이나 페이징처럼 복잡하고 쉽게 좌절할 수 있는 요소들이 초반부터 등장합니다. 그래서 이 책에서는 “자체 OS를 어떻게 디버깅하면 좋을까?”라는 주제도 함께 다뤄보려 합니다. 14 | 15 | 물론, 디버깅이 힘든 만큼, 제대로 동작했을 더 큰 만족감을 느낄겁니다. 흥미진진한 OS 개발의 세계로 뛰어들어봅시다! 16 | 17 | - 구현 예제는 [GitHub](https://github.com/nuta/operating-system-in-1000-lines) 에서 다운로드할 수 있습니다. 18 | - 이 책은 [CC BY 4.0 라이센스](https://creativecommons.jp/faq) 하에 사용할 수 있습니다. 구현 예제와 본문 소스 코드는 [MIT 라이센스](https://opensource.org/licenses/MIT) 하에 사용할 수 있습니다. 19 | - C언어와 UNIX 계열 환경에 익숙하고 `gcc hello.c && ./a.out`을 실행할 수 있다면 충분합니다! 20 | - 이 책은 원래 제가 쓴 책 *[Design and Implementation of Microkernels](https://www.shuwasystem.co.jp/book/9784798068718.html)* (일본어로 쓴 책)의 부록 자료로 작성되었습니다. 21 | 22 | Happy OS hacking! 23 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "vitepress": "^1.6.3", 4 | "vitepress-plugin-group-icons": "^1.3.5" 5 | }, 6 | "scripts": { 7 | "dev": "vitepress dev", 8 | "build": "vitepress build", 9 | "preview": "vitepress preview" 10 | } 11 | } -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuta/operating-system-in-1000-lines/33bf69c1841c759e0a102b5a51364dbff919f987/website/public/favicon.ico -------------------------------------------------------------------------------- /website/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": null, 3 | "buildCommand": "npm run build", 4 | "installCommand": "pnpm i", 5 | "outputDirectory": ".vitepress/dist", 6 | "cleanUrls": true, 7 | "trailingSlash": false, 8 | "github": { 9 | "silent": true 10 | }, 11 | "redirects": [ 12 | { 13 | "source": "/ja/welcome", 14 | "destination": "/ja", 15 | "permanent": true 16 | }, 17 | { 18 | "source": "/ja/setting-up-development-environment", 19 | "destination": "/ja/01-setting-up-development-environment", 20 | "permanent": true 21 | }, 22 | { 23 | "source": "/ja/assembly", 24 | "destination": "/ja/02-assembly", 25 | "permanent": true 26 | }, 27 | { 28 | "source": "/ja/overview", 29 | "destination": "/ja/03-overview", 30 | "permanent": true 31 | }, 32 | { 33 | "source": "/ja/boot", 34 | "destination": "/ja/04-boot", 35 | "permanent": true 36 | }, 37 | { 38 | "source": "/ja/hello-world", 39 | "destination": "/ja/05-hello-world", 40 | "permanent": true 41 | }, 42 | { 43 | "source": "/ja/libc", 44 | "destination": "/ja/06-libc", 45 | "permanent": true 46 | }, 47 | { 48 | "source": "/ja/kernel-panic", 49 | "destination": "/ja/07-kernel-panic", 50 | "permanent": true 51 | }, 52 | { 53 | "source": "/ja/exception", 54 | "destination": "/ja/08-exception", 55 | "permanent": true 56 | }, 57 | { 58 | "source": "/ja/memory-allocation", 59 | "destination": "/ja/09-memory-allocation", 60 | "permanent": true 61 | }, 62 | { 63 | "source": "/ja/process", 64 | "destination": "/ja/10-process", 65 | "permanent": true 66 | }, 67 | { 68 | "source": "/ja/page-table", 69 | "destination": "/ja/11-page-table", 70 | "permanent": true 71 | }, 72 | { 73 | "source": "/ja/application", 74 | "destination": "/ja/12-application", 75 | "permanent": true 76 | }, 77 | { 78 | "source": "/ja/user-mode", 79 | "destination": "/ja/13-user-mode", 80 | "permanent": true 81 | }, 82 | { 83 | "source": "/ja/system-call", 84 | "destination": "/ja/14-system-call", 85 | "permanent": true 86 | }, 87 | { 88 | "source": "/ja/virtio-blk", 89 | "destination": "/ja/15-virtio-blk", 90 | "permanent": true 91 | }, 92 | { 93 | "source": "/ja/file-system", 94 | "destination": "/ja/16-file-system", 95 | "permanent": true 96 | }, 97 | { 98 | "source": "/ja/conclusion", 99 | "destination": "/ja/17-outro", 100 | "permanent": true 101 | } 102 | ] 103 | } -------------------------------------------------------------------------------- /website/zh/01-setting-up-development-environment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 入门 3 | --- 4 | 5 | # 入门 6 | 7 | 本书假设你正在使用 UNIX 或类 UNIX 的操作系统,例如 macOS 或者 Ubuntu。如果你用的是 Windows,那么你需要安装 Windows Subsystem for Linux (WSL2),然后按照 Ubuntu 的说明做。 8 | 9 | ## 安装开发工具 10 | 11 | ### macOS 12 | 13 | 安装 [Homebrew](https://brew.sh) 然后运行下面这个命令去获取所有需要用到的工具: 14 | 15 | ``` 16 | brew install llvm lld qemu 17 | ``` 18 | 19 | ### Ubuntu 20 | 21 | 使用 `apt` 安装所需的包: 22 | 23 | ``` 24 | sudo apt update && sudo apt install -y clang llvm lld qemu-system-riscv32 curl 25 | ``` 26 | 27 | 然后下载 OpenSBI(把它理解成是 PC 的 BIOS/UEFI): 28 | 29 | ``` 30 | curl -LO https://github.com/qemu/qemu/raw/v8.0.4/pc-bios/opensbi-riscv32-generic-fw_dynamic.bin 31 | ``` 32 | 33 | > [!WARNING] 34 | > 35 | > 当你运行 QEMU 的时候,确保 `opensbi-riscv32-generic-fw_dynamic.bin` 是在当前目录的。否则你将会看到这个错误: 36 | > 37 | > ``` 38 | > qemu-system-riscv32: Unable to load the RISC-V firmware "opensbi-riscv32-generic-fw_dynamic.bin" 39 | > ``` 40 | 41 | ### 其他操作系统的用户 42 | 43 | 如果你用的是其他操作系统,你需要准备好下面的工具: 44 | 45 | - `bash`:命令行工具。通常是操作系统预装的。 46 | - `tar`:通常是操作系统预装的。推荐 GUN 版而不是 BSD 版。 47 | - `clang`:C 编译器。确保它支持 32 位 RISC-V CPU(参考后续说明)。 48 | - `lld`: LLVM 链接器,将编译后的目标文件链接成可执行文件。 49 | - `llvm-objcopy`:目标文件编辑器。附带了 LLVM 包(通常是 `llvm` 包)。 50 | - `llvm-objdump`:反汇编程序。与 `llvm-objcopy` 一样。 51 | - `llvm-readelf`:ELF 文件阅读器。与 `llvm-objcopy` 一样。 52 | - `qemu-system-riscv32`:32 位的 RISC-V CPU 模拟器。它是 QEMU 包的一部分(通常是 `qemu` 包)。 53 | 54 | > [!TIP] 55 | > 56 | > 用下面这个命令检查你的 `clang` 是否支持 32 位 RISC-V CPU: 57 | > 58 | > ``` 59 | > $ clang -print-targets | grep riscv32 60 | > riscv32 - 32-bit RISC-V 61 | > ``` 62 | > 63 | > 你应该能看到 `riscv32`。注意,macOS 自带的 clang 不会显示。这就是为什么你需要用 Homebrew 安装 `llvm` 包中的另一个 `clang`。 64 | 65 | ## 设置 Git 仓库(可选) 66 | 67 | 如果你在用 Git 仓库管理代码,添加下面的 `.gitignore` 文件: 68 | 69 | ```gitignore [.gitignore] 70 | /disk/* 71 | !/disk/.gitkeep 72 | *.map 73 | *.tar 74 | *.o 75 | *.elf 76 | *.bin 77 | *.log 78 | *.pcap 79 | ``` 80 | 81 | 到这里你已经做好准备了,接下来请开始构建你的第一个操作系统! 82 | -------------------------------------------------------------------------------- /website/zh/02-assembly.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: RISC-V 101 3 | --- 4 | 5 | # RISC-V 6 | 7 | 就像网页浏览器隐藏了 Windows/macOS/Linux 的差异,操作系统则是隐藏了不同 CPU 之间的差异。换句话说,操作系统是一个操控 CPU 并为应用程序提供抽象层的程序。 8 | 9 | 本书选择 RISC-V 作为目标 CPU 是因为: 10 | 11 | - [规范](https://riscv.org/technical/specifications/)简单且适合新手。 12 | - 它是近年来流行的 ISA(指令集架构),与 x86 和 Arm 并驾齐驱。 13 | - 设计决策在整个规范中都有详细记录,并且读起来很有趣。 14 | 15 | 我们将会基于 **32 位**的 RISC-V 来编写操作系统。当然稍作改动你也可以让它适配 64 位的 RISC-V。但是,更宽的位宽会变得稍微复杂一些,而且更长的地址读起来可能很繁琐。 16 | 17 | ## QEMU 虚拟机 18 | 19 | 计算机是由不同的设备组成的:CPU、内存、网卡、硬盘等等。例如,虽然 iPhone 和树莓派都使用 Arm CPUs,但显而易见它们是不一样的计算机。 20 | 21 | 本书支持 QEMU `virt` 虚拟机([文档](https://www.qemu.org/docs/master/system/riscv/virt.html))是因为: 22 | 23 | - 哪怕它不是真实存在于这个世界的,它也是简单的并且跟真实设备非常相似。 24 | - 你可以免费使用 QEMU 来模拟而不需要购买物理硬件。 25 | - 当你遇到调试问题,你可以阅读 QEMU 的代码、或者用调试器附加到 QEMU 进程来研究哪里出错了。 26 | 27 | ## RISC-V assembly 101 28 | 29 | RISC-V,或者说是 RISC-V ISA(指令集架构),定义了一些列 CPU 可以执行的指令。有点类似于 API 或者编程语言规范。当你写 C 程序时,编译器会将代码翻译成 RISC-V 汇编代码。不幸的是,在编写操作系统过程中你需要写一些汇编代码,不过不用担心!汇编没有你想象中那么难。 30 | 31 | > [!TIP] 32 | > 33 | > **试试 Compiler Explorer!** 34 | > 35 | > [Compiler Explorer](https://godbolt.org/)是一个在线编译器,是帮助你学汇编的实用工具。当你写下 C 代码,它就会显示相应的汇编代码。 36 | > 37 | > Compiler Explorer 默认使用 x86-64 CPU 汇编。可以通过在右边面板中指定 `RISC-V rv32gc clang (trunk)` 来输出 32 位 RISC-V 汇编。 38 | > 39 | > 这里还有个有趣的事,你可以试试在编译选项中指定优化选项,例如 `-O0`(关闭优化)或者 `-O2` (二级优化),然后看看汇编有什么变化。 40 | 41 | ### 汇编语言基础 42 | 43 | 汇编语言(几乎)是机器代码的直接表示。我们来看一个简单的例子: 44 | 45 | ```asm 46 | addi a0, a1, 123 47 | ``` 48 | 49 | 通常来说,每一行汇编代码表示一个单一指令。第一列(`addi`)是指令名,也被叫做 *操作码(opcode)*。接下来的几列(`a0, a1, 123`)被称作 *操作数(oprands)*,是指令的参数。在这个例子中,`addi` 指令将值 `123` 与寄存器 `a1` 中的值相加,然后将结果保存到寄存器 `a0`。 50 | 51 | ### 寄存器 52 | 53 | 寄存器类就像 CPU 中的临时变量,它们比内存快很多。CPU 将内存中的数据读取到寄存器,对寄存器做算术运算,然后将结果回写到内存或者寄存器。 54 | 55 | 下面是 RISC-V 中一些常见的寄存器: 56 | 57 | | 寄存器 | ABI 名称 (别名) | 解释 | 58 | |---| -------- | ----------- | 59 | | `pc` | `pc` | 程序计数器(下一条指令的位置) | 60 | | `x0` |`zero` | 硬连线零(始终读为零) | 61 | | `x1` |`ra` | 返回地址 | 62 | | `x2` |`sp` | 栈指针 | 63 | | `x5` - `x7` | `t0` - `t2` | 临时寄存器 | 64 | | `x8` | `fp` | 栈帧指针 | 65 | | `x10` - `x11` | `a0` - `a1` | 函数参数/返回值 | 66 | | `x12` - `x17` | `a2` - `a7` | 函数参数 | 67 | | `x18` - `x27` | `s0` - `s11` | 调用期间保存的临时寄存器 | 68 | | `x28` - `x31` | `t3` - `t6` | 临时寄存器 | 69 | 70 | > [!TIP] 71 | > 72 | > **调用约定** 73 | > 74 | > 通常来说,你可以按照你的喜好来使用寄存器,但为了和其他软件互通,寄存器的使用方式有明确的定义 —— 这被称为 *调用约定*。 75 | > 76 | > 例如,`x10` - `x11` 寄存器被用作函数参数和返回值。为了可读性,它们在 ABI 中被赋予了像 `a0` - `a1` 这样的别名。在[规范](https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf)可以看到更多细节。 77 | 78 | ### 内存访问 79 | 80 | 寄存器真的很快,但它们的数量有限。绝大部分的数据都被保存在内存中,程序从内存中读取数据或者往内存中写入数据是通过 `lw` (load word) 指令和 `sw` (store word) 指令来实现的: 81 | 82 | ```asm 83 | lw a0, (a1) // 从 a1 寄存器中保存的地址中读取一个字(word,32 位) 84 | // 然后保存到 a0 寄存器。在 C 语言就是: a0 = *a1; 85 | ``` 86 | 87 | ```asm 88 | sw a0, (a1) // 向 a1 保存的地址中写入一个字,这个字是保存在 a0 的。 89 | // 在 C 语言中是: *a1 = a0; 90 | ``` 91 | 92 | 你可以将 `(...)` 看作是 C 语言中的指针解引用。在这个例子里,`a1` 是一个指向 32 位宽值的指针。 93 | 94 | ## 分支指令 95 | 96 | 分支指令将会改变程序的控制流。它们被用于实现 `if`、`for` 和 `while` 语句, 97 | 98 | 99 | ```asm 100 | bnez a0,