Ogni sistema operativo comincia prima di essere davvero sé stesso. La CPU parte in un ambiente definito dalla piattaforma, il firmware inizializza abbastanza hardware da caricare il primo eseguibile, e quell’eseguibile prepara la macchina per il kernel. Solo dopo che questa catena ha fatto il proprio lavoro il sistema operativo può iniziare ad applicare le proprie regole.

È facile trattare quel percorso iniziale come semplice impianto, ma il processo di boot fa parte del modello di sicurezza. Per EriX, il boot è il primo punto in cui byte non fidati diventano esecuzione fidata, ed è anche il primo punto in cui l’autorità della macchina viene tradotta in struttura esplicita: immagini verificate, mappe di memoria, descrittori di moduli, indirizzi di ingresso, metadati del framebuffer, puntatori ACPI e, alla fine, oggetti del kernel.

Se questo percorso è trascurato, il sistema di capability parte da una bugia. Questo articolo percorre il cammino di boot di EriX dal firmware al kernel: che cosa fornisce UEFI, che cosa deve fare il bootloader, che cosa contiene l’handoff e perché si entra nel kernel come eseguibile higher-half.


Il boot comincia con l’autorità del firmware

Sugli obiettivi attuali di EriX, il percorso di boot comincia con UEFI su x86_64. UEFI è firmware, e gira prima del sistema operativo. Fornisce i servizi di boot che permettono a un’applicazione EFI di leggere file, allocare memoria, ispezionare la mappa di memoria della piattaforma, scoprire tabelle di configurazione come ACPI, interrogare l’output grafico tramite GOP e uscire dai servizi di boot prima che il kernel prenda il controllo.

Il bootloader di EriX è costruito come applicazione UEFI, quindi il firmware carica prima il bootloader e chiama il suo entrypoint UEFI. Nell’implementazione, quell’entrypoint è efi_main, usando la convenzione di chiamata UEFI x86_64. In questo momento EriX non controlla ancora la macchina; sta girando dentro un ambiente fornito dal firmware.

Il bootloader può chiedere a UEFI di leggere un file, allocare memoria, ispezionare tabelle del firmware e preparare page table, ma tutta questa autorità è temporanea. I servizi di boot UEFI non sono il sistema operativo. Sono impalcatura che il bootloader deve usare con cura, riassumere in fatti espliciti e poi lasciarsi alle spalle.


Il bootloader è codice fidato

Il bootloader gira prima che il kernel possa applicare qualunque regola, e questo lo mette nella trusted computing base. Se il bootloader carica i byte sbagliati, salta all’indirizzo sbagliato, accetta un’immagine manomessa, etichetta male la memoria o inventa autorità che non deriva da input verificato, il kernel parte da una base compromessa. Per mantenere questo rischio auditabile, EriX tiene il lavoro del bootloader stretto ed esplicito:

  • localizzare e caricare boot.img
  • validare l’immagine prima di fidarsi
  • validare il dynamic boot store
  • rilocare il kernel dinamico e i suoi oggetti iniziali richiesti
  • costruire una struttura di handoff deterministica
  • preparare page table e uno stack di bootstrap
  • uscire dai servizi di boot UEFI
  • saltare nel kernel una sola volta

Questo non è deliberatamente un ambiente di boot general-purpose. Nell’ambito attuale non c’è politica di menu di avvio, non c’è percorso di recovery remoto e non c’è tentativo di continuare dopo input malformato critico per il boot. La regola è verificare prima di eseguire: se l’immagine di boot non può essere parsata, verificata, caricata, mappata o descritta in modo coerente, il tentativo di boot fallisce prima che il kernel riceva il controllo.


Caricare boot.img

Il percorso UEFI attuale cerca questo file sul volume di boot. Il percorso è fisso per il profilo corrente, il che mantiene stretto il trattamento precoce del supporto ed evita di trasformare il boot in un problema di scoperta di policy:

