Vývoj vlastního jednoduchého operačního systému: principy a implementace

Cíle a rozsah vlastního jednoduchého OS

Vytvoření vlastního jednoduchého operačního systému je náročný, ale mimořádně poučný projekt. Cílem není konkurovat produkčním systémům, ale porozumět základním principům: zavádění systému, správě paměti, přerušení, plánování procesů, systémovým voláním, základním ovladačům a jednoduchému uživatelskému rozhraní. Tento článek představuje technickou roadmapu, návrhové vzory a praktické rady, jak se z nuly dopracovat k minimalistickému, avšak funkčnímu OS běžícímu v emulátoru nebo na reálném hardwaru.

Volba cílové platformy a architektury

Než napíšete jediný řádek kódu, zvolte CPU architekturu a bootovací ekosystém. Pro studijní projekt se osvědčují:

  • x86_64 + UEFI/BIOS: nejvíce dostupných materiálů, snadné spuštění v QEMU či Bochs, možnost využití GRUB a specifikace Multiboot.
  • RISC-V (rv64): čistá instrukční sada (ISA), moderní ekosystém, transparentní stránkování, vhodné pro výuku.
  • AArch64: reálné nasazení v embedded prostředí, složitější startovací proces (Device Tree, firmware), bohaté možnosti rozšíření.

Začněte s jednou platformou a jasným cílem: „boot na konzoli, výpis textu, inicializace paměti, jednoduchý plánovač vláken, několik systémových volání“.

Nástroje a build řetězec

  • Cross-kompilátor: vlastní GCC nebo Clang toolchain (např. x86_64-elf-gcc) umožňující tvorbu čistých binárek bez vazby na libc hostitelského systému.
  • Binutils: ld (linker), objcopy, objdump, nm pro manipulaci s artefakty.
  • Emulátor: QEMU pro rychlé iterace, Bochs pro detailní emulaci; GDB pro vzdálené ladění.
  • Build systém: Make či CMake doplněné skripty, reprodukovatelný build a verzování konfigurace.
  • Kontinuální testy: skripty, které nabootují OS v QEMU, validují výstup na sériové konzoli a vyhodnocují návratový kód.

Boot proces: od firmware po jádro

Bootovací postup se liší podle firmware a architektury:

  • BIOS (legacy): CPU startuje v reálném režimu (16bit), MBR načte bootloader, který přepne CPU do protected/long módu a předá řízení jádru.
  • UEFI: firmware načte PE/COFF binárku, poskytuje služby (protokoly), vaše EFI aplikace zavolá ExitBootServices a předá kontrolu jádru.
  • GRUB/Multiboot(2): robustní řešení, kdy GRUB připraví prostředí a předá parametry (mapu paměti, moduly, příkazovou řádku) jádru podle specifikace.

V obou případech potřebujete linker script (pro mapování sekcí do paměti), startovací kód (pro nastavení zásobníku, segmentů a skok do C kódu) a konvenci pro předání parametrů (Multiboot, Device Tree, UEFI hand-off struktura).

Režimy CPU a přepnutí do long módu (x86_64)

Pro architekturu x86_64 je běžný postup následující: inicializace GDT, povolení PAE, nastavení tabulek stránek pro identické mapování, zapnutí příznaků v registrech CR0 a CR4, přepnutí do long módu přes bit EFER.LME a skok do 64bitového kódu. Následně probíhá inicializace IDT a konfigurace maskování přerušení.

Správa paměti: fyzická, virtuální a alokátory

  • Mapa paměti: získejte ji od firmware (např. UEFI memory map) nebo bootloaderu (Multiboot). Vyřaďte rezervované oblasti.
  • Fyzický alokátor rámců: implementace bitmapy nebo buddy systému pro přidělování rámců o velikosti 4 KiB (případně větších).
  • Virtuální paměť: zavedení čtyřúrovňového stránkování (u x86_64: PML4 → PDPTE → PDE → PTE) a mapování jádra do vyšších adresových prostorů.
  • Kernel heap: jednoduchý slab nebo zone alokátor, případně kombinace buddy a slab pro malé objekty; pozor na fragmentaci a smyčky při alokaci během obsluh přerušení.

