Hlavní navigace

Programujeme OS: hello world

21. 7. 2009
Doba čtení: 7 minut

Sdílet

Operační systém je softwarová záležitost, bez které by dnes nemohla existovat široká škála zařízení, je to věc, která probudí hardware k životu a plní nejrůznější funkce – všechny jsou si ale v jádru principiálně podobné. Někteří z vás, stejně jako já, rádi nakouknou, jak daná věc funguje a chtějí se naučit něco nového.

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.

CS24_early

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.

Byl pro vás článek přínosný?

Autor článku

Autor je studentem VŠB FEI a pracuje na několika projektech jako např. operační systém ZeX/OS nebo hra Tuxánci.