\ERIX\BOOT.IMG

Il bootloader apre il volume di boot tramite protocolli file UEFI, legge il file in memoria allocata da UEFI e fa un primo controllo economico del magic di contenitore ERIXBOOT prima del parsing più profondo. Dopo, lib-bootimg prende il controllo: il bootloader parsa l’immagine come BootImage, verifica firma e hash, controlla l’architettura del manifest e solo allora inizia a estrarre i payload.

Questa separazione conta perché il supporto di boot non è fidato. Il file può mancare, essere troncato, essere malformato o essere stato manomesso, e il bootloader non può trattare un descrittore di sezione come vero solo perché arriva dal disco. In EriX, il parser e verificatore di boot.img fa parte del percorso di boot fidato, e deve rifiutare struttura errata, limiti errati, hash errati, versioni non supportate e dati di architettura incompatibili prima che qualunque byte caricato diventi eseguibile.

I prossimi articoli entreranno più a fondo nel formato boot.img e nella verifica delle immagini. Per questo articolo, il punto importante è l’ordine:

  1. leggere byte
  2. parsare struttura
  3. verificare integrità
  4. controllare compatibilità del target
  5. validare e rilocare artefatti dinamici di boot
  6. costruire metadati di handoff

L’esecuzione viene dopo la validazione, non prima. Questo ordine è la differenza tra trattare l’immagine di boot come input e trattarla come autorità.


Caricare il kernel

Una volta accettato boot.img, il bootloader ha bisogno di un kernel, e nel percorso di boot di EriX il kernel viene caricato dal dynamic boot store firmato. Il bootloader valida il kernel dinamico come oggetto ELF64 ET_DYN, controlla i metadati dynamic-link di EriX, verifica nomi di dipendenze e hash di oggetti contro metadati firmati, carica gli oggetti condivisi richiesti, applica le rilocazioni approvate, impone vincoli W^X e prepara un catalogo di boot dinamico per il kernel.

Sembra molto perché è molto, ma la forma della sicurezza resta diretta e revisionabile:

  • i dati provengono da un’immagine di boot verificata
  • il parsing del formato eseguibile è esplicito
  • i range dei segmenti sono controllati
  • gli effetti collaterali delle rilocazioni sono vincolati
  • metadati dinamici mancanti o incoerenti falliscono prima dell’ingresso nel kernel

Il bootloader esiste per validare e rilocare il kernel dinamico, poi descrivere al kernel il grafo verificato degli oggetti iniziali. Non diventa un linker dinamico generale per il sistema in esecuzione; quel ruolo appartiene a dopo, quando esistono il kernel e il modello dei servizi in user space.


Preservare metadati di boot

Il kernel dinamico non è l’unica cosa nell’immagine di boot. Il bootloader preserva anche metadati di boot non eseguibili richiesti dal kernel e dal sistema iniziale in user space, mentre i servizi eseguibili appartengono al grafo di oggetti dinamico. Quegli artefatti eseguibili sono descritti dal catalogo dinamico come oggetti, segmenti e archi di dipendenza derivati dallo store e dal manifest firmati.

Altre sezioni richieste sono blob non eseguibili. Gli esempi includono configurazione di boot, store di metadati dynamic-link e dati del font della console. Vengono copiati in memoria mappata e descritti come moduli di sola lettura senza entrypoint, perché un blob non è eseguibile solo perché appare nell’immagine di boot.

Questa distinzione è importante per il design a capability. Un payload di configurazione di boot dovrebbe essere leggibile dal sistema iniziale, ma non dovrebbe essere trattato come codice. Un blob di font può essere necessario per la continuità del framebuffer, ma non ha bisogno di autorità di esecuzione. EriX preserva questa distinzione nell’handoff:

  • descrittori di oggetti dinamici per artefatti eseguibili
  • descrittori di segmenti dinamici per range eseguibili e dati mappati
  • descrittori di dipendenze dinamiche per relazioni tra oggetti iniziali
  • SECTION_TYPE_BOOT_CONFIG per dati di policy di boot
  • descrittori di moduli per blob di boot non eseguibili

