이 문서는 x86-64 커널 하나를 아무것도 없는 상태에서 시작해, 셸에서 디스크의 프로그램을 fork·exec·wait 로 띄우는 데까지 차근차근 쌓아 올리는 과정을 따라갑니다.
learn 브랜치의 태그 하나가 단계 하나이고, 태그를 하나씩 넘길 때마다 코드가 조금씩 덧붙습니다.
문서도 같은 방식으로 늘어납니다. step05-pmm 태그에는 Step 05까지의 문서만 있고, 마지막 태그에는 전부 들어 있습니다.
각 문서 끝의 "다음 단계로" 링크는 다음 태그로 넘어가야 실제 파일이 생기니, 미리 읽고 싶다면 git show learn:docs/step06-vmm.md 처럼 꺼내 보면 됩니다.
단계마다 같은 순서를 반복하길 권합니다.
먼저 그 단계의 문서를 처음부터 끝까지 읽습니다. 문서는 "왜 이게 필요한가"에서 출발해 "무엇을 어떤 순서로 만들었는가", "이 단계가 뒤 단계들을 위해 어떤 약속을 깔아 두는가"를 이야기처럼 설명합니다.
그다음 태그로 이동해 문서가 안내한 순서대로 코드를 읽습니다. 바로 앞 단계와 비교해 무엇이 늘었는지 보는 것이 가장 효율적이라, git diff 로 차이만 보는 습관을 들이면 좋습니다.
마지막으로 빌드해서 QEMU를 띄우고, 문서의 "확인하기"에 적힌 결과가 정말 나오는지 눈으로 봅니다.
여유가 있다면 이전 태그로 돌아가 "직접 만들어 보기"의 순서대로 혼자 구현한 뒤, 다음 태그의 코드와 비교해 보세요. 읽기만 했을 때와는 이해의 깊이가 완전히 다릅니다.
os-learn # 메뉴로 단계 고르기 · 빌드 · QEMU 실행 (dotfiles 의 bin/os-learn)
# 손으로 할 때
git checkout step05-pmm
git diff step04-keyboard step05-pmm # 이번 단계에서 바뀐 코드만
make CC=gcc LD=ld AS=as MKRSC=grub-mkrescue
qemu-system-x86_64 -cdrom micro.iso -m 512M -serial stdio
git checkout learn # 최신으로 돌아오기Makefile은 원래 macOS용(x86_64-elf-gcc, i686-elf-grub-mkrescue, -display cocoa)이라 Linux에서는 위처럼 도구 이름을 바꿔서 부릅니다.
Step 16부터는 디스크 이미지가, Step 18부터는 유저 프로그램 빌드가 추가로 필요해서 그때부터는 os-learn 을 쓰는 편이 훨씬 편합니다.
전체 여정은 네 부분으로 나뉩니다.
Part 1에서는 전원을 켜고 화면에 글자를 찍는 것부터 시작해, 인터럽트와 메모리 관리, 여러 실행 흐름을 번갈아 돌리는 스케줄러, 그리고 사람과 대화하는 셸까지 만듭니다. 여기까지는 모든 코드가 커널 권한으로 돕니다.
Part 2에서는 CPU의 권한 구분을 이용해 커널과 분리된 유저 모드(ring 3)를 만들고, 유저 코드가 커널에 부탁하는 통로인 시스템 콜을 엽니다.
Part 3에서는 디스크를 읽는 드라이버와 ext2 파일시스템, ELF 로더를 만들어 "따로 컴파일한 C 프로그램을 디스크에서 읽어 실행"하게 됩니다.
Part 4에서는 파일 디스크립터와 fork, exec, wait/exit 를 갖춰 유닉스식 프로세스 모델을 완성하고, 셸에서 그 프로그램들을 실행합니다.
Step 01은 처음 접하는 개념이 한꺼번에 몰려 있어서 다섯 개의 작은 단계(01-1 ~ 01-5)로 나눠 두었습니다. 작은 단계마다 눈으로 확인할 수 있는 결과가 하나씩 있어서, 어디서 막혔는지 바로 알 수 있습니다.
Part 1. 커널 기초 — "부팅해서 여러 일을 번갈아 할 수 있는 커널"
01 부팅 & 화면 출력 (01-1 GRUB 인식 ─ 01-2 32비트 화면 ─ 01-3 롱 모드 ─ 01-4 드라이버 ─ 01-5 printf)
02 IDT & 예외 ─ 03 PIC & 타이머 ─ 04 키보드 & 마우스
05 물리 메모리(PMM) ─ 06 페이징(VMM) ─ 07 힙(kmalloc)
08 프로세스 & 컨텍스트 스위칭 ─ 09 셸 & 인메모리 FS
Part 2. 유저 모드 — "커널과 분리된 ring 3 코드 실행"
10 GDT 유저 세그먼트 & TSS ─ 11 int 0x80 게이트 ─ 12 유저 메모리 매핑
13 부팅 로그 강화 ─ 14 ring 3 진입 & write/exit
Part 3. 디스크와 프로그램 — "디스크의 실행 파일을 읽어 실행"
15 PCI 스캔 ─ 16 ATA 디스크 ─ 17 ext2 파일시스템 ─ 18 ELF 로더 & /init
Part 4. 유닉스 프로세스 — "fork · exec · wait 로 프로그램을 띄우는 셸"
19 파일 디스크립터 ─ 20 fork ─ 21 exec ─ 22 wait/exit ─ 23 셸에서 실행
| # | 태그 / 문서 | 한 줄 요약 |
|---|---|---|
| 01-1 | step01-1-multiboot | 빌드 체계와 multiboot2 헤더 — GRUB이 커널을 알아보게 |
| 01-2 | step01-2-vga32 | 32비트에서 화면 메모리에 직접 OK 쓰기, GRUB이 준 값 보존 |
| 01-3 | step01-3-longmode | 페이지 테이블 · GDT 로 64비트 롱 모드 진입, 첫 C 함수 |
| 01-4 | step01-4-vga-serial | VGA · 시리얼 드라이버, 포트 I/O, 최소 문자열 함수 |
| 01-5 | step01-boot | 작은 printf — Step 01 완성 |
| 02 | step02-idt | CPU 예외가 나면 레지스터를 덤프하고 멈추는 틀 |
| 03 | step03-pic-timer | 하드웨어 인터럽트(IRQ)를 C 함수로 받는 틀 + 100Hz 타이머 |
| 04 | step04-keyboard | 키보드 링 버퍼 + keyboard_getchar(), 마우스 커서 |
| 05 | step05-pmm | 4KB 물리 프레임을 빌려 주고 돌려받는 할당기 |
| 06 | step06-vmm | 가상 주소 ↔ 물리 프레임 매핑 (vmm_map_page) |
| 07 | step07-heap | 크기가 제각각인 메모리를 주는 kmalloc / kfree |
| 08 | step08-process | PCB + swtch 로 커널 스레드를 번갈아 실행 |
| 09 | step09-shell | 명령을 입력받는 셸, 메모리 위의 디렉터리 트리 |
| 10 | step10-gdt-tss | ring 3 세그먼트, ring 3 → 0 전환 때 쓸 커널 스택(TSS) |
| 11 | step11-syscall-gate | 유저가 커널을 부르는 문 int 0x80 |
| 12 | step12-user-mapping | 유저가 접근할 수 있는 페이지 (U/S 비트) |
| 13 | step13-bootlog | 타임스탬프 로그(klog) · 시리얼 디버그(kprintf) |
| 14 | step14-ring3 | iretq 로 ring 3 진입, 첫 시스템 콜 write/exit |
| 15 | step15-pci | PCI 설정 공간 읽기, 장치 목록 |
| 16 | step16-ata | 디스크 섹터 읽기/쓰기 (ATA PIO) |
| 17 | step17-ext2 | 섹터 위에 파일 · 디렉터리 해석 (ext2) |
| 18 | step18-elf-init | 디스크의 C 프로그램(ELF)을 메모리에 올려 실행 |
| 19 | step19-fd | open/read/write/close — 파일 디스크립터 |
| 20 | step20-fork | 주소 공간을 복제해 자식 프로세스 만들기 |
| 21 | step21-exec | 내 주소 공간을 새 프로그램으로 갈아 끼우기 |
| 22 | step22-wait-exit | 종료 코드 전달, 좀비 회수, 프로세스별 CR3 |
| 23 | step23-shell-spawn | 셸에서 디스크의 프로그램 실행 |
C의 포인터와 비트 연산에는 익숙해야 합니다. 페이지 테이블 엔트리, 세그먼트 디스크립터, IDT 게이트처럼 커널이 다루는 하드웨어 구조는 거의 전부 "몇 번 비트가 무슨 뜻"인 비트 필드이기 때문입니다.
x86-64 어셈블리는 직접 길게 짤 일은 적지만 읽을 수는 있어야 합니다. 이 프로젝트는 AT&T 문법을 쓰므로 mov %rax, %rbx 는 "rax 를 rbx 로" 라는 뜻이고, 피연산자 순서가 Intel 문법과 반대라는 점만 기억하면 대부분 읽힙니다.
System V AMD64 호출 규약도 자주 등장합니다. 함수 인자는 rdi, rsi, rdx, rcx, r8, r9 순서로 레지스터에 담겨 오고, 반환값은 rax 에 담기며, rbx, rbp, r12–r15 는 호출된 함수가 보존해 줘야 하는 레지스터입니다. 이 마지막 사실은 Step 08의 컨텍스트 스위칭에서 결정적인 역할을 합니다.
막히면 OSDev Wiki, Intel SDM Vol. 3, MIT 6.S081의 xv6 교재를 참고하세요. 이 프로젝트의 많은 설계가 xv6와 닮아 있습니다.