Kirjoitin aiemmin osassa 1 MBR-bootloaderista ja osassa 2 Protected Modesta sekä A20-linjasta. Näillä eväillä pääsee jo 32-bittiseen tilaan ja hyödyntämään 4GB muistia, mutta nykyään kaikki prosessorit tukevat myös 64-bittisyyttä.

Standardia 64-bittistä tilaa kutsutaan Long Modeksi ja se on AMD:n kehittämä laajennus x86-prosessoreihin. Tässä tilassa rekisterit ovat 64-bittisiä ja muistia osoitetaan fyysisillä 52-bittisillä sekä virtuaalisilla 48-bittisillä osoitteilla.

Long Modeen on Protected Modea hankalampi siirtyä, koska se vaatii myös sivutuksen (paging) aktivoinnin. Sivutus taas edellyttää hieman monimutkaisen PML4-rakenteen määrittelyä. Lisäksi GDT on vähän eri muotoinen, koska senkin pitää tukea 64-bittisiä osoitteita. Itselläni kului koko päivä näiden kiemuroiden opiskelemiseen.

Tietoa on hiukan hankala googlailla, sillä sivutus on alun perin toteutettu 386:ssa yhdellä tavalla, sitten laajennettu PAE-tilalla, ja lopuksi vielä laajennettu lisää Long Modella. Hyvä lähde on AMD 64 System Programming (Volume 2) -opus, josta löytyy määrittelyt kaikille tietorakenteille.

Long Modeen siirtymisen askeleet ovat seuraavat, kun oletetaan, että prosessori on jo Protected Modessa ja PAE on aktivoitu (kävin ne läpi edellisessä artikkelissa):

  1. Asetetaan MSR-rekisteri EFER.LME=1
  2. Rakennetaan PML4-taulu
  3. Ladataan PML4-taulun osoite CR3-rekisteriin
  4. Ladataan 64-bittinen GDT
  5. Aktioidaan sivutus asettamalla rekisteri CR0.PG=1
  6. Hypätään long jumpilla 64-bittiseen koodiin

Käyn nämä askeleet tässä artikkelissa läpi yksityiskohtaisemmin. Samalla käy myös ilmi mielenkiintoisia tietoja siitä, miten 64-bittinen muistinkäsittely oikeasti toimii.

Muutama vinkki

Mac OS X:n mukana tuleva NASM-kääntäjä on vanha versio. NASMiin lisättiin 64-bittinen tuki vasta versiossa 2.0. Helpointa on asentaa MacPorts ja komentaa sitten:

sudo port install nasm

Tämän jälkeen voi käyttää /opt/local/bin/nasm -kääntäjää.

Toinen hyödyllinen työkalu on Bochs, joka emuloi x86-prosessoria. Siitä kannattaa ottaa uusin versio CVS:stä ja kääntää näillä optioilla:

./configure --prefix=/opt/bochs --enable-cpu-level=6 --enable-x86-64 --enable-mmx --enable-sse=4 --enable-long-phy-address --enable-a20-pin --enable-smp --enable-1g-pages --enable-debugger --enable-disasm

Näin saa käyttöön 64-bittisen tilan sekä debuggerin, joka on oikein kätevä ongelmien selvittelyssä.

EFER.LME (Long Mode Enable)

Ensimmäinen askel 64-bittiseen tilaan siirtymisessä on asettaa Long Mode Enable -bitti päälle EFER-rekisteristä:

mov ecx, 0x0c0000080  ; specify EFER MSR
rdmsr                 ; read EFER MSR into EAX
or eax, 0x00000100    ; set bit 8 (LME)
wrmsr                 ; write EFER MSR from EAX

Tässä vaiheessa prosessori on valmis siirtymään Long Modeen, mutta siirtyy siihen vasta, kun sivutus aktivoidaan.

PML4 - Page Map Level 4

Sivutusta varten täytyy alustaa neliportainen taulurakenne. Näiden taulujen täytyy olla alignattu 4-kilotavuittain tasaisiin osoitteisiin:

align 4096

pml4_table: ; Page Map Level 4 Table times 512 dq 0 ; 512 x 64-bit entries (initialized in code)

pdp_table: ; Page Directory Pointer Table times 512 dq 0 ; 512 x 64-bit entries (initialized in code)

