De la firmware la kernel: procesul de boot explicat
Fiecare sistem de operare începe înainte de a fi cu adevărat el însuși. CPU-ul pornește într-un mediu definit de platformă, firmware-ul inițializează suficient hardware pentru a încărca primul executabil, iar acel executabil pregătește mașina pentru kernel. Abia după ce acest lanț și-a făcut treaba poate sistemul de operare să înceapă să își aplice propriile reguli.
Este ușor să tratezi acest drum inițial ca pe simplă instalație, dar procesul de boot face parte din modelul de securitate. Pentru EriX, boot-ul este primul loc în care octeți neîncrezători devin execuție de încredere, și este și primul loc în care autoritatea mașinii este tradusă în structură explicită: imagini verificate, hărți de memorie, descriptori de module, adrese de intrare, metadate de framebuffer, pointeri ACPI și, în cele din urmă, obiecte de kernel.
Dacă acest drum este neglijent, sistemul de capabilități începe dintr-o minciună. Acest articol parcurge drumul de boot al EriX de la firmware la kernel: ce oferă UEFI, ce trebuie să facă bootloaderul, ce conține handoff-ul și de ce kernelul este intrat ca executabil higher-half.
Boot-ul începe cu autoritate de firmware⌗
Pe țintele actuale EriX, drumul de boot începe cu UEFI pe x86_64. UEFI
este firmware și rulează înaintea sistemului de operare. Oferă serviciile de
boot care permit unei aplicații EFI să citească fișiere, să aloce memorie, să
inspecteze harta de memorie a platformei, să descopere tabele de configurare
precum ACPI, să interogheze ieșirea grafică prin GOP și să iasă din serviciile
de boot înainte ca kernelul să preia controlul.
Bootloaderul EriX este construit ca aplicație UEFI, așa că firmware-ul încarcă
mai întâi bootloaderul și îi apelează punctul de intrare UEFI. În implementare,
acel punct de intrare este efi_main, folosind convenția de apel UEFI x86_64.
În acest moment EriX nu controlează încă mașina; rulează într-un mediu furnizat
de firmware.
Bootloaderul poate cere UEFI să citească un fișier, să aloce memorie, să inspecteze tabele de firmware și să pregătească tabele de pagini, dar toată această autoritate este temporară. Serviciile de boot UEFI nu sunt sistemul de operare. Sunt schelărie pe care bootloaderul trebuie să o folosească atent, să o rezume în fapte explicite și apoi să o lase în urmă.
Bootloaderul este cod de încredere⌗
Bootloaderul rulează înainte ca kernelul să poată impune ceva, ceea ce îl pune în baza de computație de încredere. Dacă bootloaderul încarcă octeții greșiți, sare la adresa greșită, acceptă o imagine modificată, etichetează greșit memoria sau inventează autoritate care nu provine din intrare verificată, kernelul pornește de pe o fundație compromisă. Pentru ca acest risc să rămână auditabil, EriX păstrează munca bootloaderului îngustă și explicită:
- localizează și încarcă
boot.img - validează imaginea înainte de a avea încredere în ea
- validează dynamic boot store
- relochează kernelul dinamic și obiectele sale timpurii cerute
- construiește o structură handoff deterministă
- pregătește tabele de pagini și o stivă de bootstrap
- iese din serviciile de boot UEFI
- sare în kernel o singură dată
Acesta nu este în mod deliberat un mediu de boot cu scop general. Nu există politică de meniu de boot în sfera actuală, nu există cale de recuperare la distanță și nu există încercare de a continua după intrare malformată critică pentru boot. Regula este verificare înainte de execuție: dacă imaginea de boot nu poate fi parsatǎ, verificată, încărcată, mapată sau descrisă coerent, încercarea de boot eșuează înainte ca kernelul să primească controlul.
Încărcarea lui boot.img⌗
Calea UEFI actuală caută acest fișier pe volumul de boot. Calea este fixă pentru profilul curent, ceea ce păstrează tratarea timpurie a mediului îngustă și evită să transforme boot-ul într-o problemă de descoperire a politicii:
\ERIX\BOOT.IMG
Bootloaderul deschide volumul de boot prin protocoalele de fișiere UEFI, citește
fișierul în memorie alocată de UEFI și face o primă verificare ieftină a
magic-ului de container ERIXBOOT înainte de parsarea mai profundă. După aceea,
lib-bootimg preia controlul: bootloaderul parsează imaginea ca BootImage,
îi verifică semnătura și hashurile, verifică arhitectura manifestului și abia
apoi începe să extragă payloaduri.
Această separare contează deoarece mediul de boot nu este de încredere. Fișierul
poate lipsi, poate fi trunchiat, poate fi malformat sau poate fi modificat, iar
bootloaderul nu are voie să trateze un descriptor de secțiune ca adevărat doar
pentru că a venit de pe disc. În EriX, parserul și verificatorul boot.img fac
parte din drumul de boot de încredere și trebuie să respingă structură rea,
limite rele, hashuri rele, versiuni nesuportate și date de arhitectură
incompatibile înainte ca orice octet încărcat să devină executabil.
Următoarele articole vor intra mai adânc în formatul boot.img și verificarea
imaginii. Pentru acest articol, punctul important este ordinea:
- citește octeți
- parsează structura
- verifică integritatea
- verifică compatibilitatea cu ținta
- validează și relochează artefactele dinamice de boot
- construiește metadatele de handoff
Execuția vine după validare, nu înainte. Această ordine este diferența dintre a trata imaginea de boot ca intrare și a o trata ca autoritate.
Încărcarea kernelului⌗
Odată ce boot.img a fost acceptat, bootloaderul are nevoie de un kernel, iar
în drumul de boot EriX kernelul este încărcat din dynamic boot store semnat.
Bootloaderul validează kernelul dinamic ca obiect ELF64 ET_DYN, verifică
metadatele dynamic-link EriX, verifică numele dependențelor și hashurile
obiectelor față de metadate semnate, încarcă obiectele partajate cerute, aplică
relocări aprobate, impune constrângeri W^X și pregătește un catalog de boot
dinamic pentru kernel.
Sună ca mult pentru că este mult, dar forma securității rămâne directă și revizuibilă:
- datele vin dintr-o imagine de boot verificată
- parsarea formatului executabil este explicită
- intervalele segmentelor sunt verificate
- efectele secundare ale relocărilor sunt constrânse
- metadatele dinamice lipsă sau inconsistente eșuează înainte de intrarea în kernel
Bootloaderul există pentru a valida și reloca kernelul dinamic, apoi pentru a descrie kernelului graful verificat de obiecte timpurii. Nu devine un linker dinamic general pentru sistemul care rulează; acel rol aparține mai târziu, după ce există kernelul și modelul de servicii în user space.
Păstrarea metadatelor de boot⌗
Kernelul dinamic nu este singurul lucru din imaginea de boot. Bootloaderul păstrează și metadate de boot neexecutabile cerute de kernel și de sistemul timpuriu în user space, în timp ce serviciile executabile aparțin grafului de obiecte dinamic. Aceste artefacte executabile sunt descrise de catalogul dinamic ca obiecte, segmente și muchii de dependență derivate din store-ul și manifestul semnate.
Alte secțiuni cerute sunt blobs neexecutabile. Exemplele includ configurație de boot, stores de metadate dynamic-link și date de font pentru consolă. Acestea sunt copiate în memorie mapată și descrise ca module read-only fără punct de intrare, deoarece un blob nu este executabil doar pentru că apare în imaginea de boot.
Această distincție este importantă pentru designul cu capabilități. Un payload de configurație de boot ar trebui să fie lizibil de sistemul timpuriu, dar nu ar trebui tratat ca drept cod. Un blob de font poate fi necesar pentru continuitatea framebufferului, dar nu are nevoie de autoritate de execuție. EriX păstrează această distincție în handoff:
- descriptori de obiecte dinamice pentru artefacte executabile
- descriptori de segmente dinamice pentru intervale executabile și de date mapate
- descriptori de dependențe dinamice pentru relații de obiecte timpurii
SECTION_TYPE_BOOT_CONFIGpentru date de politică de boot- descriptori de module pentru blobs de boot neexecutabile
Handoff-ul duce această diferență mai departe, astfel încât kernelul și rootd
să nu fie nevoite să ghicească dacă o bucată de date de boot este autoritate
executabilă, configurație read-only sau metadate obișnuite.
Handoff-ul este contractul⌗
Bootloaderul nu apelează un API al kernelului, deoarece nu există încă un kernel
care rulează. În schimb, bootloaderul construiește un blob de handoff: un
contract binar versionat definit de lib-handoff. În profilul curent
bootloader-către-kernel, acesta începe cu magic-ul ERIXHK01, câmpuri de
versiune majoră/minoră, dimensiune totală, identificatori de arhitectură și
platformă, apoi offseturi și contoare pentru tabelele care urmează.
Handoff-ul poate include mai multe clase de date de care kernelul are nevoie înainte să își poată construi propria vedere runtime asupra mașinii:
- intrări normalizate de hartă de memorie
- descriptori de module încărcate
- pointer ACPI RSDP
- build ID și hash de imagine verificate
- metadate de continuitate framebuffer
- descriptori de obiecte dinamice
- descriptori de segmente dinamice
- muchii de dependență dinamice
Aceasta este prima transferare structurată de autoritate. Firmware-ul a dat bootloaderului fapte brute și servicii temporare, iar imaginea de boot verificată i-a dat bootloaderului metadate de payload semnate. Bootloaderul le combină într-o descriere deterministă a ceea ce a încărcat, unde a pus acel lucru și în ce poate avea încredere kernelul.
Handoff-ul nu este un indiciu sau metadate “best effort”. Este intrarea din care kernelul începe să își construiască propria vedere asupra mașinii, motiv pentru care poartă contoare, dimensiuni de intrări, offseturi, hashuri, tipuri, intervale și câmpuri de versiune. Kernelul trebuie să îl poată respinge.
Hărțile de memorie trebuie normalizate⌗
Hărțile de memorie ale firmware-ului nu sunt automat modelate pentru politica kernelului. UEFI raportează regiuni cu tipuri de memorie firmware, în timp ce bootloaderul cunoaște și memoria pe care a alocat-o pentru kernelul dinamic, mapările de obiecte timpurii, blobs de boot cerute, stiva de bootstrap, paginile de handoff, mapările framebuffer și imaginea de boot originală. Aceste vederi trebuie unite înainte ca kernelul să poată raționa despre memoria disponibilă.
În EriX, bootloaderul capturează harta de memorie UEFI, adaugă regiuni deținute explicit de boot și normalizează rezultatul în intervale nesuprapuse cu tipuri de memorie EriX precum:
- RAM utilizabilă
- memorie rezervată
- memorie ACPI reclaimable
- memorie ACPI NVS
- memorie MMIO/dispozitiv
- memorie deținută de bootloader
- memorie deținută de imaginea de boot
Regiunile deținute explicit de boot câștigă peste clasificările generice ale firmware-ului. Acest lucru contează deoarece kernelul nu trebuie să trateze din greșeală memoria care conține imaginea de boot, blob-ul de handoff, mapările de obiecte dinamice, blobs de boot cerute sau stiva de bootstrap ca RAM liberă obișnuită. Memoria este autoritate în EriX, așa că sistemul este atent încă de acum la cine poate reutiliza care octeți.
Lăsarea UEFI în urmă⌗
Serviciile de boot UEFI sunt utile, dar temporare. Înainte de a sări în kernel,
bootloaderul apelează ExitBootServices, care este în practică o tranziție
într-un singur sens. După o ieșire reușită, bootloaderul trebuie să trateze
pointerii către servicii de boot ca invalizi; nu mai poate cere firmware-ului să
aloce memorie sau să citească fișiere după ce sistemul de operare preia
controlul.
Această tranziție este delicată deoarece UEFI cere bootloaderului să iasă folosind o cheie curentă a hărții de memorie. Dacă harta se schimbă între captură și ieșire, apelul poate eșua și loaderul trebuie să reîncerce cu o hartă proaspătă. Adaptorul UEFI al EriX gestionează această buclă de retry în stratul de platformă.
Punctul important de design este că se intră în kernel după ce bootloaderul a terminat de folosit servicii de firmware. Kernelul nu ar trebui să moștenească o dependență de firmware pe jumătate deschisă; ar trebui să moștenească date explicite.
Construirea primelor tabele de pagini⌗
Se intră în kernel cu paginarea deja activă. Pentru profilul UEFI x86_64
actual, bootloaderul construiește tabele de pagini minime cu două tipuri de
mapări: o regiune de memorie joasă mapată identic pentru bring-up timpuriu și
mapări higher-half specifice pentru obiectele pe care kernelul le va folosi
imediat.
Implementarea actuală mapează identic primul 1 GiB folosind pagini de 2 MiB și mapează identic și ferestrele MMIO APIC. Apoi mapează intervale virtuale higher-half pentru kernel, obiecte dinamice încărcate, blobs de boot cerute, blob-ul de handoff, framebuffer și stiva de bootstrap. Acest lucru este suficient pentru ca kernelul să înceapă în spațiul de adrese pe care îl așteaptă.
Tabelele de pagini nu sunt sistemul final de memorie virtuală. Sunt un pod care permite kernelului să se execute, să valideze handoff-ul, să instaleze stare CPU timpurie și să înceapă să construiască mediul real de kernel și user space. Podul trebuie totuși să fie corect: dacă pagina de intrare a kernelului nu este mapată, mașina face fault imediat; dacă blob-ul de handoff este mapat la adresa virtuală greșită, kernelul citește gunoi; dacă lipsește stiva, intrarea eșuează înainte ca codul Rust să poată face mare lucru.
De ce un kernel higher-half?⌗
EriX intră în kernel în jumătatea înaltă a spațiului de adrese virtual. Asta înseamnă că kernelul rulează la adrese virtuale înalte în loc să fie linkat și executat doar în intervalul jos mapat identic. Este un design comun de kernel deoarece îi oferă kernelului o regiune stabilă de adrese virtuale, independentă de locul unde a fost alocată memoria fizică.
Layoutul higher-half separă și spațiul virtual al kernelului de intervalele obișnuite ale user space-ului și permite kernelului să își păstreze propriile mapări prezente peste schimbări ulterioare de spațiu de adrese, în timp ce mapările user space pot varia pe task. Layoutul adreselor nu impune singur tot modelul de securitate, dar sprijină limita făcând memoria kernelului distinctă de memoria normală a procesului.
În EriX, bootloaderul este responsabil să facă posibilă intrarea inițială
higher-half. Încarcă kernelul conform adreselor virtuale ELF, mapează acele
adrese virtuale către pagini fizice alocate, creează o stivă de bootstrap în
jumătatea înaltă, mapează blob-ul de handoff la o adresă higher-half cunoscută,
încarcă cr3 și sare la punctul de intrare al kernelului.
La salt, contractul x86_64 curent este intenționat mic și explicit. Bootloaderul furnizează doar starea de care kernelul are nevoie pentru a începe validarea handoff-ului și instalarea propriei stări CPU timpurii:
- ABI SysV
rdiconține pointerul de handoffrspindică stiva de bootstrap cu alinierea așteptată- paginarea este activă
- controlul nu se întoarce
Acest ABI este mic prin design. Cu cât intrarea kernelului depinde de mai puțină stare implicită, cu atât limita este mai ușor de auditat.
Intrarea în kernel⌗
Pe partea kernelului, intrarea începe înainte să existe runtime-ul complet al
kernelului. Kernelul dinamic expune erix_dynlink_entry, care intră în drumul
timpuriu al kernelului cu pointerul de handoff, iar prima muncă este defensivă,
nu încărcată de politică.
Kernelul dezactivează întreruperile, inițializează ieșirea serială timpurie,
verifică faptul că pointerul de handoff nu este nul, citește dimensiunea
handoff-ului din header, impune o dimensiune maximă de handoff și apoi validează
întregul blob prin lib-handoff. După validarea structurală, kernelul aplică
propriile verificări de politică:
- arhitectura trebuie să fie
x86_64 - platforma trebuie să fie UEFI
- build ID nu trebuie să fie gol
- tabelele catalogului dinamic trebuie să fie intern coerente
- numele obiectelor dinamice, hashurile, segmentele, dependențele și intervalele store-ului trebuie validate înainte de utilizare
Bootloaderul a construit deja handoff-ul, dar kernelul tot îl validează. Componentele de încredere nu pot sări peste contracte doar pentru că o altă componentă de încredere a produs datele. Rostul unui handoff versionat este ca ambele părți să poată conveni exact asupra a ceea ce a fost transferat.
Abia după aceea kernelul merge mai adânc în inițializarea timpurie: instalare GDT, instalare IDT, configurarea căii de syscall, inițializare opțională a consolei timpurii și, în final, orchestration de bootstrap pentru primul task root. Kernelul devine kernel treptat, iar primul lucru pe care îl face este să verifice solul de sub picioare.
Boot-ul este traducere de autoritate⌗
Este tentant să descrii boot-ul ca “încarcă kernelul și sari”. Este tehnic adevărat, dar ratează punctul de design al sistemului de operare. Pentru EriX, boot-ul este traducere de autoritate: autoritatea firmware devine fapte de boot explicite, octeții imaginii de boot devin secțiuni verificate, fișierele ELF devin intervale executabile mapate, blobs devin descriptori de module neexecutabile, hărțile de memorie firmware devin regiuni de memorie normalizate, metadatele dynamic-link devin un graf de obiecte delimitat, iar starea framebuffer devine metadate de continuitate.
Toate acestea devin un blob de handoff, iar kernelul primește acel blob și decide dacă este acceptabil. Abia atunci poate începe să transforme resursele mașinii în obiecte de kernel, capabilități, spații de adrese, endpoints și primul task în user space. De aceea procesul de boot aparține unei discuții despre sisteme de operare bazate pe capabilități: modelul de capabilități nu începe după boot ca adaos; depinde ca boot-ul să nu strecoare autoritate ambientă.
Bootloaderul nu ar trebui să spună doar că a încărcat câteva lucruri. Ar trebui să spună exact ce a încărcat, unde este, ce este, cum a fost verificat și ce fapte de platformă a observat. Aceasta este diferența dintre un salt și un handoff.
Ce ține EriX în afara bootloaderului⌗
Bootloaderul este puternic deoarece rulează devreme, și tocmai de aceea trebuie să rămână mic. EriX nu vrea ca bootloaderul să decidă politică runtime: nu ar trebui să decidă ce serviciu deține politica de memorie, cum sunt supravegheate procesele, cum sunt gestionate driverele sau cum sunt compuse sistemele de fișiere.
Aceste decizii aparțin kernelului și serviciilor de sistem în user space. Munca bootloaderului este mai îngustă: validează artefactul de boot, pregătește mediul minim de execuție, descrie ce a făcut și transferă controlul.
Această linie contează pentru dimensiunea TCB. Un bootloader cu mai multe funcționalități nu este automat mai bun, deoarece fiecare funcționalitate în boot timpuriu este cod care rulează înainte ca kernelul să îl poată izola. Fiecare parser, cale de fallback, mod interactiv și excepție de politică crește cantitatea de comportament de încredere care trebuie să fie corectă înainte ca sistemul să pornească.
Privind înainte⌗
Acest articol a tratat boot.img în principal ca pe un container verificat.
Următorul pas este să deschidem acel container și să îi privim direct designul.
Următorul articol va explica formatul boot.img al EriX: de ce sistemul
folosește o imagine unificată, cum sunt așezate secțiunile, ce metadate sunt
purtate și cum formatul susține artefacte de boot reproductibile și
deterministe.