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):
- Asetetaan MSR-rekisteri EFER.LME=1
- Rakennetaan PML4-taulu
- Ladataan PML4-taulun osoite CR3-rekisteriin
- Ladataan 64-bittinen GDT
- Aktioidaan sivutus asettamalla rekisteri CR0.PG=1
- 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!