L’handoff porta avanti questa differenza così il kernel e rootd non devono indovinare se un pezzo di dati di boot sia autorità eseguibile, configurazione di sola lettura o metadati ordinari.


L’handoff è il contratto

Il bootloader non chiama una API del kernel, perché non c’è ancora un kernel in esecuzione. Invece, il bootloader costruisce un blob di handoff: un contratto binario versionato definito da lib-handoff. Nel profilo attuale da bootloader a kernel, comincia con il magic ERIXHK01, campi di versione major/minor, dimensione totale, identificatori di architettura e piattaforma, poi offset e conteggi per le tabelle che seguono.

L’handoff può includere diverse classi di dati che il kernel deve avere prima di poter costruire la propria vista runtime della macchina:

  • voci normalizzate della mappa di memoria
  • descrittori di moduli caricati
  • puntatore ACPI RSDP
  • build ID e hash dell’immagine verificati
  • metadati di continuità del framebuffer
  • descrittori di oggetti dinamici
  • descrittori di segmenti dinamici
  • archi di dipendenza dinamici

Questa è la prima trasmissione strutturata di autorità. Il firmware ha dato al bootloader fatti grezzi e servizi temporanei, e l’immagine di boot verificata ha dato al bootloader metadati di payload firmati. Il bootloader combina tutto questo in una descrizione deterministica di ciò che ha caricato, dove lo ha messo e di cosa il kernel può fidarsi.

L’handoff non è un suggerimento o metadati “best effort”. È l’input da cui il kernel inizia a costruire la propria vista della macchina, ed è per questo che porta conteggi, dimensioni di entry, offset, hash, tipi, range e campi di versione. Deve essere possibile per il kernel rifiutarlo.


Le mappe di memoria devono essere normalizzate

Le mappe di memoria del firmware non sono automaticamente modellate per la policy del kernel. UEFI riporta regioni con tipi di memoria del firmware, mentre il bootloader conosce anche la memoria che ha allocato per il kernel dinamico, le mappature di oggetti iniziali, i blob di boot richiesti, lo stack di bootstrap, le pagine di handoff, le mappature del framebuffer e l’immagine di boot originale. Queste viste devono essere fuse prima che il kernel possa ragionare sulla memoria disponibile.

In EriX, il bootloader fa uno snapshot della mappa di memoria UEFI, aggiunge regioni esplicitamente possedute dal boot e normalizza il risultato in range non sovrapposti con tipi di memoria EriX come:

  • RAM utilizzabile
  • memoria riservata
  • memoria ACPI reclaimable
  • memoria ACPI NVS
  • memoria MMIO/dispositivo
  • memoria posseduta dal bootloader
  • memoria posseduta dall’immagine di boot

Le regioni esplicitamente possedute dal boot vincono sulle classificazioni generiche del firmware. Questo conta perché il kernel non deve trattare per errore la memoria che contiene l’immagine di boot, il blob di handoff, le mappature degli oggetti dinamici, i blob di boot richiesti o lo stack di bootstrap come RAM libera ordinaria. La memoria è autorità in EriX, quindi anche così presto il sistema è attento a chi può riutilizzare quali byte.


Lasciarsi UEFI alle spalle

I servizi di boot UEFI sono utili, ma temporanei. Prima di saltare al kernel, il bootloader chiama ExitBootServices, che in pratica è una transizione a senso unico. Dopo un’uscita riuscita, il bootloader deve trattare i puntatori ai servizi di boot come invalidi; non può continuare a chiedere al firmware di allocare memoria o leggere file dopo che il sistema operativo prende il controllo.

