Esempi eseguibili
Tecnicamente, un programma che funziona senza un SO, è un SO. Vediamo quindi come creare ed eseguire alcuni minuscoli sistemi operativi Hello World.
Il codice di tutti gli esempi seguenti è presente in questo repository GitHub .
Settore di avvio
Su x86, la cosa più semplice e di livello più basso che puoi fare è creare un Master Boot Sector (MBR) , che è un tipo di settore di avvio , e quindi installarlo su un disco.
Qui ne creiamo uno con una sola printf
chiamata:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Risultato:
Testato su Ubuntu 18.04, QEMU 2.11.1.
main.img
contiene quanto segue:
\364
in ottale == 0xf4
in esadecimale: la codifica per hlt
un'istruzione, che indica alla CPU di smettere di funzionare.
Pertanto il nostro programma non farà nulla: solo avviare e arrestare.
Usiamo ottale perché i \x
numeri esadecimali non sono specificati da POSIX.
Potremmo ottenere facilmente questa codifica con:
echo hlt > a.asm
nasm -f bin a.asm
hd a
ma la 0xf4
codifica è ovviamente documentata anche nel manuale Intel.
%509s
produrre 509 spazi. Necessario per compilare il file fino al byte 510.
\125\252
in ottale == 0x55
seguito da 0xaa
: byte magici richiesti dall'hardware. Devono essere byte 511 e 512.
Se non presente, l'hardware non lo tratterà come un disco di avvio.
Nota che anche senza fare nulla, alcuni personaggi sono già stampati sullo schermo. Quelli sono stampati dal firmware e servono per identificare il sistema.
Esegui su hardware reale
Gli emulatori sono divertenti, ma l'hardware è il vero affare.
Nota che questo è pericoloso e potresti cancellare il tuo disco per errore: fallo solo su macchine vecchie che non contengono dati critici! O ancora meglio, devboard come il Raspberry Pi, vedi l'esempio ARM di seguito.
Per un tipico laptop, devi fare qualcosa del tipo:
Masterizza l'immagine su una chiavetta USB (distruggerà i tuoi dati!):
sudo dd if=main.img of=/dev/sdX
collegare l'USB su un computer
accendilo
digli di avviarsi dall'USB.
Ciò significa che il firmware deve selezionare USB prima del disco rigido.
Se questo non è il comportamento predefinito del tuo computer, continua a premere Invio, F12, ESC o altre chiavi così strane dopo l'accensione fino a quando non ottieni un menu di avvio in cui è possibile selezionare per l'avvio da USB.
Spesso è possibile configurare l'ordine di ricerca in quei menu.
Ad esempio, sul mio vecchio Lenovo Thinkpad T430, UEFI BIOS 1.16, posso vedere:
Ciao mondo
Ora che abbiamo creato un programma minimo, spostiamoci in un mondo di ciao.
La domanda ovvia è: come fare IO? Alcune opzioni:
- chiedere al firmware, ad esempio BIOS o UEFI, di fare se per noi
- VGA: area di memoria speciale che viene stampata sullo schermo se scritta. Può essere utilizzato in modalità protetta.
- scrivere un driver e parlare direttamente con l'hardware del display. Questo è il modo "corretto" per farlo: più potente, ma più complesso.
porta seriale . Questo è un protocollo standardizzato molto semplice che invia e recupera i caratteri da un terminale host.
Fonte .
Sfortunatamente non è esposto sulla maggior parte dei laptop moderni, ma è il modo comune di utilizzare schede di sviluppo, vedere gli esempi ARM di seguito.
Questo è davvero un peccato, dal momento che tali interfacce sono davvero utili per il debug del kernel Linux, ad esempio .
utilizzare le funzionalità di debug dei chip. ARM chiama ad esempio il loro semihosting . Sull'hardware reale, richiede un supporto hardware e software aggiuntivo, ma sugli emulatori può essere un'opzione conveniente gratuita. Esempio .
Qui faremo un esempio di BIOS in quanto è più semplice su x86. Ma nota che non è il metodo più robusto.
main.S
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
link.ld
SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
Assembla e collega con:
gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
Risultato:
Testato su: Lenovo Thinkpad T430, UEFI BIOS 1.16. Disco generato su un host Ubuntu 18.04.
Oltre alle istruzioni standard di assemblaggio per l'utente, abbiamo:
.code16
: dice a GAS di emettere codice a 16 bit
cli
: disabilita gli interrupt software. Questi potrebbero far ripartire il processore dopo ilhlt
int $0x10
: esegue una chiamata BIOS. Questo è ciò che stampa i personaggi uno per uno.
I flag di link importanti sono:
--oformat binary
: genera codice assembly binario non elaborato, non deformarlo all'interno di un file ELF come nel caso dei normali eseguibili userland.
Usa C invece di assemblare
Poiché C viene compilato per l'assemblaggio, l'utilizzo di C senza la libreria standard è piuttosto semplice, in pratica è sufficiente:
- uno script di linker per mettere le cose in memoria nel posto giusto
- flag che indicano a GCC di non utilizzare la libreria standard
- un piccolo punto di ingresso dell'assieme che imposta lo stato C richiesto per
main
, in particolare:
TODO: collega così alcuni esempi x86 su GitHub. Ecco un ARM che ho creato .
Le cose diventano più divertenti se si desidera utilizzare la libreria standard, poiché non abbiamo il kernel Linux, che implementa gran parte delle funzionalità della libreria C standard tramite POSIX .
Alcune possibilità, senza passare a un sistema operativo completo come Linux, includono:
newlib
Esempio dettagliato su: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
In Newlib, devi implementare tu stesso le syscalls, ma ottieni un sistema molto minimale ed è molto facile implementarle.
Ad esempio, è possibile reindirizzare printf
verso i sistemi UART o ARM o implementare exit()
con semihosting .
sistemi operativi integrati come FreeRTOS e Zephyr .
Tali sistemi operativi in genere consentono di disattivare la pianificazione preventiva, offrendo quindi il pieno controllo sul tempo di esecuzione del programma.
Possono essere visti come una sorta di Newlib pre-implementato.
BRACCIO
In ARM, le idee generali sono le stesse. Ho caricato:
Per il Raspberry Pi, https://github.com/dwelch67/raspberrypi sembra il tutorial più popolare disponibile oggi.
Alcune differenze rispetto a x86 includono:
L'IO viene fatto scrivendo direttamente agli indirizzi magici, non ci sono in
e out
istruzioni.
Questo si chiama IO mappato in memoria .
per alcuni hardware reali, come Raspberry Pi, puoi aggiungere tu stesso il firmware (BIOS) all'immagine del disco.
Questa è una buona cosa, poiché rende l'aggiornamento del firmware più trasparente.
firmware
In verità, il tuo settore di avvio non è il primo software che gira sulla CPU del sistema.
Ciò che effettivamente viene eseguito per primo è il cosiddetto firmware , che è un software:
- prodotto dai produttori di hardware
- tipicamente sorgente chiusa ma probabilmente basato su C.
- memorizzato nella memoria di sola lettura e quindi più difficile / impossibile da modificare senza il consenso del fornitore.
I firmware ben noti includono:
- BIOS : vecchio firmware x86 tutto presente. SeaBIOS è l'implementazione open source predefinita utilizzata da QEMU.
- UEFI : successore del BIOS, meglio standardizzato, ma più capace e incredibilmente gonfio.
- Coreboot : il nobile tentativo open source ad arco incrociato
Il firmware fa cose come:
passa su ogni disco rigido, USB, rete, ecc. fino a quando non trovi qualcosa di avviabile.
Quando eseguiamo QEMU, -hda
dice che main.img
è un disco rigido collegato all'hardware e
hda
è il primo a essere provato e viene utilizzato.
caricare i primi 512 byte nell'indirizzo di memoria RAM 0x7c00
, inserire lì il RIP della CPU e lasciarlo funzionare
mostra cose come il menu di avvio o le chiamate di stampa del BIOS sul display
Il firmware offre funzionalità simili al sistema operativo da cui dipende la maggior parte dei sistemi operativi. Ad esempio, un sottoinsieme Python è stato portato per essere eseguito su BIOS / UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
Si può sostenere che i firmware sono indistinguibili dai sistemi operativi e che il firmware è l'unica "vera" programmazione in metallo nudo che si possa fare.
Come dice questo sviluppatore CoreOS :
La parte difficile
Quando si accende un PC, i chip che compongono il chipset (northbridge, southbridge e SuperIO) non sono ancora inizializzati correttamente. Anche se la ROM del BIOS è la più lontana possibile dalla CPU, ciò è accessibile dalla CPU, perché deve essere, altrimenti la CPU non avrebbe istruzioni da eseguire. Ciò non significa che la ROM del BIOS sia completamente mappata, di solito no. Ma quel tanto che basta è mappato per avviare il processo di avvio. Qualsiasi altro dispositivo, dimenticalo e basta.
Quando esegui Coreboot in QEMU, puoi sperimentare con i livelli più alti di Coreboot e con i payload, ma QEMU offre poche opportunità di sperimentare con il codice di avvio di basso livello. Per prima cosa, la RAM funziona fin dall'inizio.
Stato iniziale post BIOS
Come molte cose nell'hardware, la standardizzazione è debole e una delle cose su cui non dovresti fare affidamento è lo stato iniziale dei registri quando il codice inizia a funzionare dopo il BIOS.
Quindi fatevi un favore e utilizzate un codice di inizializzazione come il seguente: https://stackoverflow.com/a/32509555/895245
I registri piacciono %ds
e %es
hanno effetti collaterali importanti, quindi dovresti azzerarli anche se non li stai usando esplicitamente.
Nota che alcuni emulatori sono più belli dell'hardware reale e ti danno un buono stato iniziale. Quindi, quando corri su hardware reale, tutto si rompe.
GNU GRUB Multiboot
I settori di avvio sono semplici, ma non sono molto convenienti:
- puoi avere un solo SO per disco
- il codice di caricamento deve essere veramente piccolo e contenere 512 byte. Questo potrebbe essere risolto con la chiamata int 0x13 BIOS .
- devi fare molte startup da solo, come passare alla modalità protetta
È per questi motivi che GNU GRUB ha creato un formato file più conveniente chiamato multiboot.
Esempio di lavoro minimo: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Lo uso anche sul mio repository di esempi GitHub per essere in grado di eseguire facilmente tutti gli esempi su hardware reale senza masterizzare l'USB un milione di volte. Su QEMU è simile al seguente:
Se prepari il tuo sistema operativo come file multiboot, GRUB è in grado di trovarlo all'interno di un normale filesystem.
Questo è ciò che fanno la maggior parte delle distro, mettendo sotto le immagini del sistema operativo /boot
.
I file multiboot sono fondamentalmente un file ELF con un'intestazione speciale. Sono specificati da GRUB su: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
È possibile trasformare un file multiboot in un disco di avvio con grub-mkrescue
.
El Torito
Formato che può essere masterizzato su CD: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
È anche possibile produrre un'immagine ibrida che funziona su ISO o USB. Questo può essere fatto con grub-mkrescue
( esempio ), ed è anche fatto dal kernel di Linux make isoimage
usando isohybrid
.
risorse