Pur ribadendo che la conoscenza delle caratteristiche dei processori Intel x86 (e compatibili) è da considerare un prerequisito per l'uso di queste dispense, in questo capitolo riassumiamo le loro caratteristiche fondamentali.
Il processore 8086 ha le seguenti peculiarità:
gestisce la memoria in modo segmentato e con ordinamento little endian;
gestisce le periferiche con l'I/O isolato e possiede quindi, nel suo repertorio, istruzioni dedicate al trasferimento dei dati (in e out);
prevede istruzioni a due indirizzi (con qualche eccezione riguardante ad esempio le moltiplicazioni e le divisioni); sono ammesse tutte le combinazioni nei due operandi tra locazioni di memoria, registri e valori immediati eccetto la combinazione «memoria-memoria»
è non ortogonale in quanto i registri sono specializzati.
Essendo la gestione della memoria segmentata, gli indirizzi si indicano nella forma: regseg:offset, cioè: indirizzo di segmento : indirizzo della cella all'interno di quel segmento, ricordando che i valori si esprimono preferibilmente in esadecimale.
Ricordiamo inoltre che la memoria gestita è di 1 MB suddivisi in 65.536 segmenti da 64 KB ciascuno, parzialmente sovrapposti (ognuno inizia 16 locazioni di memoria dopo il precedente); di conseguenza una stessa cella fisica può essere individuata da molti indirizzi segmentati diversi.
Per calcolare l'indirizzo effettivo associato a un indirizzo segmentato, come ad esempio a0e3:1b56, occorre effettuare un semplice calcolo: si moltiplica per 16 l'indirizzo del segmento (a questo scopo basta aggiungere uno zero alla sua destra) e poi si somma con l'offset.
Applicando la regola all'indirizzo prima citato dobbiamo sommare a0e3016 e 1b5616 ottenendo a298616.
Nella CPU 8086 abbiamo i seguenti 14 registri che hanno un'ampiezza di 16 bit:
4 registri di segmento: cs (Segmento Codice o istruzioni), ds (Segmento Dati), ss (Segmento Stack), es (Segmento Extra dati);
4 registri accumulatori o generali: ax, bx, cx, dx, di cui si possono utilizzare anche le rispettive metà, «alta» e «bassa», da 8 bit, identificate con ah, al, bh, bl, ch, cl, dh, dl;
2 registri indice: di e si;
2 registri per la gestione della pila o stack: sp (Stack Pointer) e bp (Base Pointer);
il registro flags o registro di stato;
il registro contatore di programma, ip (Instruction Pointer), che però non viene usato direttamente dal programmatore ma serve a contenere l'indirizzo della prossima istruzione che il processore deve eseguire.
Nel registro di stato solo 9 dei 16 bit sono significativi (vedi figura 1.1) e si dividono in:
flag di stato (bit 0, 2, 4, 6, 7, 11): sono influenzati dai risultati delle operazioni di calcolo aritmetico svolte dalla ALU; anche se esistono istruzioni che ne forzano il cambiamento di valore, vengono soprattutto «consultati» dalle istruzioni di salto condizionato;
flag di controllo (bit 8, 9, 10): permettono di impostare alcune modalità operative della CPU grazie ad apposite istruzioni che impostano il loro valore.
|
Vediamo in dettaglio il ruolo di ogni singolo flag:
flag di stato:
flag di carry (cf): in caso di operazioni su numeri senza segno, vale 1 se la somma precedente ha fornito un riporto (carry) o se la differenza precedente ha fornito un prestito (borrow);
flag di parità (pf): vale 1 se l'operazione precedente ha dato un risultato che ha un numero pari di bit con valore uno;
flag di carry ausiliario (af): opera come il flag cf però relativamente a prestiti e riporti tra il bit 3 e il bit 4 dei valori coinvolti, cioè opera a livello di semibyte (o nibble); è un flag utile soprattutto quando si opera con valori espressi nel codice BCD (Binary Coded Decimal);
flag di zero (zf): vale 1 se l'ultima operazione ha dato risultato zero;
flag di segno (sf): vale 1 se il segno del risultato dell'ultima operazione è negativo; essendo i valori espressi in complemento a due, tale flag viene valorizzato semplicemente tramite la copia del bit più significativo del risultato, che vale appunto 1 per i numeri negativi;
flag di overflow (of): ha la stessa funzione del flag cf ma per numeri con segno; in pratica segnala se l'ultima operazione ha fornito un risultato che esce dall'intervallo dei valori interi rappresentabili nell'aritmetica del processore;
flag di controllo:
flag di trap (tf): viene impostato a 1 per avere l'esecuzione del programma step by step; utile quando si fa il debug dei programmi;
flag di interrupt (if): viene impostato a 1 (istruzione sti) per abilitare le risposte ai segnali di interruzione provenienti dalle periferiche; viene impostato a 0 (istruzione cli) mascherare le interruzioni;
flag di direzione (df): si imposta per stabilire la direzione secondo la quale operano le istruzioni che gestiscono i caratteri di una stringa (1 verso sinistra, o in ordine decrescente di memoria, 0 verso destra, o in ordine crescente di memoria).
Notiamo che, quando si usano i flag del registro di stato, cioè nelle istruzioni di salto condizionato, si fa riferimento solo alla prima lettera del loro nome (ad esempio il flag di zero è identificato solo con z).
Con IA-32 si ha il passaggio a 32 bit dell'architettura dei processori; il capofila della nuova famiglia è stato l'80386, che ha portato molte novità riguardanti i registri e la gestione della memoria, pur mantenendo inalterate alcune caratteristiche (ad esempio la non ortogonalità, la gestione delle periferiche):
i registri aumentano di numero e aumentano quasi tutti la lunghezza a 32 bit; rimangono a 16 bit i registri di segmento che assumono il nome di registri selettori e ai quali si aggiungono due ulteriori registri per segmenti extra: fs e gs;
i registri accumulatori, il registro di stato e quelli usati come offset diventano tutti a 32 bit e il loro nome cambia con l'aggiunta di una «e» iniziale; abbiamo quindi: eip, esp, ebp, esi, edi, eflags, eax, ebx, ecx, edx;
i registri accumulatori rimangono utilizzabili anche in modo parziale: abbiamo infatti eax, ebx, ecx, edx a 32 bit ma si possono ancora usare ax, bx, cx, dx per usufruire delle rispettive loro parti basse di 16 bit, a loro volta divisibili nelle sezioni a 8, bit come in precedenza.
La successiva evoluzione verso i 64 bit, con l'architettura x86-64, ha portato l'ampiezza dei registri, appunto, a 64 bit con le seguenti caratteristiche:
i nomi dei registri sono gli stessi di prima con una «r» al posto della «e»;
ci sono ulteriori otto «registri estesi» indicati con le sigle da r8 a r15;
rimane la possibilità dell'uso parziale (porzioni di 8, 16, 32 bit, ad esempio al o ah, ax, eax nel caso di rax) dei registri accumulatori e dei registri indice;
per questi ultimi diviene possibile usare anche il solo byte meno significativo, (ad esempio per rsp si può usare anche spl oltre a esp e sp) cosa non prevista nelle CPU x86.
Riguardo alla memoria rimane la possibilità di gestirne 1 MB in modo segmentato come nell'8086 (si dice allora che si lavora in modalità reale); la novità è però la modalità protetta nella quale si gestiscono 4 GB di memoria con indirizzi nella forma selettore:offset chiamati indirizzi virtuali.
Questo nome dipende dal fatto che non è detto che tali indirizzi corrispondano a reali indirizzi fisici di memoria (solo da poco tempo i comuni Personal Computer hanno una dotazione di memoria centrale di qualche GB); i processori sono infatti in grado di gestire la memoria virtuale con un meccanismo detto di paginazione dinamica sfruttato poi in modo opportuno anche dai moderni sistemi operativi.
Questo argomento richiederebbe notevoli approfondimenti che però esulano dagli scopi di queste dispense.
Concludiamo citando la presenza della tabella dei descrittori, alle cui righe si riferiscono i registri selettori, che contiene indirizzi iniziali e dimensioni dei vari segmenti; questi ultimi hanno infatti dimensione variabile (massimo 4 GB).
Di ogni selettore solo 13 dei 16 bit sono usati per individuare una riga della tabella; i segmenti sono quindi al massimo 8192.
Gli indirizzi virtuali nella forma selettore:offset con offset a 32 bit, vengono tradotti in indirizzi lineari a 32 bit che, come detto, non è detto che corrispondano direttamente a indirizzi fisici di memoria.
Lo spazio virtuale effettivamente a disposizione di ogni processo è comunque di soli 3 GB perché la zona più alta, ampia 1 GB, è uno spazio comune a tutti i processi in esecuzione e riservata al kernel di Linux.