Přerušení, výjimky a časování

  • IDT: instalace bran pro zpracování výjimek (např. dělení nulou, page fault) a hardwarových IRQ.
  • Řadiče přerušení: PIC (8259) pro legacy systémy, APIC/IOAPIC a LAPIC timer pro moderní SMP systémy; v UEFI prostředí preferujte HPET nebo HPET MSI.
  • Časovače: PIT (8253) pro základní funkčnost, HPET pro vysokou přesnost, TSC deadline timer pro nízkou režii.

Ovladače základních zařízení

Startovní OS si vystačí s minimem:

  • Konzole: textový režim (VGA) nebo sériová linka (16550 UART) pro výpis logů a příkazovou řádku.
  • Úložiště: začněte s RAM diskem a později přidejte AHCI (SATA) nebo virtio-blk (v QEMU).
  • Vstup: PS/2 klávesnice nebo virtio-input; mapování scancode → klíč.
  • Síť: odložte na později, případně použijte virtio-net pro jednoduché rámce.

Souborové systémy: od RAMFS k FAT

Pro první iteraci je vhodný RAMFS – jednoduchý strom v paměti s pevně zabudovaným obsahem. Pro perzistenci přidejte čtečku FAT12/16/32 (snadná specifikace, vhodná pro bootovací média). Interně definujte VFS vrstvu (inode, dentry, superblock) a oddělte ji od ovladačů blokových zařízení.

Procesy, vlákna a plánování

  • Model adresního prostoru: jeden globální kernel space a oddělené user space mapy pro jednotlivé procesy.
  • Vlákna: kernelové kontexty s vlastními zásobníky; přepínání kontextu na základě ticku nebo události.
  • Plánovač: nejprve round-robin s časovým kvantem, později priority a vícestupňový scheduler (interactive vs. batch).
  • Synchronizace: spinlocky v jádře, semafory a mutexy v subsystémech; dbejte na problém priority inversion.

Systémová volání a ABI

Návrh malého a čistého API je klíčový. Začněte se systémovými voláními: write (pro konzoli nebo soubory), read, open/close, fork/exec/exit (nebo jednodušší spawn), sleep, gettime, mmap/brk. Zvolte volací konvenci (x86_64 SysV: registry rdi, rsi, rdx, r10, r8, r9 jako argumenty a rax pro číslo volání) a mechanismus přechodu do jádra (instrukce syscall/sysret nebo int 0x80).

Uživatelský prostor a minimalistická knihovna

Pro spuštění uživatelských programů vytvořte základní subset libc (např. printf, malloc/free, string.h), jednoduchý loader (ELF64 parser) a shell – i kdyby jen se sadou interních příkazů. Programy kompilujte cross-kompilátorem proti vaší libc.

Bezpečnostní pilíře od první iterace

  • Izolace: uživatelský režim vs. kernel (ring3/ring0), NX bit, oddělené stránkování, kontrola přístupu k I/O portům.
  • Validace vstupů: pečlivá kontrola parametrů systémových volání a hranic bufferů.
  • Neprivilegované API: rozhraní, která nevyžadují přímý přístup k hardwaru (VFS, device files).
  • Audit a logování: sériová konzole, kruhové buffery, jednoduché úrovně logů.

Debugging a testování

  • GDB remote: spuštění QEMU s ladicím rozhraním a připojení GDB; breakpointy ve startu i v jádře.
  • Symboly a mapy: generování symbolů a udržování mapy adres pro rychlé vyhledávání chyb.
  • Smoke testy: skripty ověřující při každém buildu „boot → prompt → spuštění testu → návratový kód“.

Modularita a správná separace vrstev