Questa transizione è delicata perché UEFI richiede al bootloader di uscire usando una chiave corrente della mappa di memoria. Se la mappa cambia tra lo snapshot e l’uscita, la chiamata può fallire e il loader deve riprovare con una mappa fresca. L’adapter UEFI di EriX gestisce quel ciclo di retry nello strato di piattaforma.

Il punto di design importante è che si entra nel kernel dopo che il bootloader ha finito di usare i servizi del firmware. Il kernel non dovrebbe ereditare una dipendenza firmware mezza aperta; dovrebbe ereditare dati espliciti.


Costruire le prime page table

Si entra nel kernel con la paginazione già attiva. Per il profilo UEFI x86_64 attuale, il bootloader costruisce page table minimali con due tipi di mappature: una regione di memoria bassa identity-mapped per il bring-up iniziale e mappature specifiche higher-half per gli oggetti che il kernel userà subito.

L’implementazione attuale mappa per identità il primo 1 GiB usando pagine da 2 MiB e mappa per identità anche le finestre MMIO APIC. Poi mappa range virtuali higher-half per il kernel, gli oggetti dinamici caricati, i blob di boot richiesti, il blob di handoff, il framebuffer e lo stack di bootstrap. Questo è sufficiente perché il kernel inizi nello spazio di indirizzi che si aspetta.

Le page table non sono il sistema finale di memoria virtuale. Sono un ponte che permette al kernel di eseguire, validare l’handoff, installare lo stato CPU iniziale e cominciare a costruire il vero ambiente kernel e user space. Il ponte deve comunque essere corretto: se la pagina di entry del kernel non è mappata, la macchina va in fault immediatamente; se il blob di handoff è mappato all’indirizzo virtuale sbagliato, il kernel legge spazzatura; se manca lo stack, l’ingresso fallisce prima che il codice Rust possa fare molto.


Perché un kernel higher-half?

EriX entra nel kernel nella metà alta dello spazio di indirizzi virtuale. Questo significa che il kernel gira ad alti indirizzi virtuali invece di essere linkato ed eseguito solo nel range basso identity-mapped. È un design comune per kernel perché dà al kernel una regione di indirizzi virtuali stabile, indipendente da dove sia stata allocata la memoria fisica.

Il layout higher-half separa anche lo spazio virtuale del kernel dai range ordinari dello user space e permette al kernel di mantenere presenti le proprie mappature attraverso cambi di address space successivi, mentre le mappature user space possono variare per task. Il layout degli indirizzi non impone da solo tutto il modello di sicurezza, ma supporta il confine rendendo la memoria del kernel distinta dalla normale memoria di processo.

In EriX, il bootloader è responsabile di rendere possibile l’ingresso iniziale higher-half. Carica il kernel secondo gli indirizzi virtuali ELF, mappa quegli indirizzi virtuali su pagine fisiche allocate, crea uno stack di bootstrap nella metà alta, mappa il blob di handoff a un indirizzo higher-half noto, carica cr3 e salta all’entrypoint del kernel.

Al salto, il contratto x86_64 attuale è intenzionalmente piccolo ed esplicito. Il bootloader fornisce solo lo stato di cui il kernel ha bisogno per cominciare a validare l’handoff e installare il proprio stato CPU iniziale:

  • ABI SysV
  • rdi contiene il puntatore di handoff
  • rsp punta allo stack di bootstrap con l’allineamento atteso
  • la paginazione è attiva
  • il controllo non ritorna

Questa ABI è piccola per design. Meno stato implicito serve all’entry del kernel, più facile è auditare il confine.


Entrare nel kernel

Dal lato del kernel, l’ingresso comincia prima che esista il runtime completo del kernel. Il kernel dinamico espone erix_dynlink_entry, che entra nel percorso iniziale del kernel con il puntatore di handoff, e il primo lavoro è difensivo più che carico di policy.

