QEMU: Creare ed Eseguire un Boot-Sector Assembly

boot sector
boot sector

Preso dalla curiosità di ripassare l’assembly (tanti anni fa studiai il Motorola 68000) ho scritto una guida ordinata che illustra passo per passo come scrivere un semplice programma Assembly in real-mode (16 bit), confezionarlo come boot-sector su floppy virtuale, e infine eseguirlo e verificarne il funzionamento tramite QEMU.

Il piccolo programma assembly, descritto nella sezione 1, prende il contenuto di una locazione di memoria e lo moltiplica per due, scrivendo il risultato in un’altra locazione di memoria. Per fare questo semplice task dobbiamo andare sul nudo metallo della macchina, saltando anche il sistema operativo.

Il programma per essere avviato deve venire collocato in una zona di disco detto boot sector: non sarà il sistema operativo a decidere dove caricarlo, ma verrà caricato da questo boot sector in una area di memoria e avviato da un emulatore del BIOS.

Dunque, a dispetto del nome (boot), in realtà non avverrà alcun boot del PC ma soltanto della macchina virtuale implementata da QEMU. Il boot sector in pratica può essere una qualsiasi zona del disco formattata nel modo che vedremo.

1. Il boot-sector

Il boot-sector è il primo settore (512 B) di un dispositivo di avvio (floppy, disco fisso, USB, …) che il BIOS (o nel nostro caso l’emulatore) legge ed esegue automaticamente all’accensione.

Per essere riconosciuto come tale dal BIOS esso deve terminare i suoi 512 byte con la sequenza di byte:

0x55 0xAA.

Questa sequenza viene detta firma di avvio e si trova neigli ultimi due byte del blocco (510 e 511). Quando un blocco termina con questa firma il BIOS sa che può caricare in memoria RAM il contenuto ed esegurilo, senza alcuna intermediazione del sistema operativo! È un po’ quello che succede al vero boot del sistema quando il BIOS interroga in sequenza i dispositivi in cui si può trovare l’avvio, carica in memoria il contenuto (che è il loader del OS) e lo avvia. Solo che qui impariamo che possiamo fare questa cosa anche in modo estemporaneo, col PC già acceso.

Anche nel nostro caso avremo il caricamento in memoria del contenuto del boot sector e l’impostazione dell’Instruction Pointer Register (IP) in modo tale cda far partire l’esecuzione del programma. Più precisamente verranno impostati due registri, CS e IP, avviando così il codice “bare-metal”.

Dunque per avviare un programmino così semplice devo avere anche un boot sector. Perché?

All’avvio, il processore i5 (come qualsiasi x86) entra in uno stato chiamato real‐mode, con il registro CS:IP inizializzato a 0xF000:0xFFF0 — un indirizzo dove risiede il firmware BIOS. Il BIOS, prima ancora del sistema operativo o kernel, cerca sugli storage (disco fisso, floppy, CD…) un “settore di boot” valido, che è un blocco di 512 byte nel quale:

  1. Il BIOS copia quel settore in memoria all’indirizzo fisico 0x0000:0x7C00.
  2. Controlla che gli ultimi due byte siano 0x55 e 0xAA (la “boot signature”).
  3. Imposta con questo indiorizzo (CS:IP ← 0x0000:0x7C00) la coppia di registri Code Segment e Instruction Pointer e così avviene l’avvio del nostro piccolo programma.

Se non trova questa firma, ignora il dispositivo e passa al successivo; non esegue direttamente il nostro codice se non è formattato come boot‐sector.

Perché serve un boot‐sector anche per un “hello world” in Assembly?

  • Nessun OS, nessun loader: stiamo lavorando “bare metal”, senza DOS, Linux o GRUB che carichino ed eseguano un binario. Ignoriamo bellamente la presenza di GRUB e del kernel, ci prendiamo direttamente il processore.
  • BIOS+bootloader = unico meccanismo d’avvio: è il BIOS che governa i primissimi passi di qualsiasi PC x86 e l’unico modo per dirgli “carica ed esegui questo codice” è confezionarlo come boot‐sector. OK? quindi sfruttiamo il meccanismo primitivo di avvio per avviare picccoli programmi di esempio.
  • Attenzione che si potrebbero fare anche danni… Infatti i virus volentieri si installano nei boot sector perché così posssono andare in esecuzione senza alcun controllo!
  • 512 byte bastano per tutto: il nostro programma sta in 512 byte e può iniziare a girare subito, senza filesystem né driver.

Real mode è quella modalità che ci consentirebbe di accedere liberamente a tutta la RAM?

No: è vero che in real mode non hai protezioni né paging, ma hai comunque dei limiti:

Segmentazione a 20 bit

CS, DS, ES, SS, … sono registri a 16 bit usati come basi di segmento.

L’indirizzo fisico poi si calcola così, a partire dai valori scritti dentro a questi registri:

\text{fisico} = \text{registro\_segmento}\times 2^4 + \text{registro\_offset}.\\

Con segmenti e offset a 16 bit ottieni al massimo

\tt{0xFFFF} \times 16 + 0xFFFF = 0x10FFEF \approx 1\,114\,095\;(\approx1.06\text{ MiB}).

In pratica l’indirizzo “reale” arriva fino a poco sopra 1 MiB.

A20 Gate

Per motivi di retrocompatibilità, i primi IBM-PC “chiudevano” il 21° bit, limitando l’accesso esattamente a 1 MiB (0x00000–0xFFFFF).