page_directory: ; Page Directory Table times 512 dq 0 ; 512 x 64-bit entries (initialized in code)

identity_table: ; Page Table times 512 dq 0 ; 512 x 64-bit entries (initialized in code)

Ensimmäinen taso (PML4) sisältää 512 kpl 64-bittisiä pointtereita seuraavan tason tauluihin (PDPT). Ne sisältävät edelleen pointtereita PDT-tauluihin, jotka taas sisältävät pointtereita alimman tason Page-tauluihin. Tämä alin taso sisältää vihdoin pointtereita fyysiseen RAM-muistiin.

Rakenteen idea on mäpätä 48-bittiset virtuaaliset muistiosoitteet taulujen kautta fyysiseen RAM-muistiin. Mäppäys tapahtuu biteittäin muistiosoitteen osien mukaan:

bitit 47-39: PML4-taulun rivi 0-511 bitit 38-30: PDPT-taulun rivi 0-511 bitit 29-21: PDT-taulun rivi 0-511 bitit 20-12: Page-taulun rivi 0-511 bitit 11-0: tarkka muistipaikka 4096 tavun blokin sisällä

Tällä tavoin virtuaalista muistia voidaan osoittaa 256TB alueelta, ja jokainen 4096 kilotavun blokki voi fyysisesti sijaita jossain 4096TB alueella.

Koska muistiosoitus tapahtuu 4096 kilotavun tarkkuudella, kaikissa tauluissa alimmat 12 bittiä ovat käytettävissä erilaisiin lippuihin. Oleellisin on bitti 0 (Present), joka kertoo onko sivu tällä hetkellä muistissa vai ei. Tämän bitin avulla käyttöjärjestelmä voi swapata muistisivuja kovalevylle ja ladata niitä tarvittaessa takaisin muistiin. Prosessori heittää page faultin, kun sellaista muistia yritetään lukea, jonka Present-bitti on 0.

Kernel-ohjelmointia varten on hyödyllistä mäpätä PC:n muistiavaruuden ensimmäinen megatavu itseensä ja merkitä se presentiksi sekä read/writeksi. Tällöin normaalit muistiosoitteet, kuten esimerkiksi VGA:n tekstipuskuri (0xb000), toimivat normaalisti. Ohjelmointi on muutenkin helpompaa, koska käännetty assembly-koodi toimii sellaisenaan. Mäppäystä varten riittää, että kutakin tauluhierarkiaa on yksi kappale.

Tämä esimerkki rakentaa tarvittavat taulut muistiin:

init_pml4_table:              ; initialize page map level 4
        cld
        mov edi, pml4_table   ; clear everything with zeroes
        xor eax, eax
        mov ecx, 1024         ; 1024 x 32 bits (512 entries)
        rep stosd
        mov edi, pml4_table   ; first entry points to pdp_table
        mov eax, pdp_table
        or eax, 0x00000007    ; bits: user, read/write, present
        stosd
        xor eax, eax          ; zero higher 32 bits
        stosd

init_pdp_table: ; initialize page directory pointers cld mov edi, pdp_table ; clear everything with zeroes xor eax, eax mov ecx, 1024 ; 1024 x 32 bits (512 entries) rep stosd mov edi, pdp_table ; first entry points to page_directory mov eax, page_directory or eax, 0x00000007 ; bits: user, read/write, present stosd xor eax, eax stosd ; zero higher 32 bits

init_page_directory: ; initialize page directory cld mov edi, page_directory ; clear everything with zeroes xor eax, eax mov ecx, 1024 ; 1024 x 32 bit (512 entries) rep stosd mov edi, page_directory ; first entry points to identity_table mov eax, identity_table or eax, 0x00000007 ; bits: user, read/write, present stosd xor eax, eax ; zero higher 32 bits stosd

init_identity_table: ; identity table will map first 1MB to itself cld mov edi, identity_table ; clear everything with zeroes xor eax, eax mov ecx, 1024 ; 1024 x 32 bit (512 entries) rep stosd mov edi, identity_table ; generate 512 entries mov ecx, 512 mov ebx, 0x00000000 ; use ebx for the increasing pointer (0k, 4k, 8k..) init_identity_loop: mov eax, ebx ; lower 32 bits of entry or eax, 0x00000007 ; bits: user, read/write, present stosd xor eax, eax ; higher 32 bits of entry stosd add ebx, 0x1000 ; increment in 4k blocks dec ecx jnz init_identity_loop

