Easyboot supporta vari kernel tramite plugin. Ma se non viene trovato alcun plugin adatto, ricorre ai binari ELF64 o PE32+ con una variante semplificata (non c'è bisogno di incorporare nulla) del protocollo Multiboot2.
Questo è lo stesso protocollo utilizzato da Simpleboot; tutti i kernel di esempio in quel repository devono funzionare anche con Easyboot.
Puoi usare l'intestazione multiboot2.h originale nel repository di GRUB, o il file di intestazione C/C++ easyboot.h per ottenere typedef più facili da usare. Il formato binario di basso livello è lo stesso, puoi anche usare qualsiasi libreria Multiboot2 esistente, anche con linguaggi diversi dal C, come questa libreria Rust ad esempio (nota: non sono in alcun modo affiliato a quegli sviluppatori, ho solo cercato "Rust Multiboot2" e quello è stato il primo risultato).
[[TOC]]
Nelle macchine BIOS, il primo settore del disco viene caricato su 0:0x7C00 dal firmware e il controllo gli viene passato. In questo settore Easyboot ha boot_x86.asm, che è abbastanza intelligente da individuare e caricare il caricatore di 2° stadio e anche da impostare la modalità lunga per esso.
Sulle macchine UEFI lo stesso file di 2° stadio, chiamato EFI/BOOT/BOOTX64.EFI
, viene caricato direttamente dal firmware. La
fonte per questo loader può essere trovata in loader_x86.c. Ecco fatto, Easyboot non è GRUB né
syslinux, entrambi richiedono decine e decine di file di sistema sul disco. Qui non servono altri file, solo questo (i plugin sono
opzionali, nessuno è necessario per fornire la compatibilità Multiboot2).
Su Raspberry Pi il caricatore si chiama KERNEL8.IMG
, compilato da loader_rpi.c.
Questo loader è scritto con molta attenzione per funzionare su più configurazioni. Carica la GUID Partitining Table dal disco e
cerca una "EFI System Partition". Quando la trova, cerca il file di configurazione easyboot/menu.cfg
su quella partizione di
avvio. Dopo aver selezionato l'opzione di avvio e aver conosciuto il nome del file del kernel, il loader lo individua e lo carica.
Quindi rileva automaticamente il formato del kernel ed è abbastanza intelligente da interpretare le informazioni di sezione e segmento su dove caricare cosa (esegue il mapping della memoria su richiesta ogni volta che è necessario). Quindi imposta un ambiente appropriato a seconda del protocollo di avvio rilevato (Multiboot2 / Linux / ecc. modalità protetta o lunga, argomenti ABI ecc.). Dopo che lo stato della macchina è solido e ben definito, come ultimo atto, il loader salta al punto di ingresso del kernel.
Tutto ciò che è scritto nella specifica Multiboot2 sullo stato della macchina è valido, fatta eccezione per i registri di uso generale. Easyboot passa due argomenti al punto di ingresso del kernel in base alla SysV ABI e alla Microsoft fastcall ABI. Il primo parametro è la magia, il secondo è un indirizzo di memoria fisica, che punta a un taglist Multiboot Information (abbreviato in MBI in seguito, vedi sotto).
Inoltre, violiamo un po' il protocollo Multiboot2 per gestire i kernel della metà superiore. Multiboot2 impone che la memoria debba essere mappata in base all'identità. Bene, in Easyboot questo è vero solo in parte: garantiamo solo che tutta la RAM fisica sia sicuramente mappata in base all'identità come previsto; tuttavia, alcune regioni superiori (a seconda delle intestazioni di programma del kernel) potrebbero essere ancora disponibili. Ciò non interrompe i normali kernel conformi a Multiboot2, che non dovrebbero accedere alla memoria al di fuori della RAM fisica disponibile.
Il tuo kernel viene caricato esattamente nello stesso modo sia sui sistemi BIOS che UEFI e anche su RPi, le differenze di firmware sono solo "problemi di qualcun altro". L'unica cosa che il tuo kernel vedrà è se l'MBI contiene o meno il tag della tabella di sistema EFI. Per semplificarti la vita, Easyboot non genera nemmeno il tag della mappa di memoria EFI (tipo 17), fornisce solo il tag Mappa di memoria (tipo 6) indiscriminatamente su tutte le piattaforme (anche sui sistemi UEFI, lì la mappa di memoria viene semplicemente convertita per te, quindi il tuo kernel deve gestire solo un tipo di tag di elenco di memoria). Anche i tag vecchi e obsoleti vengono omessi e non vengono mai generati da questo boot manager.
Il kernel viene eseguito a livello di supervisore (ring 0 su x86, EL1 su ARM), possibilmente su tutti i core della CPU in parallelo.
GDT non specificato, ma valido. Lo stack è impostato nei primi 640k e cresce verso il basso (ma dovresti cambiarlo il prima possibile con qualsiasi stack tu ritenga degno). Quando SMP è abilitato, tutti i core hanno i propri stack e l'ID del core è in cima allo stack (ma puoi anche ottenere l'ID del core nel solito modo specifico della piattaforma, usando cpuid / mpidr / ecc.).
Dovresti considerare IDT come non specificato; IRQ, NMI e interrupt software disabilitati. I gestori di eccezioni fittizi sono impostati per visualizzare un dump minimo e arrestare la macchina. Ci si dovrebbe basare su di essi solo per segnalare se il kernel va in tilt prima di essere in grado di impostare il proprio IDT e i gestori, preferibilmente il prima possibile. Su ARM vbar_el1 è impostato per chiamare gli stessi gestori di eccezioni fittizi (anche se scaricano registri diversi, ovviamente).
Anche framebuffer è impostato di default. Puoi modificare la risoluzione in config, ma se non viene specificato, framebuffer è comunque configurato.
È importante non tornare mai dal kernel. Sei libero di sovrascrivere qualsiasi parte del loader in memoria (non appena hai finito con i tag MBI), quindi non c'è semplicemente nessun posto a cui tornare. "Der Mohr hat seine Schuldigkeit getan, der Mohr kann gehen."
Non è ovvio a prima vista, ma la specifica Multiboot2 definisce in realtà due set di tag totalmente indipendenti:
Il primo set dovrebbe essere inline in un kernel compatibile con Multiboot2, chiamato header Multiboot2 dell'immagine del sistema operativo (sezione 3.1.2), quindi fornito dal kernel. Easyboot non si preoccupa di questi tag e non analizza il tuo kernel per questi. Semplicemente non hai bisogno di dati magici speciali incorporati nel tuo file kernel con Easyboot, gli header ELF e PE sono sufficienti.
Il secondo set viene passato al kernel dinamicamente all'avvio, Easyboot usa solo questi tag. Tuttavia, non genera tutto ciò che Multiboot2 specifica (omette semplicemente quelli vecchi, obsoleti o legacy). Questi tag sono chiamati tag MBI, vedere Boot information (sezione 3.6).
NOTA: la specifica Multiboot2 sui tag MBI è piena di bug. Di seguito puoi trovare una versione corretta, che si allinea con il file header multiboot2.h che puoi trovare nel repository sorgente di GRUB.
Il primo parametro del kernel è il magico 0x36d76289 (in rax
, rcx
e rdi
). Puoi individuare i tag MBI usando il secondo
parametro (in rbx
, rdx
e rsi
). Sulla piattaforma ARM, il magico è in x0
e l'indirizzo è in x1
. Su RISC-V e MIPS vengono
usati rispettivamente a0
e a1
. Se e quando questo loader viene portato su un'altra architettura, devono sempre essere usati i
registri specificati da SysV ABI per gli argomenti della funzione. Se ci sono altre ABI comuni sulla piattaforma che non
interferiscono con SysV ABI, allora i valori devono essere duplicati anche nei registri di quelle ABI (o in cima allo stack).
L'indirizzo passato è sempre allineato a 8 byte e inizia con un'intestazione MBI:
+-------------------+
u32 | total_size |
u32 | reserved |
+-------------------+
Segue una serie di tag allineati anch'essi da 8 byte. Ogni tag inizia con i seguenti campi di intestazione tag:
+-------------------+
u32 | type |
u32 | size |
+-------------------+
type
contiene un identificatore del contenuto del resto del tag. size
contiene la dimensione del tag, inclusi i campi di
intestazione, ma escluso il padding. I tag si susseguono l'uno con l'altro, con padding quando necessario, in modo che ogni tag
inizi con un indirizzo allineato di 8 byte.
+-------------------+
u32 | type = 0 |
u32 | size = 8 |
+-------------------+
I tag terminano con un tag di tipo 0
e dimensione 8
.
+-------------------+
u32 | type = 1 |
u32 | size |
u8[n] | string |
+-------------------+
string
contiene la riga di comando specificata nella riga kernel
di menuentry (senza il percorso e il nome del file del
kernel). La riga di comando è una normale stringa UTF-8 terminata da zero in stile C.
+----------------------+
u32 | type = 2 |
u32 | size = 17 |
u8[n] | string "Easyboot" |
+----------------------+
string
contiene il nome di un boot loader che avvia il kernel. Il nome è una normale stringa UTF-8 terminata da zero in stile C.
+-------------------+
u32 | type = 3 |
u32 | size |
u32 | mod_start |
u32 | mod_end |
u8[n] | string |
+-------------------+
Questo tag indica al kernel quale modulo di avvio è stato caricato insieme all'immagine del kernel e dove può essere trovato.
mod_start
e mod_end
contengono gli indirizzi fisici di inizio e fine del modulo di avvio stesso. Non otterrai mai un buffer
compresso con gzip, perché Easyboot li decomprime in modo trasparente per te (e se fornisci un plugin, funziona anche con dati
compressi diversi da gzip). Il campo string
fornisce una stringa arbitraria da associare a quel particolare modulo di avvio; è
una normale stringa UTF-8 terminata da zero in stile C. Specificata nella riga module
di menuentry e il suo utilizzo esatto è
specifico del sistema operativo. A differenza del tag della riga di comando di avvio, i tag del modulo includono anche il
percorso e il nome file del modulo.
Appare un tag per modulo. Questo tipo di tag può apparire più volte. Se un ramdisk iniziale è stato caricato insieme al kernel, allora apparirà come primo modulo.
C'è un caso speciale: se il file è una tabella DSDT ACPI, un blob FDT (dtb) o GUDT, allora non apparirà come un modulo, ma piuttosto il ACPI vecchio RSDP (tipo 14) o il ACPI nuovo RSDP (tipo 15) verranno patchati e il loro DSDT sostituito con il contenuto di questo file.
Questo tag fornisce una mappa della memoria.
+-------------------+
u32 | type = 6 |
u32 | size |
u32 | entry_size = 24 |
u32 | entry_version = 0 |
varies | entries |
+-------------------+
size
contiene la dimensione di tutte le voci, incluso questo campo stesso. entry_size
è sempre 24. entry_version
è impostato
su 0
. Ogni voce ha la seguente struttura:
+-------------------+
u64 | base_addr |
u64 | length |
u32 | type |
u32 | reserved |
+-------------------+
base_addr
è l'indirizzo fisico iniziale. length
è la dimensione della regione di memoria in byte. type
è la varietà
dell'intervallo di indirizzi rappresentato, dove il valore 1
indica RAM disponibile, il valore 3
indica memoria utilizzabile
contenente informazioni ACPI, il valore 4
indica memoria riservata che deve essere preservata durante la sospensione, il valore
5
indica una memoria occupata da moduli RAM difettosi e tutti gli altri valori indicano attualmente un'area riservata. reserved
è impostato su 0
all'avvio del BIOS.
Quando l'MBI viene generato su una macchina UEFI, varie voci della mappa di memoria EFI vengono memorizzate come tipo 1
(RAM
disponibile) o 2
(RAM riservata) e, se necessario, il tipo di memoria EFI originale viene inserito nel campo reserved
.
La mappa fornita garantisce di elencare tutta la RAM standard che dovrebbe essere disponibile per un uso normale, ed è sempre
ordinata in base a base_addr
crescente. Questo tipo di RAM disponibile include tuttavia le regioni occupate da kernel, mbi,
segmenti e moduli. Il kernel deve fare attenzione a non sovrascrivere queste regioni (Easyboot potrebbe facilmente escludere
tali regioni, ma ciò interromperebbe la compatibilità Multiboot2).
+----------------------------------+
u32 | type = 8 |
u32 | size = 38 |
u64 | framebuffer_addr |
u32 | framebuffer_pitch |
u32 | framebuffer_width |
u32 | framebuffer_height |
u8 | framebuffer_bpp |
u8 | framebuffer_type = 1 |
u16 | reserved |
u8 | framebuffer_red_field_position |
u8 | framebuffer_red_mask_size |
u8 | framebuffer_green_field_position |
u8 | framebuffer_green_mask_size |
u8 | framebuffer_blue_field_position |
u8 | framebuffer_blue_mask_size |
+----------------------------------+
Il campo framebuffer_addr
contiene l'indirizzo fisico del framebuffer. Il campo framebuffer_pitch
contiene la lunghezza di una
riga in byte. I campi framebuffer_width
, framebuffer_height
contengono le dimensioni del framebuffer in pixel. Il campo
framebuffer_bpp
contiene il numero di bit per pixel. framebuffer_type
è sempre impostato su 1 e reserved
contiene sempre 0
nella versione corrente della specifica e deve essere ignorato dall'immagine del sistema operativo. I campi rimanenti descrivono
il formato pixel compresso, la posizione e la dimensione dei canali in bit. Puoi usare l'espressione
((~(0xffffffff << size)) << position) & 0xffffffff
per ottenere una maschera di canale simile a UEFI GOP.
Questo tag esiste solo se Easyboot è in esecuzione su una macchina UEFI. Su una macchina BIOS questo tag non è mai stato generato.
+-------------------+
u32 | type = 12 |
u32 | size = 16 |
u64 | pointer |
+-------------------+
Questo tag contiene un puntatore a EFI system table.
Questo tag esiste solo se Easyboot è in esecuzione su una macchina UEFI. Su una macchina BIOS questo tag non è mai stato generato.
+-------------------+
u32 | type = 20 |
u32 | size = 16 |
u64 | pointer |
+-------------------+
Questo tag contiene un puntatore a EFI image handle. Di solito si tratta dell'handle dell'immagine del boot loader.
+-------------------+
u32 | type = 13 |
u32 | size |
u8 | major |
u8 | minor |
u8[6] | reserved |
| smbios tabelle |
+-------------------+
Questo tag contiene una copia delle tabelle SMBIOS e la loro versione.
+-------------------+
u32 | type = 14 |
u32 | size |
| copia di RSDPv1 |
+-------------------+
Questo tag contiene una copia di RSDP come definito dalla specifica ACPI 1.0. (Con un indirizzo a 32 bit.)
+-------------------+
u32 | type = 15 |
u32 | size |
| copia di RSDPv2 |
+-------------------+
Questo tag contiene una copia di RSDP come definito dalle specifiche ACPI 2.0 o successive. (Probabilmente con un indirizzo a 64 bit.)
Questi (tipo 14 e 15) puntano a una tabella RSDT
o XSDT
con un puntatore a una tabella FACP
, che a sua volta contiene due
puntatori a una tabella DSDT
, che descrive la macchina. Easyboot falsifica queste tabelle su macchine che altrimenti non
supportano ACPI. Inoltre, se fornisci una tabella DSDT, un blob FDT (dtb) o GUDT come modulo, Easyboot applicherà una patch ai
puntatori per puntare a quella tabella fornita dall'utente. Per analizzare queste tabelle, puoi usare la mia libreria
hwdet a intestazione singola e senza dipendenze (o le gonfie
apcica e libfdt).
I tag con type
maggiore o uguale a 256 non fanno parte della specifica Multiboot2, ma sono comunque forniti da Easyboot.
Questi potrebbero essere aggiunti tramite plugins opzionali all'elenco, se e quando un kernel ne ha bisogno.
+-------------------+
u32 | type = 256 |
u32 | size |
| copia di EDID |
+-------------------+
Questo tag contiene una copia dell'elenco delle risoluzioni dei monitor supportate in base alle specifiche EDID.
+-------------------+
u32 | type = 257 |
u32 | size |
u32 | numcores |
u32 | running |
u32 | bspid |
+-------------------+
Questo tag esiste se è stata data la direttiva multicore
. numcores
contiene il numero di core CPU nel sistema, running
è il
numero di core che hanno inizializzato con successo e che eseguono lo stesso kernel in parallelo. bspid
contiene l'identificativo
del core BSP (su ID lAPIC x86), in modo che i kernel possano distinguere gli AP ed eseguire un codice diverso su di essi. Tutti gli
AP hanno il loro stack e in cima allo stack ci sarà l'ID del core corrente.
+-------------------+
u32 | type = 258 |
u32 | size = 24 / 40 |
u128 | bootuuid |
u128 | rootuuid |
+-------------------+
Questo tag contiene i campi identificativi univoci nel GPT delle partizioni di avvio e root. Se l'avvio non utilizza una tabella di
partizionamento GUID, bootuuid
viene generato come 54524150-(codice dispositivo)-(numero partizione)-616F6F7400000000
.
Inizio | Fine | Descrizione |
---|---|---|
0x0 | 0x400 | Tabella dei vettori di interruzione (utilizzabile, modalità reale IDT) |
0x400 | 0x4FF | Area dati BIOS (utilizzabile) |
0x4FF | 0x500 | Codice dell'unità del BIOS (molto probabilmente 0x80, utilizzabile) |
0x500 | 0x5A0 | dati di sincronizzazione per SMP (utilizzabili) |
0x5A0 | 0x1000 | stack del gestore delle eccezioni (utilizzabile dopo aver impostato l'IDT) |
0x1000 | 0x8000 | tabelle di paging (utilizzabili dopo aver impostato le tabelle di paging) |
0x8000 | 0x20000 | codice e dati del caricatore (utilizzabili dopo aver impostato l'IDT) |
0x20000 | 0x40000 | config + tag (utilizzabili dopo l'analisi di MBI) |
0x40000 | 0x90000 | ID dei plugin; dall'alto verso il basso: stack del kernel |
0x90000 | 0x9A000 | Solo kernel Linux: zero page + cmdline |
0x9A000 | 0xA0000 | Area dati BIOS estesa (meglio non toccare) |
0xA0000 | 0xFFFFF | VRAM e BIOS ROM (non utilizzabili) |
0x100000 | x | segmenti del kernel, seguiti dai moduli, ogni pagina allineata |
Nessuno lo sa. UEFI alloca la memoria come vuole. Aspettatevi di tutto e di più. Tutte le aree saranno sicuramente elencate nella
mappa di memoria come tipo = 1 (MULTIBOOT_MEMORY_AVAILABLE
) e riservate = 2 (EfiLoaderData
), tuttavia questo non è esclusivo,
anche altri tipi di memoria potrebbero essere elencati in questo modo (la sezione bss del boot manager, ad esempio).
Inizio | Fine | Descrizione |
---|---|---|
0x0 | 0x500 | riservato dal firmware (meglio non toccare) |
0x500 | 0x5A0 | dati di sincronizzazione per SMP (utilizzabili) |
0x5A0 | 0x1000 | stack del gestore delle eccezioni (utilizzabile dopo aver impostato l'VBAR) |
0x1000 | 0x9000 | tabelle di paging (utilizzabili dopo aver impostato le tabelle di paging) |
0x9000 | 0x20000 | codice e dati del caricatore (utilizzabili dopo aver impostato l'VBAR) |
0x20000 | 0x40000 | config + tag (utilizzabili dopo l'analisi di MBI) |
0x40000 | 0x80000 | firmware fornito FDT (dtb); dall'alto verso il basso: stack del kernel |
0x100000 | x | segmenti del kernel, seguiti dai moduli, ogni pagina allineata |
I primi byte sono riservati per armstub. Solo il core 0 è
partito, quindi per avviare Application Processors, scrivi l'indirizzo di una funzione su 0xE0 (core 1), 0xE8 (core 2), 0xF0 (core 3),
i cui indirizzi si trovano in quest'area. Questo è irrilevante quando viene usata la direttiva multicore
, quindi tutti i core
eseguiranno il kernel.
Sebbene non supportato nativamente su RPi, si ottiene comunque un vecchio tag RSDP ACPI (tipo 14), con tabelle false. La tabella
APIC
viene utilizzata per comunicare al kernel il numero di core CPU disponibili. L'indirizzo della funzione di avvio è memorizzato
nel campo RSD PTR-> RSDT -> APIC -> cpu[x].apic_id (e core id in cpu[x].acpi_id, dove BSP è sempre cpu[0].acpi_id = 0 e
cpu[0].apic_id = 0xD8. Attenzione, "acpi" e "apic" sembrano piuttosto simili).
Se il firmware passa un blob FDT valido, o se uno dei moduli è un file .dtb, .gud o .aml, viene aggiunta anche una tabella FADT (con
FACP
magico). In questa tabella, il puntatore DSDT (32 bit, all'offset 40) punta al blob dell'albero dei dispositivi appiattito
fornito.
Sebbene il firmware non fornisca alcuna funzionalità di mappatura della memoria, otterrai comunque un tag di mappa di memoria (tipo 6), che elenca la RAM rilevata e la regione MMIO. Puoi usarlo per rilevare l'indirizzo di base dell'MMIO, che è diverso su RPi3 e RPi4.