Architettura x86
Riportiamo qui una vista semplificata e riassuntiva dell'architettura x86 per la quale scriveremo programmi assembler.
L'architettura x86 è a 32 bit. Questo implica che i registri generali, così come tutti gli indirizzi per locazioni in memoria, sono a 32 bit. L'evoluzione di questa architettura, x64 a 64 bit, che è quella che troviamo nei processori in commercio, è del tutto retrocompatibile.
La visione del processore che proponiamo è molto limitata, ed omette diversi importanti registri, flag e funzionalità che saranno esplorati in corsi successivi.
Questi includono, per esempio, il registro ebp
, la natura dei meccanismi di protezione, il significato di SEGMENTATION FAULT
, e che cosa sia un kernel.
Quanto discutiamo è tuttavia sufficiente agli scopi didattici di questo corso.
Registri
I registri che utilizzeremo direttamente sono 6: eax
, ebx
, ecx
, edx
, esi
, edi
.
Per i primi quattro di questi, è possibile operare sulle loro porzioni a 16 e 8 bit tramite ax
, ah
, al
e così via.
Per i registri esi
ed edi
è possibile operare solo sulle porzioni a 16 bit, tramite si
e di
.
Tipicamente, i registri eax
...edx
sono utilizzati per processare dati, mentre esi
ed edi
sono utilizzati come registri puntatori.
Questa divisione di utilizzo non è però affatto obbligatoria per la maggior parte delle istruzioni.
Altri registri sono invece utilizzati in modo indiretto:
esp
è il registro puntatore per la cima dello stack, viene utilizzato dapop
/push
per prelevare/spostare valori nella pila, e dacall
/ret
per la chiamata di sottoprogrammi;eip
è il registro puntatore verso la prossima istruzione da eseguire, viene incrementato alla fine del fetch di una istruzione e modificato da istruzioni che cambiano il flusso d'esecuzione, comecall
,ret
e le variejmp
;eflags
è il registro dei flag, una serie di booleani con informazioni sullo stato dell'esecuzione e sul risultato dell'ultima operazione aritmetica. I flag di nostro interesse sono il carry flagCF
(posizione 0), lo zero flagZF
(6), il sign flagSF
(7), l'overflow flagOF
(11). Sono tipicamente aggiornati dalle istruzioni aritmetiche, e testati indirettamente con istruzioni condizionali comejcon
,set
ecmov
.
Di seguito uno schema funzionale dei registri del processore x86.
![](/reti-logiche-esercitazioni/img/assembler/architettura/registri-h.drawio.png)
Memoria
Lo spazio di memoria dell'architettura x86 è indirizzato su 32 bit. Ciascun indirizzo corrisponde a un byte, ma è possibile eseguire anche letture e scritture a 16 e 32 bit.
Per tali casi è importante ricordare che l'architettura x86 è little-endian, che significa little end first, un riferimento a I viaggi di Gulliver. Questo si traduce nel fatto che quando un valore di byte viene salvato in memoria a partire dall'indirizzo , il byte meno significativo del valore viene salvato in , il secondo meno significativo in , e così via fino al più significativo in .
Questo ordinamento dei byte in memoria non inficia sulla coerenza dei dati nei registri: eseguendo movl %eax, a
e movl a, %eax
il contenuto di eax
non cambia, e l'ordinamento dei bit rimane coerente.
I meccanismi di protezione ci precludono l'accesso alla maggior parte dello spazio di memoria. Potremmo accedere senza incorrere in errori solo
- allo stack
- allo spazio allocato nella sezione
.data
- alle istruzioni nella sezione
.text
Queste sezioni tipicamente non includono gli indirizzi "bassi", cioè a partire da 0x0
.
È importante anche tenere presente che
- non è possibile eseguire istruzioni dallo stack e da
.data
- non è possibile scrivere nella sezione
.text
Vanno quindi opportunamente dichiarate le sezioni, e vanno evitate operazioni di jmp
, call
etc. verso locazioni di .data
così come le mov
verso locazioni di .text
.
In caso di violazione di questi meccanismi, l'errore più tipico è SEGMENTATION FAULT
.
Spazio di I/O
Lo spazio di I/O, sia quello fisico (monitor, speaker, tastiera, etc.) sia quello virtuale (terminale, files su disco, etc.) ci è in realtà precluso tramite meccanismi di protezione.
Tentare di eseguire istruzioni in
o out
porterà infatti al brusco arresto del programma.
Il nostro programma può interagire con lo spazio di I/O solo tramite il kernel del sistema operativo.
Tutta questa complessità è astratta tramite i sottoprogrammi di input/output dell'ambiente, documentati qui.
Condizioni al reset
Il reset iniziale e l'avvio del nostro programma sono concetti completamente diversi e scollegati.
Non possiamo sfruttare nessuna ipotesi sullo stato dei registri al momento dell'avvio del nostro programma, se non che il registro eip
punterà ad un certo punto alla prima istruzione di _main
.
Il fatto che _main
sia l'entrypoint del nostro programma, così come l'uso di ret
senza alcun valore di ritorno, è una caratteristica di questo ambiente.