Udržujte jasné hranice mezi vrstvami: architektura (architekturně specifický kód), HAL (abstrakce hardwaru), jádro (správa paměti, plánovač, IPC), VFS/ovladače a uživatelský prostor. Striktní rozdělení minimalizuje vzájemnou závislost (coupling), usnadňuje portování a testování.

IPC a synchronizace procesů

Pro jednoduchý OS postačí pipes a signály. Později můžete přidat message queues nebo sdílenou paměť s pojmenovanými semafory. Pečlivě zvažte preempci a priority, aby IPC nevedlo k hladovění vláken.

Správa času a časových pásem

Zavádějte monotónní čas (pro plánování a timeouty) a reálný čas (RTC) pro časová razítka. Udržujte tickless režim, kde je to možné, aby se šetřila spotřeba CPU.

Jednoduché grafické prostředí (volitelné)

Po stabilizaci jádra můžete přidat framebuffer driver (VBE/UEFI GOP) a minimalistický kompozitor. Začněte s „Hello pixel“ a kreslením fontů; grafické uživatelské rozhraní však výrazně zkomplikuje správu vstupu, kompozici oken a bezpečnost.

Formáty binárek a loader

Doporučeným formátem je ELF64. Loader mapuje segmenty do paměti, nastaví vstupní bod, předá argv a envp a vytvoří počáteční vláknový kontext. ASLR implementujte až později; vyžaduje zdroj náhodnosti a adekvátní mapování knihoven.

Rozšíření: SMP a více jader

Po zprovoznění jádra na jednom CPU aktivujte další jádra (AP bring-up), nastavte per-CPU struktury a lokální časovače. Plánovač musí být škálovaný (run-queue pro každé CPU) a synchronizace musí zohledňovat lock contention.

Minimalistická politika napájení a úsporné režimy

I v jednoduchém OS můžete implementovat instrukci pro usínání CPU mezi tick-y (např. hlt na x86), případně základní ACPI pro vypnutí a restart systému. Pokročilejší P-stavy a C-stavy ponechte na pozdější iterace.

Distribuce, balení a spouštění

  • ISO/IMG: vytvořte bootovatelné ISO s GRUB nebo UEFI aplikací; přidejte kernel a initramfs či RAM disk.
  • Konfigurační soubory: grub.cfg nebo UEFI boot entries; volby jádra v příkazové řádce (např. root=ramfs).
  • Artefakty: publikujte ladicí symboly, mapy a dokumentaci ABI.

Licencování a třetí strany

I u studijního OS zvažte vhodnou licenci (MIT, BSD, GPL) a původ kódu (včetně fontů, mikroknihoven či obrázků). Vyvarujte se kopírování produkčních zdrojových kódů bez souladu s licencemi.

Roadmapa implementace krok za krokem

  1. Boot minimum: textový výpis na konzoli, vlastní linker script, start v long módu.
  2. IDT a výjimky: obsluha page fault, časovač, základní IRQ.
  3. Fyzická a virtuální paměť: alokátor rámců, mapování kernelu, jednoduchý heap.
  4. Konzole a I/O: sériová linka, základní ovladač klávesnice.
  5. Vláknový plánovač: přepínání kontextu, timerová preempce, synchronizace.
  6. Systémová volání: jádrové API, přechod do uživatelského prostoru, program „Hello world“.
  7. VFS a RAMFS: soubory, adresáře, file deskriptory; později čtečka FAT.
  8. Loader ELF: spouštění binárek, jednoduchá libc, shell.
  9. Testy a ladění: CI skripty, GDB scénáře, regresní testy.
  10. Rozšíření: SMP, virtio-blok a síť, IPC, základní bezpečnostní politiky.

Výkonnost a profilace

Měřte čas přepnutí kontextu, latenci přerušení, propustnost I/O a režii synchronizačních primitiv. Přidejte jednoduchý profiler (počítadla cyklů a časové stopky). Mikrooptimalizace odkládejte až do dosažení funkční stability.

Spolehlivost a zotavení z chyb