Lopuksi koko höskä ladataan CR3-rekisteriin:

        mov eax, pml4_table
        mov cr3, eax                    ; store PML4 in CR3

64-bit Global Descriptor Table (GDT)

AMD lisäsi 64-bittisessä moodissa GDT:hen pari pientä jekkua. Perinteisessä Protected Modessa kernelin GDT koostuu tyypillisesti yhdestä 32-bittisestä koodisegmentistä sekä yhdestä datasegmentistä, jotka molemmat osoittavat 4GB muistiavaruutta.

64-bittinen GDT on rakenteeltaan sama kuin 32-bittinen, mutta osoitteilla (base address ja segment limit) ei ole enää merkitystä. Ne voi siis jättää nollaksi. Prosessori ignoroi myös monet muut kentät. GDT:n alkuun osoittavan deskriptorin osoitekenttä on pidennetty 64-bittisesi, joten se pitää muistaa muuttaa DD:stä DQ:ksi.

AMD otti GDT-segmenttien määrittelyissä käyttöön aiemman reserved-bitin, joka on nyt nimeltään CS.L. Koodisegmenteissä tämä bitti määrittelee sen, ajetaanko koodia 64-bittisessä (1) vai 32-bittisessä (0) tilassa. Käyttöjärjestelmä voi siis luoda prosesseille erilaisia segmenttejä sen mukaan, onko sovellukset käännetty 32-bittisiksi vai 64-bittisiksi.

Aiemmin 32-bit-mode-bittinä tunnettu bitti puolestaan on nyt nimeltään CS.D. Tämän bitin täytyy olla 0 silloin kun CS.L on 1. Kannattaa lukea AMD:n ohjekirjaa tarkkaan, sillä tämä logiikka oli ensin vaikea ymmärtää.

Omissa kokeiluissa voi määritellä vain yhden 64-bittisen koodisegmentin, koska kaikki koodihan on tietysti 64-bittistä. Datasegmenttikin tarvitaan, mutta se on lähinnä muodollisuus. Tässä esimerkki:

align 8
gdt64:
gdt64_null:
        dq 0x0000000000000000           ; null segment

KERNEL64_CODE equ $-gdt64 gdt64_code: ; Code segment dw 0x0000 ; segment-limit-15-0 dw 0x0000 ; base-address-15-0 db 0x00 ; base-address-23-16 db 10011000b ; P(1), DPL(00), always(11), C(0), R(0), A(0), base-address-23-16(0) db 00100000b ; G(0), CS.D(0), CS.L(1), AVL(0), segment-limit-19-16(0) db 0x00 ; base-address-31-24

KERNEL64_DATA equ $-gdt64 gdt64_data: ; Data segment dw 0x0000 ; segment-limit-15-0 dw 0x0000 ; base-address-15-0 db 0x00 ; base-address-23-16 db 10010000b ; P(1), DPL(00), always(10), C(0), R(0), A(0), base-address-23-16(0) db 00000000b ; G(0), CS.D(0), CS.L(0), AVL(0), segment-limit-19-16(0) db 0x00 ; base-address-31-24

gdt64_end: gdt64_desc: dw gdt64_end - gdt64 - 1 ; 64-bit Global Descriptor Table info dq gdt64

Vielä täytyy muistaa ladata tämä uusi 64-bittinen GDT prosessorille:

lgdt [gdt64_desc]

Sivutuksen aktivointi ja hyppy 64-bittiseen koodiin

Nyt ollaan jo voiton puolella. Sivutuksen edellyttämä PML4-tietorakenne on muodostettu ja ladattu CR3:een, joten sivutuksen voi aktivoida:

mov eax, cr0                    ; set bit 31 (PG) of CR0
or eax, 0x80000000
mov cr0, eax

Lopuksi hypätään oikeaan 64-bittiseen koodiin. Tässä KERNEL64_CODE on aiemmin määritellyn 64-bittisen GDT:n koodisegmentti:

jmp KERNEL64_CODE:boot_64       ; long jump to 64-bit code

[bits 64] boot_64: mov byte [0x00000000000b8000], ':' mov byte [0x00000000000b8002], '-' mov byte [0x00000000000b8004], ')' <lisää käyttöjärjestelmä tähän>

Valmis!