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.

Published 20.8.2009