Firmwarestä ytimeen: käynnistysprosessi selitetty
Jokainen käyttöjärjestelmä alkaa ennen kuin se on kunnolla oma itsensä. CPU käynnistyy alustan määrittelemässä ympäristössä, firmware alustaa riittävästi laitteistoa ensimmäisen suoritettavan ohjelman lataamiseksi, ja tuo ohjelma valmistelee koneen ydintä varten. Vasta kun tämä ketju on tehnyt työnsä, käyttöjärjestelmä voi alkaa panna täytäntöön omia sääntöjään.
Tuo varhainen polku on helppo nähdä pelkkänä putkituksena, mutta käynnistysprosessi on osa turvallisuusmallia. EriXissä boot on ensimmäinen kohta, jossa epäluotettavista tavuista tulee luotettua suoritusta, ja se on myös ensimmäinen kohta, jossa koneen auktoriteetti käännetään eksplisiittiseksi rakenteeksi: varmennetuiksi imageiksi, muistikartoiksi, moduulideskriptoreiksi, entry-osoitteiksi, framebuffer-metadataksi, ACPI-osoittimiksi ja lopulta ytimen objekteiksi.
Jos tämä polku on huolimaton, kyvykkyysjärjestelmä alkaa valheesta. Tämä artikkeli käy läpi EriXin käynnistyspolun firmwarestä ytimeen: mitä UEFI antaa, mitä bootloaderin täytyy tehdä, mitä handoff sisältää ja miksi ytimeen tullaan higher-half-suoritettavana.
Boot alkaa firmwaren auktoriteetilla⌗
Nykyisillä EriX-kohteilla käynnistyspolku alkaa UEFI-ympäristöstä
x86_64:llä. UEFI on firmwareä, ja se toimii ennen käyttöjärjestelmää. Se
tarjoaa boot-aikaiset palvelut, joiden avulla EFI-sovellus voi lukea tiedostoja,
varata muistia, tarkastaa alustan muistikartan, löytää konfiguraatiotauluja
kuten ACPI:n, kysyä grafiikkaulostuloa GOP:n kautta ja poistua boot-palveluista
ennen kuin ydin ottaa koneen haltuun.
EriXin bootloader rakennetaan UEFI-sovellukseksi, joten firmware lataa ensin
bootloaderin ja kutsuu sen UEFI-entrypointia. Toteutuksessa tuo entrypoint on
efi_main, joka käyttää UEFI:n x86_64-kutsukonventiota. Tässä vaiheessa EriX ei
vielä hallitse konetta; se toimii firmwaren tarjoamassa ympäristössä.
Bootloader voi pyytää UEFI:a lukemaan tiedoston, varaamaan muistia, tarkastamaan firmware-tauluja ja valmistelemaan sivutauluja, mutta kaikki tämä auktoriteetti on väliaikaista. UEFI:n boot-palvelut eivät ole käyttöjärjestelmä. Ne ovat telineistöä, jota bootloaderin täytyy käyttää varovasti, tiivistää eksplisiittisiksi faktoiksi ja sitten jättää taakseen.
Bootloader on luotettua koodia⌗
Bootloader suoritetaan ennen kuin ydin voi pakottaa mitään sääntöjä, joten se on osa luotettua laskentapohjaa. Jos bootloader lataa väärät tavut, hyppää väärään osoitteeseen, hyväksyy peukaloidun imagen, merkitsee muistin väärin tai keksii auktoriteettia, joka ei perustu varmennettuun syötteeseen, ydin aloittaa kompromettoidulta perustalta. Jotta tämä riski pysyy auditoitavana, EriX pitää bootloaderin työn kapeana ja eksplisiittisenä:
- paikanna ja lataa
boot.img - validoi image ennen siihen luottamista
- validoi dynaaminen boot store
- relokoi dynaaminen ydin ja sen vaatimat varhaiset objektit
- rakenna deterministinen handoff-rakenne
- valmistele sivutaulut ja bootstrap-pino
- poistu UEFI:n boot-palveluista
- hyppää ytimeen yhden kerran
Tämä ei tarkoituksella ole yleiskäyttöinen boot-ympäristö. Nykyisessä skoopissa ei ole käynnistysvalikkopolitiikkaa, etäpalautuspolkua eikä yritystä jatkaa boot-kriittisen virheellisen syötteen jälkeen. Sääntö on varmentaa ennen suoritusta: jos boot imagea ei voi parsia, varmentaa, ladata, mapata tai kuvata johdonmukaisesti, käynnistysyritys epäonnistuu ennen kuin ydin saa kontrollin.
boot.img:n lataaminen⌗
Nykyinen UEFI-polku etsii tätä tiedostoa boot-volyymiltä. Polku on kiinteä nykyiselle profiilille, mikä pitää varhaisen median käsittelyn kapeana eikä tee bootista politiikan etsintäongelmaa:
\ERIX\BOOT.IMG
Bootloader avaa boot-volyymin UEFI-tiedostoprotokollien kautta, lukee tiedoston
UEFI:n varaamaan muistiin ja tekee halvan ensimmäisen tarkistuksen ERIXBOOT-
konttitaialle ennen syvempää parsintaa. Sen jälkeen lib-bootimg ottaa
vastuun: bootloader parsii imagen BootImage-rakenteena, varmentaa sen
allekirjoituksen ja hashit, tarkistaa manifestin arkkitehtuurin ja vasta sitten
alkaa purkaa payloadeja.
Tällä erottelulla on merkitystä, koska boot-media ei ole luotettava. Tiedosto
voi puuttua, olla katkaistu, olla virheellinen tai olla peukaloitu, eikä
bootloader saa kohdella sektiodeskriptoria totena vain siksi, että se tuli
levyltä. EriXissä boot.img-parseri ja -varmentaja ovat osa luotettua
boot-polkua, ja niiden täytyy hylätä virheellinen rakenne, virheelliset rajat,
virheelliset hashit, tukemattomat versiot ja yhteensopimattomat
arkkitehtuuritiedot ennen kuin yksikään ladattu tavu muuttuu suoritettavaksi.
Seuraavat artikkelit menevät syvemmälle boot.img-formaattiin ja imagejen
varmennukseen. Tämän artikkelin kannalta tärkeä asia on järjestys:
- lue tavut
- parsi rakenne
- varmista eheys
- tarkista kohdeyhteensopivuus
- validoi ja relokoi dynaamiset boot-artefaktit
- rakenna handoff-metadata
Suoritus tulee validoinnin jälkeen, ei ennen sitä. Tämä järjestys erottaa boot imagen käsittelemisen syötteenä sen käsittelemisestä auktoriteettina.
Ytimen lataaminen⌗
Kun boot.img on hyväksytty, bootloader tarvitsee ytimen, ja EriXin
boot-polussa ydin ladataan allekirjoitetusta dynaamisesta boot storesta.
Bootloader validoi dynaamisen ytimen ELF64 ET_DYN -objektina, tarkistaa EriXin
dynamic-link-metadatan, varmentaa riippuvuuksien nimet ja objektien hashit
allekirjoitettua metadataa vasten, lataa vaaditut jaetut objektit, soveltaa
hyväksytyt relokaatiot, pakottaa W^X-rajoitukset ja valmistelee dynaamisen
boot-katalogin ytintä varten.
Tämä kuulostaa paljolta, koska sitä se on, mutta turvallisuuden muoto pysyy suorana ja katselmoitavana:
- data tulee varmennetusta boot imagesta
- suoritettavan formaatin parsinta on eksplisiittistä
- segmenttien alueet tarkistetaan
- relokaatioiden sivuvaikutukset ovat rajattuja
- puuttuva tai epäjohdonmukainen dynaaminen metadata epäonnistuu ennen ytimen sisääntuloa
Bootloader on olemassa validoidakseen ja relokoidakseen dynaamisen ytimen ja kuvatakseen sitten varmennetun varhaisen objektigraafin ytimelle. Siitä ei tule yleistä dynaamista linkkeriä käynnissä olevalle järjestelmälle; tuo rooli kuuluu myöhemmäksi, kun ydin ja käyttäjätilan palvelumalli ovat olemassa.
Boot-metadatan säilyttäminen⌗
Dynaaminen ydin ei ole ainoa asia boot imagessa. Bootloader säilyttää myös ytimen ja varhaisen käyttäjätilajärjestelmän vaatiman ei-suoritettavan boot-metadatan, kun taas suoritettavat palvelut kuuluvat dynaamiseen objektigraafiin. Nämä suoritettavat artefaktit kuvataan dynaamisessa katalogissa objekteina, segmentteinä ja riippuvuusreunoina, jotka johdetaan allekirjoitetusta storesta ja manifestista.
Muut vaaditut sektiot ovat ei-suoritettavia blobbeja. Esimerkkejä ovat boot-konfiguraatio, dynamic-link-metadatastoret ja konsolifontin data. Ne kopioidaan mapattuun muistiin ja kuvataan vain luettavina moduuleina ilman entrypointia, koska blob ei ole suoritettava vain siksi, että se esiintyy boot imagessa.
Tällä erolla on merkitystä kyvykkyyssuunnittelulle. Boot-konfiguraation payloadin pitää olla varhaisen järjestelmän luettavissa, mutta sitä ei pidä kohdella koodina. Fonttiblob voi olla tarpeen framebuffer-jatkuvuudelle, mutta se ei tarvitse suoritusauktoriteettia. EriX säilyttää tämän eron handoffissa:
- dynaamisten objektien deskriptorit suoritettaville artefakteille
- dynaamisten segmenttien deskriptorit mapatuille suoritettaville ja data-alueille
- dynaamisten riippuvuuksien deskriptorit varhaisille objektisuhteille
SECTION_TYPE_BOOT_CONFIGboot-politiikkadatalle- moduulideskriptorit ei-suoritettaville boot-blobbeille
Handoff vie tämän eron eteenpäin, jotta ytimen ja rootd:n ei tarvitse arvata,
onko jokin boot-data suoritettavaa auktoriteettia, vain luettavaa konfiguraatiota
vai tavallista metadataa.
Handoff on sopimus⌗
Bootloader ei kutsu kernel-API:a, koska käynnissä olevaa ydintä ei vielä ole.
Sen sijaan bootloader rakentaa handoff-blobin: lib-handoff:n määrittelemän
versioidun binäärisopimuksen. Nykyisessä bootloaderista ytimeen kulkevassa
profiilissa se alkaa ERIXHK01-taialla, major/minor-version kentillä,
kokonaiskoolla, arkkitehtuuri- ja alustatunnisteilla sekä niitä seuraavien
taulujen offseteilla ja laskureilla.
Handoff voi sisältää useita dataluokkia, joita ydin tarvitsee ennen kuin se voi rakentaa oman runtime-näkymänsä koneesta:
- normalisoidut muistikartta-alkiot
- ladattujen moduulien deskriptorit
- ACPI RSDP -osoitin
- varmennettu build ID ja image hash
- framebuffer-jatkuvuuden metadata
- dynaamisten objektien deskriptorit
- dynaamisten segmenttien deskriptorit
- dynaamiset riippuvuusreunat
Tämä on ensimmäinen rakenteinen auktoriteetin siirto. Firmware antoi bootloaderille raakoja faktoja ja väliaikaisia palveluja, ja varmennettu boot image antoi bootloaderille allekirjoitettua payload-metadataa. Bootloader yhdistää nämä deterministiseksi kuvaukseksi siitä, mitä se latasi, mihin se sen sijoitti ja mihin ydin voi luottaa.
Handoff ei ole vihje eikä “best effort” -metadataa. Se on syöte, jonka pohjalta ydin alkaa rakentaa omaa näkemystään koneesta, ja siksi se kantaa laskureita, alkiokokoja, offsetteja, hasheja, tyyppejä, alueita ja versiokenttiä. Ytimellä täytyy olla mahdollisuus hylätä se.
Muistikartat täytyy normalisoida⌗
Firmwaren muistikartat eivät ole automaattisesti ytimen politiikalle sopivassa muodossa. UEFI raportoi alueita firmware-muistityypeillä, kun taas bootloader tietää myös muistista, jonka se varasi dynaamiselle ytimelle, varhaisille objektimappauksille, vaadituille boot-blobbeille, bootstrap-pinolle, handoff-sivuille, framebuffer-mappauksille ja alkuperäiselle boot imagelle. Nämä näkymät täytyy yhdistää ennen kuin ydin voi päätellä, mitä muistia on käytettävissä.
EriXissä bootloader ottaa tilannekuvan UEFI:n muistikartasta, lisää eksplisiittiset bootin omistamat alueet ja normalisoi tuloksen päällekkäisyyksiä sisältämättömiksi alueiksi EriXin muistilajeilla, kuten:
- käytettävä RAM
- varattu muisti
- ACPI reclaimable -muisti
- ACPI NVS -muisti
- MMIO/laitemuisti
- bootloaderin omistama muisti
- boot imagen omistama muisti
Eksplisiittisesti bootin omistamat alueet voittavat yleiset firmware-luokitukset. Tämä on tärkeää, koska ydin ei saa vahingossa käsitellä boot imagea, handoff-blobia, dynaamisten objektien mappauksia, vaadittuja boot-blobbeja tai bootstrap-pinoa tavallisena vapaana RAM-muistina. Muisti on EriXissä auktoriteettia, joten jo näin varhain järjestelmä on tarkka siitä, kuka saa käyttää mitkä tavut uudelleen.
UEFI jätetään taakse⌗
UEFI:n boot-palvelut ovat hyödyllisiä, mutta väliaikaisia. Ennen ytimeen
hyppäämistä bootloader kutsuu ExitBootServices, mikä on käytännössä
yksisuuntainen siirtymä. Onnistuneen poistumisen jälkeen bootloaderin täytyy
kohdella boot-palveluiden osoittimia kelvottomina; se ei voi enää pyytää
firmwareä varaamaan muistia tai lukemaan tiedostoja sen jälkeen, kun
käyttöjärjestelmä ottaa vallan.
Tämä siirtymä on herkkä, koska UEFI vaatii bootloaderia poistumaan nykyisen muistikartta-avaimen avulla. Jos kartta muuttuu tilannekuvan ja poistumisen välillä, kutsu voi epäonnistua ja loaderin täytyy yrittää uudelleen tuoreella kartalla. EriXin UEFI-adapteri hoitaa tämän uudelleenyrityssilmukan alustakerroksessa.
Tärkeä suunnittelukohta on, että ytimeen tullaan sen jälkeen, kun bootloader on lopettanut firmware-palveluiden käytön. Ytimen ei pidä periä puoliksi auki olevaa firmware-riippuvuutta; sen pitää periä eksplisiittistä dataa.
Ensimmäisten sivutaulujen rakentaminen⌗
Ytimeen tullaan paginointi jo aktiivisena. Nykyiselle x86_64 UEFI -profiilille
bootloader rakentaa minimaaliset sivutaulut kahdella mappaustyypillä:
identiteettimapatun alamuistealueen varhaiseen bring-upiin ja tietyt
higher-half-mappaukset objekteille, joita ydin käyttää välittömästi.
Nykyinen toteutus mapittaa ensimmäisen 1 GiB:n 2 MiB:n sivuilla ja mapittaa myös APIC:n MMIO-ikkunat identiteettinä. Sen jälkeen se mapittaa higher-half-virtuaali- alueet ytimelle, ladatuille dynaamisille objekteille, vaadituille boot-blobbeille, handoff-blobille, framebufferille ja bootstrap-pinolle. Tämä riittää siihen, että ydin voi aloittaa odottamassaan osoiteavaruudessa.
Sivutaulut eivät ole lopullinen virtuaalimuistijärjestelmä. Ne ovat silta, jonka avulla ydin voi suorittaa, validoida handoffin, asentaa varhaisen CPU-tilan ja alkaa rakentaa todellista ytimen ja käyttäjätilan ympäristöä. Sillan täytyy silti olla oikein: jos ytimen entry-sivua ei ole mapattu, kone faulttaa heti; jos handoff-blob on mapattu väärään virtuaaliosoitteeseen, ydin lukee roskaa; jos pino puuttuu, sisääntulo epäonnistuu ennen kuin Rust-koodi ehtii tehdä paljon mitään.
Miksi higher-half-ydin?⌗
EriX tulee ytimeen virtuaalisen osoiteavaruuden ylemmässä puoliskossa. Tämä tarkoittaa, että ydin suoritetaan korkeissa virtuaaliosoitteissa sen sijaan, että se olisi linkitetty ja suoritettu vain matalassa identiteettimapatulla alueella. Se on yleinen ydinsuunnittelu, koska se antaa ytimelle vakaan virtuaaliosoitteiden alueen riippumatta siitä, mihin fyysinen muisti varattiin.
Higher-half-layout erottaa myös ytimen virtuaalitilan tavallisista käyttäjätilan alueista ja antaa ytimen pitää omat mappauksensa läsnä myöhempien osoiteavaruuden vaihtojen yli, kun käyttäjätilan mappaukset voivat vaihdella tehtäväkohtaisesti. Osoitelayout ei yksin pakota koko turvallisuusmallia, mutta se tukee rajaa tekemällä ytimen muistista erillistä tavallisesta prosessimuistista.
EriXissä bootloader vastaa siitä, että ensimmäinen higher-half-sisääntulo on
mahdollinen. Se lataa ytimen ELF-virtuaaliosoitteiden mukaan, mapittaa nuo
virtuaaliosoitteet varatuille fyysisille sivuille, luo bootstrap-pinon
higher-halfiin, mapittaa handoff-blobin tunnettuun higher-half-osoitteeseen,
lataa cr3:n ja hyppää ytimen entrypointiin.
Hypyn kohdalla nykyinen x86_64-sopimus on tarkoituksella pieni ja eksplisiittinen. Bootloader antaa vain sen tilan, jota ydin tarvitsee aloittaakseen handoffin validoinnin ja oman varhaisen CPU-tilansa asentamisen:
- SysV ABI
rdisisältää handoff-osoittimenrsposoittaa bootstrap-pinoon odotetulla kohdistuksella- paginointi on aktiivinen
- kontrolli ei palaa
Tämä ABI on pieni tarkoituksella. Mitä vähemmän implisiittisestä tilasta ytimen entry riippuu, sitä helpompi raja on auditoida.
Ytimeen siirtyminen⌗
Ytimen puolella sisääntulo alkaa ennen kuin koko kernel-runtime on olemassa.
Dynaaminen ydin julkaisee erix_dynlink_entry:n, joka siirtyy varhaiseen
ydinpolkuun handoff-osoittimen kanssa, ja ensimmäinen työ on puolustavaa eikä
politiikkapainotteista.
Ydin poistaa keskeytykset käytöstä, alustaa varhaisen sarjaulostulon, tarkistaa
ettei handoff-osoitin ole nolla, lukee handoffin koon otsakkeesta, pakottaa
handoffille enimmäiskoon ja validoi sitten koko blobin lib-handoff:n kautta.
Rakenteellisen validoinnin jälkeen ydin soveltaa omia politiikkatarkistuksiaan:
- arkkitehtuurin täytyy olla
x86_64 - alustan täytyy olla UEFI
- build ID ei saa olla tyhjä
- dynaamisen katalogin taulujen täytyy olla sisäisesti johdonmukaisia
- dynaamisten objektien nimet, hashit, segmentit, riippuvuudet ja storen alueet täytyy validoida ennen käyttöä
Bootloader rakensi handoffin jo, mutta ydin silti validoi sen. Luotetut komponentit eivät saa ohittaa sopimuksia vain siksi, että toinen luotettu komponentti tuotti datan. Versioidun handoffin tarkoitus on, että molemmat puolet voivat sopia täsmälleen siitä, mitä siirrettiin.
Vasta sen jälkeen ydin siirtyy syvemmälle varhaiseen alustukseen: GDT:n asennukseen, IDT:n asennukseen, syscall-polun alustukseen, valinnaiseen varhaisen konsolin alustukseen ja lopulta bootstrap-orkestrointiin ensimmäiselle root-tehtävälle. Ydin muuttuu ytimeksi vähitellen, ja ensimmäiseksi se tarkistaa maan jalkojensa alla.
Boot on auktoriteetin kääntämistä⌗
Bootia on houkuttelevaa kuvata sanomalla “lataa ydin ja hyppää”. Se on teknisesti totta, mutta se ohittaa käyttöjärjestelmäsuunnittelun olennaisen kohdan. EriXissä boot on auktoriteetin kääntämistä: firmwaren auktoriteetti muuttuu eksplisiittisiksi boot-faktoiksi, boot imagen tavut muuttuvat varmennetuiksi sektioiksi, ELF-tiedostot muuttuvat mapatuiksi suoritettaviksi alueiksi, blobit muuttuvat ei-suoritettaviksi moduulideskriptoreiksi, firmwaren muistikartat muuttuvat normalisoiduiksi muistialueiksi, dynamic-link-metadata muuttuu rajatuksi objektigraafiksi ja framebuffer-tila muuttuu jatkuvuusmetadataksi.
Kaikesta tästä tulee handoff-blob, ja ydin vastaanottaa blobin ja päättää, onko se hyväksyttävä. Vasta sitten se voi alkaa muuttaa koneen resursseja ytimen objekteiksi, kyvykkyyksiksi, osoiteavaruuksiksi, endpointeiksi ja ensimmäiseksi käyttäjätilan tehtäväksi. Siksi boot-prosessi kuuluu kyvykkyyskäyttöjärjestelmän keskusteluun: kyvykkyysmalli ei ala bootin jälkeen jälkiliitteenä, vaan se riippuu siitä, ettei boot salakuljeta mukaan ambienttia auktoriteettia.
Bootloaderin ei pitäisi vain sanoa ladanneensa joitakin asioita. Sen pitäisi sanoa täsmälleen mitä se latasi, missä se on, mitä se on, miten se varmennettiin ja mitä alustafaktoja se havaitsi. Tämä on ero hypyn ja handoffin välillä.
Mitä EriX pitää pois bootloaderista⌗
Bootloader on voimakas, koska se suoritetaan aikaisin, ja juuri siksi sen pitää pysyä pienenä. EriX ei halua bootloaderin päättävän runtime-politiikkaa: sen ei pidä päättää, mikä palvelu omistaa muistipolitiikan, miten prosesseja valvotaan, miten ajureita hallitaan tai miten tiedostojärjestelmät koostetaan.
Nämä päätökset kuuluvat ytimelle ja käyttäjätilan järjestelmäpalveluille. Bootloaderin työ on kapeampi: validoi boot-artefakti, valmistele minimaalinen suoritusympäristö, kuvaa mitä se teki ja siirrä kontrolli.
Tällä rajalla on merkitystä TCB:n koolle. Enemmän ominaisuuksia sisältävä bootloader ei ole automaattisesti parempi, koska jokainen varhaisen bootin ominaisuus on koodia, joka suoritetaan ennen kuin ydin voi eristää sen. Jokainen parseri, fallback-polku, interaktiivinen tila ja politiikkapoikkeus kasvattaa luotetun käyttäytymisen määrää, jonka täytyy olla oikein ennen kuin järjestelmä käynnistyy.
Katse eteenpäin⌗
Tämä artikkeli käsitteli boot.img:tä enimmäkseen varmennettuna konttina.
Seuraava askel on avata tuo kontti ja katsoa sen suunnittelua suoraan.
Seuraava artikkeli selittää EriXin boot.img-formaatin: miksi järjestelmä
käyttää yhtenäistä imagea, miten sektiot asetellaan, mitä metadataa kannetaan ja
miten formaatti tukee toistettavia, deterministisiä boot-artefakteja.