Kesäloman viimeisen viikon kunniaksi päätin koodata oman käyttöjärjestelmän. En niinkään tehdäkseni omaa Linux-kloonia, vaan kokeillakseni millaista nykyään on ohjelmoida raakaa x86-arkkitehtuurin PC:tä.
Oman käyttöjärjestelmän tekeminen on yllättävän helppoa, jos osaa koodata hieman assemblerilla. PC:t käynnistyvät nykyään USB-tikulta ja vielä kätevämpää on tehdä kokeiluja esimerkiksi VirtualBoxissa. Debuggaamisessa toimii hyvin Bochs, jonka logeista näkee heti, mikä meni vikaan ja mitä prosessorin rekistereissä oli silloin.
Assembly-koodaaminen onnistuu kätevimmin Linuxilla tai Mac OS X:llä, jonka mukana tulee NASM (Netwide Assembler) sekä GCC ja muut tarvikkeet. Yksinkertaisen MBR-bootloaderin voi tehdä pelkästään NASMia käyttäen (esimerkki alempana).
BIOS ja koneen käynnistyminen
Tavallisessa PC:ssä BIOSin asetuksista voi valita, miltä massamuistilta käyttöjärjestelmä ladataan. Ennen vanhaan oli kätevintä käyttää 1.44MB korppuasemaa kokeilemiseen, mutta nykyään USB-tikku on paras optio.
Kun kone käynnistyy, BIOS lataa USB-tikulta ensimmäisen blokin (512 tavua) muistiosoitteeseen 0x07c0:0000. Tätä blokkia kutsutaan MBR:ksi (Master Boot Record). Tavallisesti siihen mahtuu 440 tavua konekielistä ohjelmakoodia, jota seuraa partitiotaulu. Jos ei tarvitse partitioita, ohjelmakoodi voi olla 510 tavua pitkä.
BIOS tarkistaa, että muistiin ladattu MBR-blokki päättyy maagisiin tavuihin 0xaa55. Sitten se vain hyppää osoitteeseen 0x07c0:0000, jolloin kontrolli siirtyy omalle ohjelmakoodille. Tässä vaiheessa x86-prosessori on Real Modessa, eli rekisterit ovat 16-bittisiä, segmenttirekisterit toimivat perinteiseen tapaan ja muistin maksimimäärä on 1 megatavu.
Bootloaderin ohjelmointi
Tässä on esimerkki bootloaderista, joka tulostaa vain ruudulle "Loading..." ja pysäyttää sitten prosessorin:
; NASM MBR boot loader by Kenneth Falck 2009
[bits 16] ; 16-bit code
[org 0x7c00] ; BIOS loads us at 0x07c0:0000
jmp 0x0000:initialize_bios ; reset code segment to 0x0000 with long jump
initialize_bios:
xor ax, ax
mov ds, ax ; reset data segments to 0x0000
mov es, ax
mov [bootdrive], dl ; store boot drive
mov si, welcome ; print welcome string
call print
;jmp load_kernel_header ; proceed to load kernel
halt:
hlt ; halt CPU to save power
jmp halt ; loop if halt interrupted
print: ; Print string in SI with bios
mov al, [si]
inc si
or al, al
jz exit_function ; end at NUL
mov ah, 0x0e ; op 0x0e
mov bh, 0x00 ; page number
mov bl, 0x07 ; color
int 0x10 ; INT 10 - BIOS print char
jmp print
exit_function:
ret
data:
welcome db 'Loading…', 0 ; welcome message
error db 'Error', 0 ; error message
bootdrive db 0x00 ; original BIOS boot drive
times 510 - ($ - $$) db 0 ; filler to 510 bytes
dw 0xaa55 ; boot signature (fills to 512 bytes)
Olettaen että koodinpätkä on tallennettu nimellä bootloader.asm, sen voi kääntää binääri-imageksi komentamalla:
$ nasm -f bin -o bootloader.img bootloader.asm
Käynnistäminen USB-tikulta
Syntynyt 512 tavun mittainen bootloader.img voidaan kirjoittaa suoraan USB-tikulle. Sitä ei siis kopioida tikulla olevaan FAT16-tiedostojärjestelmään tiedostoksi, vaan tikun koko sisältö ylikirjoitetaan raa'asti. (HUOM: tikun aiempi sisältö tuhoutuu.) Olettaen että USB-tikun laitenimi on /dev/diskX, komento kuuluu:
$ dd if=bootloader.img of=/dev/diskX
$ diskutil eject /dev/diskX # Mac OS X
Nyt tikun voi kytkeä mihin tahansa PC:hen ja määritellä BIOSista koneen käynnistymään USB-tikulta.
Käynnistäminen VirtualBoxissa
VirtualBox ei osaa käynnistyä USB-tikulta, joten siinä on helpointa tehdä MBR-imagesta virtuaalinen kovalevy. Tämä onnistuu seuraavalla komennolla (*):
$ VBoxManage convertfromraw -format VDI bootloader.img bootloader.vdi
Lopuksi luodaan uusi virtuaalikone ja määritellään sille tavallinen IDE-kovalevy, jonka sisältö tulee tiedostosta bootloader.vdi. VBoxManage-komennolla voi skriptata kovalevyn päivittymään automaattisesti siten, että aina kun bootloaderista käännetään uusi versio, VirtualBox generoi ja ottaa käyttöön uuden imagen.
(*) Jos VBoxManage valittelee, että bootloader.img on liian pieni, sen perään voi lisätä vaikkapa megatavun nollia /dev/zerosta. Näillä ei ole vaikutusta, koska BIOS kuitenkin lataa muistiin vain ensimmäiset 512 tavua.
Seuraavassa osassa Protected Mode ja A20-linjan aktivointi.