Tento seriál se bude zabývat, jak už asi tušíte, vývojem vlastního operačního systému. Nejdůležitější součástí je zcela určitě kernel (jádro), které se stará o klíčové prvky jakožto správa procesů, přidělování prostředků, obsluha periférií. Další částí je obvykle už software či knihovny, které mají zjednodušit a urychlit vývoj uživatelských aplikací.
My se budeme bavit především o jádru, postupně mu budeme přidávat nové schopnosti a posléze si předvedeme i spuštění kódu v uživatelském prostoru. Protože je architektura procesorů zvaná x86 nejrozšířenější, bude náš systém určen právě pro ni, je ale docela možné, že později nakousneme další – ku příkladu ARM, jež se s dobou, počtem mobilních a úsporných zařízení rozšiřuje nezastavitelným tempem.
Doma zajisté nějaký ten stroj s procesorem x86 máte, se softwarovým vybavením to už může být jinak, proto uvedu balíky, které k vývoji budeme potřebovat a jsou běžně dostupné pro platformy (GNU/Linux, BSD, Mac OS X, Windows)
- binutils – pro slinkování objektů (.o)
- gcc – pro kompilaci zdrojových kódů jazyka C (.c)
- nasm – pro překlad jazyka symbolických adres (.s)
- make – pro pohodlnější správu kódu
- qemu – pro jednoduché testování našeho OS
- cdrtools – pro vytvoření obrazu (*.iso)
Jak jste si pravděpodobně všimli, budeme psát pomocí Assembleru, tedy jazyka symbolických adres, bez kterého se naše jádro neobejde, a také jazyka C, který nabízí komfort vysokoúrovňových jazyků. Pro emulaci reálného PC použijeme Qemu, nebude ale problém použít ani virtualizační nástroje jako jsou VirtualBox, VMWare, apod.
Povídání už asi bylo dost, pojďme tedy začít pracovat. Nejdříve je nutné navrhnout topologii našeho jádra, tedy jak a kde ho uložíme do paměti RAM.
Protože i386, pro níž budeme psát, má některé adresy použité pro komunikaci s perifériemi jako je grafický adaptér, pc-speaker, sériový, paralelní port, atd. vyhrazené prostřednictvím BIOSu, budeme nuceni naše jádro uložit tam, kde ničemu nebude překážet – pro naše účely bude vhodná adresa 0×100000 (hexadecimální), ve známých jednotkách je to 1MB. Na této adrese bude začínat binární kód jádra, tedy kopie binárky, kterou na konci zkompilujeme. Otázka zní – Jak ho tam dostat prakticky? Udělá to za nás přece zavaděč (bootloader), my použijeme GRUB.
Bohužel danou adresu mu musíme sdělit nějakým způsobem, řešení spočívá v linkovacím skriptu.
Právě v něm si definujeme onu adresu, plní však i další funkci – dokáže programu „ld“ říct, jak má výstupní binárka ve známém formátu ELF vypadat, tedy přesněji jak mají být rozděleny sekce, jež tento formát využívá. Dále nám přijde vhod pro nastavení funkce, kterou bude kód jádra začínat. Mimochodem binární formát ELF se používá v mnoha unixových systémech, GNU/Linux nevyjímaje.
Takto může vypadat linkovací skript, pojmenujeme ho „link.ld“
/* Linkovací skript pro korektní vytvoření binárky jádra - je uložena ve formátu ELF a jednotlivé části jsou rozděleny do tzv. sekcí * binární kód začíná na adrese 0x100000, tedy 1MB, poté následuje sekce .data kde najdeme především statické řetězce. * Dále vidíme sekci .bss, do které přicházejí nestatické hodnoty proměnných, * Nakonec je zde .end, což nám říká, že na tomto místě skončilo jádro */ /* Náše jádro začíná procedurou start, definovanou ve zdrojovém kódu, viz. start.s */ ENTRY(start) SECTIONS { .text 0x100000 : { code = .; _code = .; __code = .; *(.text) . = ALIGN(4096); } .data : { data = .; _data = .; __data = .; *(.data) *(.rodata) . = ALIGN(4096); } .bss : { bss = .; _bss = .; __bss = .; *(.bss) . = ALIGN(4096); } end = .; _end = .; __end = .; }
Jak vidíme z kódu, ALIGN nám pomáhá „zarovnat“ každou ze sekcí do bloků o velikosti 4kB, což je užitečné v případě mapovaní do stránek a prací s virtualní pamětí.
Nyní přejdeme ke psaní samotného zdrojového kódu – použili jsme zaváděč GRUB, ten má schopnost číst hlavičku zvanou multiboot, představme si ji jako datovou strukturu, do které uložíme potřebné informace k tomu, aby mohlo být naše jádro úspěšně zavedeno.
Obsahuje unikátní identifikátor označený jako MAGIC – je tvořen konstantou, podle které zaváděč zjistí, že se jedná právě o multiboot hlavičku. Dále obsahuje FLAGS – uchovává bitové masky, podle nichž si vyžádáme specifické nastavení zaváděče. Další nedílnou součástí je CHECKSUM – zaručuje správnost integrity dat. Nakonec jsou uvedeny adresy odkazující na samotný multiboot, sekce .text, .data, .bss, .end a adresa na proceduru start, kde kód vlastního jádra teprve začíná.
Takto vypadá zdrojový kód k uložení multibootu do binárky a následně procedura start, která je spuštěna samotným zaváděčem – soubor start.s
; Zdrojový kód v jazyku symbolických adres ; Budeme pracovat s 32bitovými instrukcemi [BITS 32] ; Zpřístupníme jednotlivé ukazatele na sekce binárky kernelu, viz. linkovací skript link.ld EXTERN code, bss, end ; Zpřístupníme funkci main () definovanou v souboru main.c EXTERN main ; Vytvoříme globální procedury pro multiboot a start GLOBAL mboot, start ; Zde začíná multiboot hlavička, která má především zaváděči říct, kde v pamětí se jádro nachází mboot: ; Nastavíme potřebné multiboot makra MULTIBOOT_PAGE_ALIGN equ 1<<0 ; Chceme aby naše jádro bylo přiřazeno na adresu stránky MULTIBOOT_MEMORY_INFO equ 1<<1 ; Poskytne jádru informace o paměti MULTIBOOT_HEADER_MAGIC equ 0x1BADB002 ; Nastavíme předdefinovanou hodnotu, podle které najde zaváděč multiboot hlavičku MULTIBOOT_HEADER_FLAGS equ MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO ; Přiřadíme nastavené parametry MULTIBOOT_CHECKSUM equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS) ; Vypočteme kontrolní součet pro ověření integrity hlavičky zaváděčem ; Nastavené makra zde použijeme k zapsání do binárky jádra pomocí instrukce "dd" ; POZOR: sled následujících instrukcí je důležitý, nesmí být změněn dd MULTIBOOT_HEADER_MAGIC dd MULTIBOOT_HEADER_FLAGS dd MULTIBOOT_CHECKSUM ; Nyní do binárky uložíme jednotlivé adresy dd mboot ; adresa multiboot hlavičky dd code ; adresa binárního kódu - sekce .text dd bss ; adresa sekce .bss dd end ; adresa konce binárky jádra, .end dd start ; adresa, která řekne procesoru, kde s vykonáváním kódu má začít ; Poté co si zaváděč "přečte" multiboot hlavičku, zjistí, ; že má procesor skočit na níže definovanou proceduru start start: push ebx ; Ze 32bitového registru ebx získáme ukazatel na strukturu dat získanou ze zaváděče call main ; Konečně zavoláme funkci main () definovanou v jazyku C - soubor main.c jmp $ ; Po vykonání předchozí funkce skončíme na nekonečné smyčce ; Toto opatření zabrání vykonávání kódu, který by mohl následovat .. ; V této chvíli je vhodné ukončit činnost procesoru
Nyní přijde hlavní funkce kernelu main (), tu už definujeme pomocí vysokoúrovňového jazyka C – soubor main.c
/* main.c - jazyk C */ #define VGA_BASE 0xb8000 #define VGA_RES_X 80 #define VGA_RES_Y 25 /* vyčistí obrazovku */ void cls () { /* definice ukazatele VGA paměti */ unsigned short *vid_mem = (unsigned short *) VGA_BASE; unsigned i; /* cyklus projde všechny pixely */ for (i = 0; i < VGA_RES_X * VGA_RES_Y; i ++) vid_mem[i] = 0; /* vloží do buňky nulový znak */ } /* ukázková funkce pro výpis textu na obrazovku */ void print (char *str) { /* definice ukazatele VGA paměti */ unsigned short *vid_mem = (unsigned short *) VGA_BASE; unsigned i; for (i = 0; str[i]; i ++) vid_mem[i] = str[i] | 0x0f << 8; /* vložení znaku | nastavení barev fontu a pozadí */ } /* Zdrojový kód funkce main () - zavolá se pomocí kódu ve start.s (call main) * Její parametr s_mboot je ukazatel na datovou strukturu poskytovanou zaváděčem (push ebx) * zahrnuje několik užitečných informací jako informace o paměti, jméno zaváděče * podrobnosti na http://www.gnu.org/software/grub/manual/multiboot/multiboot.html#multiboot_002eh */ int main (void *s_mboot) { cls (); print ("Ahoj svete !"); return 0; }
Funkce main () pochopitelně v tomto stavu nedělá mnoho – přesněji nejdříve vyčistí obrazovku od toho, co tam bylo předtím pomocí funkce cls (). Ve finále vytiskneme text „Ahoj svete!“ na obrazovku, a hle! Funkce print () je zatím velmi primitivní, takže si neporadí ani s odstavci, atd – na ukázku ale vyhovuje.
Nyní stačí kernel přeložit do spustitelné podoby a otestovat buďto na reálném železe, nebo třeba v nástroji zvaném Qemu.
V poslední části se budeme věnovat Makefile scriptu, který nám kompilaci a testování značně zjednoduší.
Ukázkový soubor Makefile:
# Zde jsou definovány parametry pro kompilaci jádra CC =gcc CFLAGS =-c -m32 -nostdlib -nostartfiles -nodefaultlibs AS =nasm ASFLAGS =-f elf LD =ld LDFLAGS =-m elf_i386 -T link.ld SOURCES =start.o main.o KERNEL =kernel.bin all: $(SOURCES) kernel clean: -rm -f *.o $(KERNEL) .s.o: $(AS) $(ASFLAGS) $< .c.o: $(CC) $(CFLAGS) $< kernel: $(LD) $(LDFLAGS) -o $(KERNEL) $(SOURCES)
Celé jádro přeložíme pouhým napsaním příkazu „make“, pokud ho budeme chtít otestovat v praxi, je nutné nainstalovat GRUB a nastavit ho. Jednodušší je si stáhnout archív s kompletními kódy, přibaleným GRUBem a schopností tvořit CD obraz ISO pomocí „make image“ a také rovnou spouštět náš systém – „make qemu“. Archív najdete zde.
Věřím, že se vám první článek o tvorbě operačního systému líbil a že vám může pomoci s vývojem vlastního. Pokud se vám zdál příliš obtížný, doporučuji číst dokumentaci k i386 a pro zájemce doporučím web OSDev.org kde lze najít spoustu návodů a tutoriálů. Vývoj operačního systému vyžaduje spoustu nervů a času, ale přináší neocenitelné vědomosti.
Kdo ví – zrovna ten váš se jednou může rozšířit, ať už na desktopy, servery, routery, mobilní zařízení nebo něco úplně jiného.