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.