Disabilitando la “porta A20” si ripristinava esattamente 1 MiB di spazio. Abilitandola (A20=1) si arriva fino a quel ~1.06 MiB di cui sopra.

Nessuna protezione né paging

In real mode il codice gira con livello di privilegio massimo (CPL=0, Current Privilege Level; 0 è il privilegio del BIOS/kernel, il massimo possibile) e non c’è paging: ogni istruzione che calcola un indirizzo fisico ci arriva davvero. Ma il massimo spazio accessibile è quello definito dalla segmentazione (circa 1 MiB), non tutto il DRAM fisico se ne hai di più.

In sintesi

  • Real mode:
    • Accesso “puro” alla memoria fino a ≈1 MiB, senza nessuna protezione o traduzione.
    • Ottimo per il boot e per esercizi di basso livello.
  • Protected mode (o long mode):
    • Introduce descrittori, protezioni, paging e permette di raggiungere tutta la RAM fisica (e virtuale) disponibile.

Quindi in real mode non “giri libero” su tutta la RAM a disposione del porcessore i5 (che ne ha decine di gigabyte), ma giri libero solo sui primi ~1 MiB, senza protezioni. Per accedere a più memoria devi passare a protected (o long) mode.


2. Scrivere il programma Assembly

Ecco il programma che raddoppia un valore cablato nel codice e termina con hlt (halt):

; double.asm — boot-sector real-mode 16 bit
org 0x7C00
bits 16

    mov al, [input]      ; AL ← valore iniziale
    add al, al           ; AL ← 2×AL
    mov [output], al     ; scrivi risultato

    cli                  ; disabilita interrupt
.hang:
    hlt                  ; ferma il processore

input   db 07h           ; dato di partenza = 7
output  db 00h           ; qui finirà 2×7 = 14

; padding + signature
times 510-($-$$) db 0
dw 0xAA55
  • org 0x7C00: indica a NASM che il settore sarà caricato a 0x7C00.
  • bits 16: modalità real-mode.
  • Padding + dw 0xAA55: riempi fino a 510 B, poi scrivi la signature 55 AA in little-endian.

3. Generare il file binario

Con NASM ottengo un file piatto di 512 B contenente codice, padding e signature:

nasm -f bin double.asm -o double.bin

4. Costruire l’immagine floppy

Per simulare un floppy “1,44 MB” (80 tracce × 2 lati × 18 settori di 512 B):

  1. Creazione del file pieno di zeri dd if=/dev/zero of=floppy.img bs=512 count=2880 ‣ Crea floppy.img di 1 474 560 B (= 1,44 MB) pieno di zeri.
  2. Iniezione del boot-sector dd if=double.bin of=floppy.img conv=notrunc ‣ Sovrascrive i primi 512 B di floppy.img con il contenuto di double.bin, mantenendo intatti i restanti settori.

5. Eseguire con QEMU

QEMU simula sia CPU che BIOS (SeaBIOS). Con questo comando monti floppy.img come drive A: e ottieni un prompt di monitor per il debug:

qemu-system-x86_64 \
  -drive file=/percorso/assoluto/floppy.img,format=raw,if=floppy \
  -monitor stdio \
  -serial null \
  -nographic
  • if=floppy: monta il file come controller floppy virtuale.
  • -monitor stdio: fornisce il prompt (qemu) sul terminale.
  • -nographic e -serial null: disabilitano la GUI e la seriale, evitando conflitti su stdin/stdout.

5.1 Avviare il codice

Al prompt (qemu), digita:

c

Il BIOS emulato carica il tuo settore in 0x7C00 e lo esegue fino all’istruzione hlt.

5.2 Verificare registri e memoria

  1. Controlla i registri: (qemu) info registers ‣ EAX conterrà 0x0000000E (AL = 0x0E = 14).
  2. Leggi direttamente i dati in memoria: (qemu) x/1xb 0x7C0A # byte “input” → 0x07 (qemu) x/1xb 0x7C0B # byte “output” → 0x0E

6. Perché questo flusso?

  • Real-mode 16 bit è il punto di partenza di qualsiasi PC x86: accesso semplice ai registri, senza protezioni né paging, limitato a ≈ 1 MiB di RAM.
  • Un boot-sector è l’unica forma di “loader” disponibile prima di un sistema operativo o di un bootloader più complesso (es. GRUB).
  • L’intero processo (NASM → double.binfloppy.img → QEMU) replica fedelmente ciò che avviene su hardware reale, offrendo agli studenti una visione completamente “bare-metal” del funzionamento della macchina.

7. Estensioni possibili

  • Input da tastiera: usare int 0x16 per leggere un carattere e convertirlo da ASCII.
  • Moltiplicazioni avanzate: dimostrare ADD, LEA o IMUL in protected/long mode.
  • Protected mode 32 bit: aggiungere un semplice stub per abilitare PE in CR0 e passare a 32 bit.
  • Architetture diverse: emulatori come EASy68K (Motorola 68000) o Y86 per comprendere microarchitetture semplificate.

Conclusione

Questo esempio illustra come, con pochi comandi e meno di 512 byte di codice, sia possibile toccare con mano il funzionamento di un processore x86 “nudo e crudo”. È un ottimo esercizio didattico per riscoprire le origini del computing e introdurre i concetti fondamentali di boot‐loader, real-mode e interazione BIOS-hardware. Ed è anche un argomento da conoscere per essere consapevoli di come questa modalità sia utilizzata anche per compiere azioni dannose.

Riferimenti

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.