Passa al contenuto principale

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.

Importanti semplificazioni

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 da pop/push per prelevare/spostare valori nella pila, e da call/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, come call, ret e le varie jmp;
  • 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 flag CF (posizione 0), lo zero flag ZF (6), il sign flag SF (7), l'overflow flag OF (11). Sono tipicamente aggiornati dalle istruzioni aritmetiche, e testati indirettamente con istruzioni condizionali come jcon, set e cmov.

Di seguito uno schema funzionale dei registri del processore x86.

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 nn byte viene salvato in memoria a partire dall'indirizzo aa, il byte meno significativo del valore viene salvato in aa, il secondo meno significativo in a+1a+1, e così via fino al più significativo in a+(n1)a+(n-1).

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

  1. allo stack
  2. allo spazio allocato nella sezione .data
  3. alle istruzioni nella sezione .text

Queste sezioni tipicamente non includono gli indirizzi "bassi", cioè a partire da 0x0.

È importante anche tenere presente che

  1. non è possibile eseguire istruzioni dallo stack e da .data
  2. 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.

note

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.