Il kernel disabilita gli interrupt, inizializza l’output seriale iniziale, controlla che il puntatore di handoff non sia nullo, legge la dimensione dell’handoff dall’header, impone una dimensione massima di handoff e poi valida l’intero blob tramite lib-handoff. Dopo la validazione strutturale, il kernel applica i propri controlli di policy:

  • l’architettura deve essere x86_64
  • la piattaforma deve essere UEFI
  • il build ID non deve essere vuoto
  • le tabelle del catalogo dinamico devono essere internamente coerenti
  • nomi, hash, segmenti, dipendenze e range dello store degli oggetti dinamici devono essere validati prima dell’uso

Il bootloader ha già costruito l’handoff, ma il kernel lo valida comunque. I componenti fidati non possono saltare i contratti solo perché un altro componente fidato ha prodotto i dati. Il senso di un handoff versionato è che entrambi i lati possano concordare esattamente su cosa è stato trasferito.

Solo dopo il kernel si muove più a fondo nell’inizializzazione iniziale: installazione della GDT, installazione della IDT, configurazione del percorso syscall, inizializzazione opzionale della console iniziale e infine orchestrazione del bootstrap per il primo task root. Il kernel diventa kernel gradualmente, e la prima cosa che fa è controllare il terreno sotto i piedi.


Il boot è traduzione di autorità

È tentante descrivere il boot come “caricare il kernel e saltare”. È tecnicamente vero, ma manca il punto di design del sistema operativo. Per EriX, il boot è traduzione di autorità: l’autorità del firmware diventa fatti di boot espliciti, i byte dell’immagine di boot diventano sezioni verificate, i file ELF diventano range eseguibili mappati, i blob diventano descrittori di moduli non eseguibili, le mappe di memoria del firmware diventano regioni di memoria normalizzate, i metadati dynamic-link diventano un grafo di oggetti limitato e lo stato del framebuffer diventa metadati di continuità.

Tutto questo diventa un blob di handoff, e il kernel riceve quel blob e decide se è accettabile. Solo allora può cominciare a trasformare le risorse della macchina in oggetti del kernel, capability, address space, endpoint e il primo task user-space. Ecco perché il processo di boot appartiene a una discussione sui sistemi operativi capability-based: il modello a capability non comincia dopo il boot come aggiunta; dipende dal fatto che il boot non introduca autorità ambient.

Il bootloader non dovrebbe dire semplicemente di aver caricato alcune cose. Dovrebbe dire esattamente che cosa ha caricato, dove si trova, che cos’è, come è stato verificato e quali fatti di piattaforma ha osservato. Questa è la differenza tra un salto e un handoff.


Che cosa EriX tiene fuori dal bootloader

Il bootloader è potente perché gira presto, ed è proprio per questo che deve restare piccolo. EriX non vuole che il bootloader decida policy runtime: non dovrebbe decidere quale servizio possiede la policy di memoria, come vengono supervisionati i processi, come vengono gestiti i driver o come vengono composti i filesystem.

Queste decisioni appartengono al kernel e ai servizi di sistema in user space. Il lavoro del bootloader è più stretto: validare l’artefatto di boot, preparare l’ambiente minimo di esecuzione, descrivere ciò che ha fatto e trasferire il controllo.

Questa linea conta per la dimensione del TCB. Un bootloader con più funzionalità non è automaticamente migliore, perché ogni funzionalità nel boot iniziale è codice che gira prima che il kernel possa isolarlo. Ogni parser, percorso di fallback, modalità interattiva ed eccezione di policy aumenta la quantità di comportamento fidato che deve essere corretto prima che il sistema parta.


Guardando avanti

Questo articolo ha trattato boot.img soprattutto come contenitore verificato. Il passo successivo è aprire quel contenitore e guardarne direttamente il design.

Il prossimo articolo spiegherà il formato boot.img di EriX: perché il sistema usa un’immagine unificata, come sono disposte le sezioni, quali metadati vengono trasportati e come il formato supporta artefatti di boot riproducibili e deterministici.