Kirjoitin edellisessä osassa PC:n bootloaderin ohjelmoimisesta. Tässä vaiheessa x86-prosessori on kuitenkin vielä melko rajoittuneessa 8086-emulaatiotilassa:
- 16-bittinen moodi (segmentointi käytössä)
- muistin maksimimäärä 1MB
- ei muistinsuojausta, sivutusta, task switchingia jne.
- koodin koko rajoittuu BIOSin lataamaan 512 tavuun
Muutamalla askeleella päästään paljon mielenkiintoisempaan 32-bittiseen tilaan:
- ladataan lisää koodia muistiin USB-tikulta BIOS interrupt 0x13:lla
- siirrytään x86 Protected Modeen (32-bit)
- aktivoidaan yli 1MB muisti asettamalla A20-linja 8042-kontrollerilla
BIOS INT 0x13
Koska MBR-bootloaderiin mahtuu alle 512 tavua koodia, sen on käytännössä pakko ladata lisää koodia muistiin käyttämällä BIOSia. BIOS on jo tallentanut tiedon käynnistyslevystä DL-rekisteriin. Levyn ensimmäinen blokki (512 tavua) on MBR, joten on helppoa tallentaa loput kernelistä seuraaviin blokkeihin ja lukea ne kerralla muistiin. Tämä esimerkki lataa 20 blokkia (10 kilotavua) osoitteeseen 0x0800:0000 ja käynnistää kernelin:load_kernel: ; DL must contain boot drive
mov ah, 0x02 ; read sectors from drive
mov al, 20 ; sectors to read
mov ch, 0x00 ; track
mov cl, 0x02 ; sector
mov dh, 0x00 ; head
mov bx, 0x0800
mov es, bx ; buffer segment
mov bx, 0 ; buffer address
int 0x13 ; INT 13 - BIOS load sector
jnc boot_kernel ; on success, load the code
jmp $ ; hang after failing
boot_kernel:
jmp 0x0000:0x8000 ; long jump to kernel code
Erillisen 10 kilotavuisen kernel-koodin voi laittaa suoraan bootloader.asm-tiedostoon MBR:n perään, tai sitten sen voi sijoittaa erilliseen kernel.asm-tiedostoon. Binäärikoodiksi käännetyt erilliset tiedostot voi helposti yhdistää tähän tapaan:
cat bootloader.img kernel.img > os.img
Syntyvän os.img-tiedoston voi sitten kirjoittaa USB-tikulle tai VirtualBox-kovalevyksi samaan tapaan kuin pelkän bootloaderinkin.
Protected Mode
Kernelin ensimmäinen operaatio on yleensä siirtyä Protected Modeen. Tässä esimerkki koodista, joka olettaa tulleensa ladatuksi osoitteeseen 0x0800:0000. Se asettaa tyhjän IDT:n (Interrupt Descriptor Table) ja 4GB muistiavaruuden täyttävän GDT:n (Global Descriptor Table), jolla kernel voi käsitellä muistia vapaasti. Keskeytykset täytyy laittaa pois päältä (CLI). Ne voidaan palauttaa myöhemmin (STI), kun on saatu tehtyä oikea IDT.
[bits 16]
[org 0x8000] ; use absolute origin 0x8000 (32k)
boot_16: ; kernel code starts here in 16-bit real mode
cli ; disable interrupts
lgdt [gdt_desc] ; load Global Descriptor Table
lidt [idt_desc] ; load Interrupt Descriptor Table
mov eax, cr0 ; set bit 0 (PE) of CR0 (protected environment enable)
or eax, 0x00000001
mov cr0, eax
mov eax, cr4 ; set bit 5 (PAE) of CR4 (physical address extension)
or eax, 0x00000010
mov cr4, eax
jmp KERNEL_CODE:init_32 ; long jump to 32-bit code
[bits 32]
init_32:
mov ax, KERNEL_DATA
mov ds, ax ; update data segment
mov es, ax ; update alt1 data segment
mov fs, ax ; update alt2 data segment
mov gs, ax ; update alt3 data segment
mov ss, ax ; update stack segment
mov esp, 0x00040000 ; stack pointer at 0x00040000 (256k)
call check_a20 ; enable A20 line
jmp boot_32
idt_desc:
dw 0x0000
dw 0x0000
dw 0x0000
gdt:
gdt_null:
dd 0x00000000 ; null segment
dd 0x00000000
KERNEL_CODE equ $-gdt
gdt_code: ; Code segment with base 0x00000000 limit 0xfffff * 4kb = 4GB
dw 0xffff ; segment limiter bits 0-15
dw 0x0000 ; base address bits 0-15
db 0x00 ; base address bits 16-23
db 10011010b ; present(1), privilege(00), data/code(1), code(1), conform(0), readable(1), access(0)
db 11001111b ; granularity(1), 32bitsize(1) reserved(0), prog(0), segment limiter bits 16-19 (1111)
db 0x00 ; base address bits 24-31
KERNEL_DATA equ $-gdt
gdt_data: ; Data segment with base 0x00000000 limit 0xfffff * 4kb = 4GB
dw 0xffff ; segment limiter bits 0-15
dw 0x0000 ; base address bits 0-15
db 0x00 ; base address bits 16-23
db 10010010b ; present(1), privilege(00), data/code(1), data(0), conform(0), readable(1), access(0)
db 11001111b ; granularity(1), 32bitsize(1), reserved(0), prog(0), segment limiter bits 16-19 (1111)
db 0x00 ; base address bits 24-31
gdt_end:
gdt_desc:
dw gdt_end - gdt - 1
dd gdt
Tämän jälkeen ajettavan koodin tulee olla 32-bittistä (NASMissa direktiivi [bits 32]). Segmenttirekistereissä pitää olla jokin GDT:ssä määritellyistä segmenteistä: 0x08 (KERNEL_CODE) tai 0x10 (KERNEL_DATA) ja muistinosoitus toimii absoluuttisesti 32-bittisenä.
A20-linja ja yli 1MB muisti
A20-linja on vanhan IBM AT:n ja 286-prosessorin peruja oleva viritys 8086-yhteensopivuuden säilyttämiseksi. Jotta muistia voidaan osoittaa 1MB-rajan yli, se pitää aktivoida ohjelmoimalla 8042-näppäimistökontrolleria sopivasti.
Tähän näyttää olevan useita erilaisia tapoja laitevalmistajasta riippuen. Esimerkiksi VirtualBox ja Bochs näyttävät aktivoivan A20-linjan automaattisesti jo bootissa, kun taas monissa oikeissa PC:issä se pitää itse aktivoida. Ohessa Linuxin peruja oleva hieman monimutkainen esimerkkikoodi, jota voi kutsua komennolla "call check_a20":
[bits 32]
check_a20:
push rax
push rbx
mov word [0x7dfe], 0xaa55
mov word [0x107dfe], 0x0000
mov word ax, [0x7dfe]
cmp ax, 0xaa55
je a20_is_enabled
call a20_enable
jmp check_a20
a20_is_enabled:
pop rbx
pop rax
ret
empty_8042:
in al, 0x64 ; 8042 status port
test al, 2 ; is input buffer full?
jnz empty_8042 ; yes, loop
ret
empty_8042_2:
in al, 0x64 ; 8042 status port
test al, 1 ; is input buffer full?
jnz empty_8042_2 ; yes, loop
ret
a20_enable:
call empty_8042
mov al, 0xad
out 0x64, al
call empty_8042
mov al, 0xd0
out 0x64, al
call empty_8042_2
in al, 0x60
push rax
call empty_8042
mov al, 0xd1
out 0x64, al
call empty_8042
pop rax
or al, 2
out 0x60, al
call empty_8042
mov al, 0xae
out 0x64, al
call empty_8042
ret
Seuraavassa osassa 64-bittinen Long Mode.