![]() |
(Incompleto) MINI CORSO DI ASSEMBLER 8086 |
![]() |
Per
descrivere funzionalmente un Computer di solito si ricorre al modello di Von
Neumann, secondo il quale un apparato elaboratore è costituito da 3 parti:
- l'unità di calcolo vera e propria (il microprocessore).
- la memoria.
- i dispositivi di ingresso e di uscita.
Nel microprocessore (o
il processore o CPU [Central Processing Unit]) avviene tutta l'elaborazione del
calcolatore.
La memoria è indispensabile in un computer perché contiene il programma
(in linguaggio macchina) che il processore è chiamato ad eseguire. Inoltre la
memoria si presta a conservare i dati eventualmente elaborati dal processore,
almeno fino a quando si riterrà opportuno salvarli in qualche punto più sicuro.
I dispositivi di input/output consentono la visualizzazione dei risultati e l'acquisizione dei dati relativi al calcolo che si intende svolgere.
Le 3 componenti della macchina
di Von Neumann devono essere collegate tra loro.
Questa struttura di collegamento è nota con il nome di bus di sistema. Si tratta in
sostanza di gruppi di fili, di solito in numero piuttosto elevato, ciascuno dei
quali può assumere uno dei 2 livelli logici possibili: "1" (presenza di
tensione) o "0" (assenza di tensione elettrica).
In questo modo a ciascun filo è affidata un'informazione elementare (bit) che
assume una certa importanza, rapportata alla quantità dei fili stessi.
Un bus a 16 bit è formato da 16 fili.
Un computer possiede 3 tipi di bus:
- il bus dati
- il bus degli indirizzi
- il bus di controllo
Tutti e 3 i bus hanno origine dentro il microprocessore e il loro fili vengono
poi riproposti anche al di fuori, sotto forma di minuscole piste, stampate sulla
scheda madre, al fine di raggiungere ogni oggetto che ne ha bisogno.
Il Bus Dati
Il Bus Dati è la
struttura di scambio principale di un computer; è così importante che è
diventato uno strumento per distinguerli tra di loro.
Il numero di linee (bit) del bus dati viene utilizzato per qualificare il
processore a cui appartiene, per cui è frequente parlare di processore a 32 bit
o di CPU a 64 bit, intendendo implicitamente la dimensione del suo bus dati.
Più grande è il numero di linee del bus
dati maggiore è la quantità d'informazione che è possibile scambiare in un colpo
solo e quindi la CPU e il computer che la utilizzano sono più veloci.
Basta pensare al tempo necessario a prelevare 8 bytes (64 bit) tutti insieme,
piuttosto che uno alla volta.
Il disegno seguente mostra un tipico bus dati:
Possiamo notare che in tutti e 4 i rami del Bus sono presenti delle frecce e un
numero. Dalla Cpu le frecce entrano ed escono: la cosa non sorprende perché,
nell'ambito di un computer, chi decide letture e scritture è proprio la Cpu.
Il senso delle frecce precisa che la memoria è un dispositivo in grado di essere
letto o scritto, mentre alcuni dispositivi di I/O potranno essere solo scritti
(quelli di output, come il monitor) o solo letti (quelli di input come la
tastiera). In realtà esistono dispositivi di I/O che possono essere sia
letti che scritti (come gli Hard Disk).
Il numero indicato dentro la traccia rettangolare (che d'ora in poi chiameremo bus...) è la quantità di piste presenti nel bus: nel nostro caso 16 che è tipico del bus dati di un 8086.
Concludendo, il bus dati
è una struttura bidirezionale gestita esclusivamente dal processore. Poiché il
flusso può avere un solo verso in un determinato istante segue che non è
possibile che l'informazione
possa entrare ed uscire contemporaneamente: nasce quindi la necessità
di un meccanismo di sincronizzazione.
Un processore non funzionare senza la presenza di un cadenzatore d'eventi, che
stimoli e orienti le sue risorse in operazioni organizzate e successive, una
all'altra.
Questo dispositivo è il generatore di clock (spesso detto orologio di
sistema) che, insieme alla
dimensione del bus dati, diventa un altro importante metro per misurare la
potenza globale di un computer.
Il Bus Indirizzi
In ogni computer il
bus dati è
dunque indispensabile al processore per inviare i comandi e le richieste ai
suoi fedeli sudditi esterni, e per riceverne l'eventuale risposta.
Ma siccome la CPU non ha occhi, ne bocca, ne dita ha la necessità di avere uno strumento che le consenta di
individuare correttamente le periferiche e la memoria con le quali deve
interagire e questo senza pericolo di confusione.
La lingua dei processori rimane ineluttabilmente il binario
per cui i nomi delle
periferiche e delle locazioni di memoria saranno sequenze di 0 e 1
(come per esempio: 0000 0011 1010 0110). Un secondo bus (bus address o
degli indirizzi), fisicamente uguale al bus dati composto anch'esso da numerosi fili paralleli, ciascuno
in grado rappresentare il nome in binario
della periferica o della locazione di memoria desiderata consente
l'individuazione corretta del dispositivo con cui interloquire.
Questo bus, a differenza del bus dati, è monodirezionale: del resto il suo compito è limitato ad individuare chi deve intervenire in un rapporto di scambio. La CPU è l'unica entità che può impartire tale disposizione, per cui tale comando (o meglio, il numero che corrisponde all'oggetto che deve intervenire) potrà solo uscire dal processore.
Lo schema evidenzia questa monodirezionalità tramite una freccia
rivolta verso chi è chiamato a ricevere il comando. Il numero
specificato dentro il bus rappresenta ancora la quantità di fili paralleli
coinvolti.
Ora però ci colpisce una differenza: nell'esempio di figura i fili che escono
dalla CPU e quelli che entrano nella memoria sono 20, mentre quelli che entrano
nei 2 dispositivi di I/O sono solo 10. Riprenderemo tra poco questa apparente
incongruenza, non riscontrabile nell'altro bus.
E' importante distinguere con sicurezza il bus dati dal bus address; entrambi
hanno origine nella CPU ma il primo è il vero canale dell'informazione, mentre
il secondo serve solo per individuare con certezza l'interlocutore della CPU.
Non confondiamo quindi i due bus dati con quello degli indirizzi!
Il bus address è nato per consentire al processore di chiamare in causa una ben precisa locazione di memoria, ma anche una determinata periferica. Il numero dell'indirizzo, posto sul bus, arriva di norma a tutti e due le tipologie le periferiche ma solo una risponderà al processore! Come è possibile?
Il Bus di controllo
La cosa non ha nulla di
magico: insieme al bus address, si fa pervenire ad entrambe un filo di
controllo. Se il livello presente su di esso è 1 sarà la memoria ad
intervenire, mentre se 0 sarà il dispositivo di I/O.
Questa tecnica è praticata fin dai primi processori e mediante una coppia di
fili di controllo è possibile far sapere a chi riceve l'indirizzo se deve
dare (read) o ricevere (write) dati.
Sul processore 8086 la prima
linea di controllo si chiama MEM/IO (MEMoria o
Input/Output) mentre le altre RD e
WR; combinando tra loro questi segnali (con
opportuni piccoli circuiti, detti di decodifica) viene evitato ogni conflitto e
il processore potrà disporre a suo piacere o dell'una o dell'altro, in lettura o
in scrittura.
Particolari linee di controllo vengono quindi utilizzate per chiarire il tipo di servizio che la CPU desidera avere dalla sua memoria o dai suoi numerosi dispositivi di input/output.
L'insieme di tali linee è detto bus di controllo
che non è quindi un vero e proprio bus (cioè tale da raccogliere e organizzare
linee dello stesso tipo)
ma piuttosto una raccolta di linee specializzate e funzionalmente molto diverse
tra loro.
Le linee del bus di controllo sono in parte generate dal processore, come RD, WR
e MEM/IO, viste in precedenza; Altre sono fornite esternamente alla CPU e la più significativa è la linea di
CLK, sulla quale è presente un'onda quadra
(clock) la cui frequenza dà il tam tam al processore;
Il microprocessore è il cuore del tuo computer; con i suoi bus chiama (address) e comunica (dati) secondo certe modalità (control bus) con il resto del suo mondo. I piedini del processore rappresentano le connessioni verso questi importanti canali di comunicazione.
Il modo migliore per tentare
di saperne di più sulle linee di controllo è quello di osservare il pin-out di
un processore, cioè la disposizione dei suoi piedini.
Poichè l'analisi dei 478 piedini del Pentium 4 sembra improponibile,
analizzeremo quelli del vecchio padre 8086 (che per altro abbiamo adottato anche
per lo sviluppo dei prossimi programmi in Assembly).
In quel tempo i componenti integrati avevano ancora la forma snella e allungata
dei contenitori Dual-in-Line (cioè con piedini in linea su 2 file) e la
dimensione dei più grandi era proprio di 40 pin (20+20). Vediamola:
I limiti di questa struttura
hanno subito imposto scelte obbligate ai progettisti; il conto è presto fatto:
20 linee d'indirizzo, più 16 di dato, più 2 per l'alimentazione (Vcc e GND)per
un totale di 38 piedini!
Avendone solo 40 pin a disposizione si doveva inventare qualcosa: per recuperare
spazio 16 pin sono stati destinati sia per le 16 linee del bus dati che per le
prime 16 (su 20) linee del bus address.
La presenza su quei piedini (AD0 - AD15) di segnali così diversi non può essere
contemporanea, per cui il processore pone su di essi, in tempi diversi, la parte
bassa dell'indirizzo necessario e poi il dato a 16 bit.
Il problema è ora scaricato sulle spalle del progettista che si trova nella
necessità di memorizzare l'indirizzo poco dopo la sua apparizione sul bus, per
mantenerlo poi stabile e visibile anche quando sarà sostituito dal dato da
utilizzare con la memoria o con il dispositivo di I/O corrispondente a quel
indirizzo. Per questa operazione è indispensabile un segnale di controllo,
ALE (Address Latch Enable), che entra in gioco nel
seguente modo:
1) il processore porta ALE a 1 (a riposo questo segnale vale 0) e subito dopo
mette su AD0 - AD15 l'indirizzo che, con le rimanenti linee "libere", AD16 -
AD19, punta la locazione (o il dispositivo) desiderata.
2) quando l'indirizzo è stabile sul suo bus, il processore porta ALE a 0: questo
fronte di discesa dovrà essere utilizzato per memorizzare localmente in un
opportuno dispositivo(ad esempio con due 74LS374) il numero a 16 bit
dell'indirizzo.
![]() |
Questo componente contiene 8 elementi di memoria (flip-flop) di tipo D-Type, controllati contemporaneamente da un unico piedino di abilitazione |
3) poco dopo (con ALE sempre a
0) leva l'indirizzo dal bus e mette al suo posto il dato, attivando quasi
contemporaneamente WR a 0.
4) dopo qualche istante riporta WR a 1 (stato di riposo) e poi toglie anche il
dato, sperando che la memoria abbia avuto il tempo di riceverlo.
Quello appena descritto è un ciclo di scrittura memoria e, per tutto il tempo
necessario (circa 4 cicli di clock), il segnale MEM/IO è stato tenuto a 1
(riposo).
Prima di passare ai dettagli
funzionali del processore 8086 desidero mostrare la struttura esterna del 80286;
il limitato contenitore DIP (Dual InLine Package) a 40 pin dell'8086 ha lasciato
il posto al più capiente PLCC (Plastic Leaded Chip Carrier) o PGA (Pin Grid
Array), da 68 pin.
Ora i piedini sono disposti su 4 lati (17 per ogni lato del quadrato) e sono
sostanzialmente tutti quelli già notati nel pin-out dell'8086.
Notiamo, in particolare, la presenza di 4 linee in più per il bus address (da A0
a A23),
mentre il bus dati è ancora a 16 bit; comunque ciascuno di essi ha la propria
dignitosa autonomia e il fastidioso procedimento di memorizzazione illustrato
per l'8086 non è ora più necessario.
La tabella seguente ricorda
caratteristiche dei diversi processori intel appartenenti alla famiglia 80x86:
Processore |
Anno |
Numero Transistor |
Register Size |
Data Bus Size |
Address Bus Size |
Memoria Max |
Bus Speed (in MHz) |
Clock Max (in MHz) |
Numero Piedini |
Contenitore | micron |
8088 | 1979 | 29.000 | 8 bit | 8 bit | 20 bit | 1 Mbytes | 5-8 | 40 | DIP (Dual InLine Package) | 3 | |
8086 | 1978 | 29.000 | 16 bit | 16 bit | 5-10 | ||||||
80188 | 1982 | 8 bit | 8 bit | 20 bit | 1 Mbytes | 5-10 | 68 | PGA (Pin Grrid Array) | 3 | ||
80186 | 1982 | 16 bit | 16 bit | 6-10 | |||||||
80286 | 1982 | 134.000 | 16 bit | 16 bit | 24 bit | 16 Mbytes | 6-20 | 68 |
PGA (Pin Grid Array) PLCC (Plastic Leaded Chip Carrier) |
1.5 | |
80386SX | 1985 | 275.000 | 32 bit | 16 bit | 24 bit | 16 Mbytes | 16-33 | 132 | DX-PGA (Pin Grid Array) | 1.5 | |
80386DX | 1988 | 32 bit | 32 bit | 32 bit | 4 Gbytes | 16-40 | |||||
80486DX | 1989 | 1.200.000 | 32 bit | 32 bit | 32 bit | 4 Gbytes | 33 | 25-50 | 168 | PGA (Pin Grrid Array) | 1 |
80486SX | 1991 | 1.185.000 | 25 | 16-33 | 196 | PQFP | 1 | ||||
80486DX2 | 1992 | 1.200.000 | 25-33 | 50-66 | 168 | PGA (Pin Grrid Array) | 0.8 | ||||
80486DX4 | 1994 | 1.600.000 | 25-33 | 75-100 | 0.6 | ||||||
Pentium | 1993 | 3.100.000 | 32 bit | 64 bit | 32 bit | 4 Gbytes | 66 | 60-100 | 273 | SPGA (Staggered Pin Grid Array) | 0.8 |
1996 | 3.300.000 | 120-200 | 0.35 | ||||||||
Pentium Pro | 1995 | 5.500.000 | 32 bit | 64 bit | 36 bit | 64 Gbytes | 66 | 150-200 | 296 | SPGA (Staggered Pin Grid Array) | 0.6 |
Pentium MMX | 1997 | 4.500.000 | 32 bit | 64 bit | 32 bit | 4 Gbytes | 66 | 166-233 | 321 | SPGA (Staggered Pin Grid Array) | 0.35 |
Pentium II | 1997 | 7.500.000 | 32 bit | 64 bit | 36 bit | 64 Gbytes | 66 | 233-300 | 370 | BGA615 (Ball Grid Array) | 0.35 |
1998 | 100 | 266-450 | 0.25 | ||||||||
Celeron | 1999 | 7.500.000 | 32 bit | 64 bit | 36 bit | 64 Gbytes | 66 | 266-533 | 370 | PGA370 | 0.25 |
Pentium III | 1999 | 9.500.000 | 32 bit | 64 bit | 36 bit | 64 Gbytes | 100-133 | 450-600 | 370 | FC-PGA | 0.25 |
1999 | 28.000.000 | 533-1130 | 0.18 | ||||||||
2001 | 44.000.000 | 133 | 1000-1400 | 0.13 | |||||||
Celeron SSE | 2000 | 28.000.000 | 32 bit | 64 bit | 36 bit | 64 Gbytes | 66 | 533-1100 | 370 | FC-PGA (Flip-Chip Pin Grid Array) | 0.18 |
2002 | 44.000.000 | 100 | 1000-1300 | 0.13 | |||||||
Pentium IV | 2001 | 42.000.000 | 32 bit | 64 bit | 36 bit | 64 Gbytes | 266 | 1300-2000 | 423 | FC-PGA2 (Flip-Chip Pin Grid Array 2) | 0.18 |
2002 | 55.000.000 | 1600-2200 | 478 | 0.13 |
Al di là dei dati tecnici, per altro importanti, è interessante notare che la vera chiave di lettura dell'evoluzione di questi oggetti straordinari sua il livello di integrazione dei microcircuiti interni, [misurata in micron 1μm = 10-6 m = un millesimo di millimetro) e indicata dall'ultima colonna a destra della Tabella proposta.
Quasi certamente possiamo
disporre di computer di ultima generazione ma, per mantenere lo studio in
termini semplici e tradizionali, faremo finta di avere a che fare con un 8086,
il padre riconosciuto di tutti i successivi modelli.
Infatti le moderne CPU mantengono la
portabilità verso il basso, cioè devono garantire ai vecchi software di poter
girare senza problemi (sebbene, spesso, il fatto di girare non sia sufficiente
per assicurare la loro godibilità, in genere a causa dell'elevata velocità a cui
sono costretti). La compatibilità segue dal fatto che le caratteristiche funzionali dell'8086 sono un sottoinsieme di quelle degli
attuali processori, per cui progettare con riferimento alle prime significa solo
rinunciare ai vantaggi garantiti dalle seconde.
Riferirsi quindi al set d'istruzioni e ai registri dell'8086 è una scelta di
comodo, didatticamente consolidata.
IL MICROPROCESSORE:
GENERALITA'
Il processore organizza ogni sua operazione misurandola in cicli T, cioè in intervalli uguali al periodo della forma d'onda di clock: in pratica un tempo numericamente pari all'inverso del numero della frequenza.
Anche la memoria cache
presente su un computer può costituire un elemento di scelta, consapevoli che
nessun computer può, oggi, farne a meno.
La memoria cache nasce dall'esigenza di risolvere il problema conseguente
all'elevata differenza tra la velocità di elaborazione galattica del processore
(dell'ordine del GHz) e quella della memoria di sistema che lavora con velocità
decisamente inferiori, quella del bus (800MHz).
Quando la veloce CPU è chiamata ad elaborare dati è, quindi, costretta ad
aspettare che questi arrivino, con comodo, dai suoi lenti bus e dalla sua lenta
memoria esterna di sistema; in questo modo le prestazioni complessive degradano
inevitabilmente.
Per questo è stata inventata la cache memory, che trova posto tra il primo e la
seconda; si tratta di una memoria di piccole dimensioni ma particolarmente
veloce; la sua velocità può variare infatti da quella di clock (se di primo
livello) a valori comunque superiori a quella del bus.
Il trucco consiste nel pre-caricare in cache il maggior numero possibile di
istruzioni (o meglio, di codici operativi e di bytes di dato) che il processore
prevede di eseguire in una determinata sessione di lavoro; non appena la CPU
tenta di comunicare con la sua memoria esterna i dati richiesti (e una buona
parte di quelli vicini ad essi) sono trasferiti nella cache.
In questo modo, almeno nell'immediato e con sufficiente probabilità, la CPU
troverà in essa anche i dati necessari in seguito, senza dover attendere troppo.
Naturalmente prima o poi capiterà che il dato o il codice operativo (dopo un
salto o una chiamata di procedura) richiesti non siano nella cache: in questo
caso la CPU sarà comunque costretta ad accedere normalmente alla lenta Ram di
sistema posta sul bus.
Una delle ragioni della diversa velocità dei 2 tipi di memoria è di tipo
tecnologico: la cache memory è fatta con circuiti elementari a transistor
bipolari ed è nota come SRAM (RAM Statica); per questo è molto veloce (meno di
2ns, più di 500MHz) ma relativamente piccola (il valore tipico è di
256kBytes÷512kBytes, fino ad un massimo di 2MBytes) e più costosa.
la memoria centrale di sistema è la Ram per antonomasia, molto semplice dal
punto di vista costruttivo; è nota come DRAM (RAM Dinamica) e, di solito,
presente in grande quantità (32MBytes, 64MBytes, 128MBytes e via raddoppiando..)
e soggetta alla velocità del bus (alcuni esempi: 15ns/66MHz, 10ns/100MHz,
7.5ns/133MHz, 3.75ns/233MHz).
Fin dalle prime architetture è stata prevista la presenza di cache tra CPU e
memoria, direttamente sulla scheda madre e, data la grande efficienza di questo
meccanismo, ben presto si è pensato di introdurre parte di essa addirittura
dentro il processore; per velocizzare lo scambio di dati tra memoria e
processore sono oggi disponibili: la cache di 1° livello, non grandissima (da
8kBytes fino a 64kBytes) ma funzionante con la stessa velocità (clock) del
processore che la ospita.
La cache di 2° livello, posta la cache di 1° livello (dentro la CPU) e la
relativamente lenta RAM esterna. Sebbene all'inizio come detto fosse allocata
sulla scheda madre, nei moderni processori è loro parte integrante (come la
cache di 1° livello).
La presenza della Cache Memory rende molto più rapida l'esecuzione dei
programmi, dato che la CPU trova con buona probabilità i bytes che gli servono
direttamente dentro se stessa (Cache di 1° livello) e nelle sue immediate
vicinanze (Cache di 2° livello), senza essere costretta a perdere tempo per
indirizzare ed aspettare risposta dalla lenta Ram convenzionale esterna.
Vale la pena ricordare che l'aumento della cache produce aumento di prestazioni,
anche se non direttamente proporzionale e comunque subordinato al caso (per
altro piuttosto probabile) di contenere dentro di sè i dati necessari alla CPU.
Poiché i suoi costi sono intrinsecamente elevati, la tendenza dei produttori è
quella, piuttosto, di rendere le frequenze del bus (e delle memorie DRAM
convenzionali) sempre più simili a quella di clock.
La CPU durante l'accensione
non può contare sul contenuto della RAM poichè se disalimentata perde il suo
contenuto, senza rimedio. Il problema è che il processore DEVE eseguire
qualcosa, all'accensione.
Per sopperire a questa lacuna i suoi progettisti hanno pensato ad un
machiavellico trucco: il processore è forzato a leggere una piccolissima memoria
indelebile, sempre presente sulla scheda madre; si tratta della famigerata BIOS,
una memoria a sola lettura (ROM) che, nel gergo, ha assunto il nome del
programma che contiene (appunto il Basic Input Output System).
Il BIOS contiene tutte le procedure necessarie per predisporre al meglio il
computer e per garantirgli autonomia operativa; tra queste la procedura di
bootstrap, esattamente la prima cosa che qualunque processore si trova ad
eseguire!
Il processore, non appena viene attivato dall'accensione del computer, esegue un
programma (cioè una sequenza di bytes operativi) detto di bootstrap, che oltre
ad eseguire un primo controllo di efficienza, ha il compito di cercare il
sistema operativo (per esempio Windows) sulla memoria di massa (HD) e di
trasferirlo nella memoria centrale (RAM).
Quando il Sistema Operativo è stato trasferito in RAM il processore smette di
eseguire il programma (di bootstrap) in BIOS e viene obbligato a saltarci
dentro, trovandosi ora ad eseguire un altro programma (appunto il SisOp) in RAM,
in attesa dei nostri comandi!
IL
MICROPROCESSORE: I REGISTRI
La struttura interna di un processore è relativamente complessa ma la logica che
lo governa rende facile la comprensione delle sue parti; in questi appunti
analizzeremo in dettaglio lo schema funzionale (architettura) del glorioso 8086,
nostro punto di riferimento per la futura fase della programmazione in Assembly.
Possiamo anticipare che ogni processore è costituito essenzialmente da 2 parti,
rispettivamente specializzate nella gestione dei dati (interfaccia con i bus) e
nella loro elaborazione (unità di esecuzione); in entrambe entrano pesantemente
in gioco i suoi Registri interni, una struttura tra le più importanti e tipiche
di una CPU.
Per questa ragione è conveniente descriverli e conoscerli dettagliatamente, al
fine di rendere più agevoli le analisi proposte nelle pagine future.
Un microprocessore è chiamato ad elaborare informazioni assunte dall'esterno e,
per poterlo fare ha bisogno di un suo taccuino d'appunti personale: chiunque di
noi memorizza sulla carta gli operandi di una addizione e poi diligentemente ne
somma le cifre in colonna, fino a ricostruire il risultato, su una riga
sottostante.
Il processore non è da meno: ha bisogno di memorizzare temporaneamente gli
operandi dentro di se, in attesa che qualcuno li prenda in considerazione e
restituisca il risultato dell'operazione richiesta. Le locazioni di memoria
personali del processore hanno il nome di Registri.
I Registri sono locazioni di memoria ultraveloci (hanno la velocità del clock)
in stretto contatto con tutte le altre parti della CPU a cui appartengono, con
le quali interagiscono al fine di elaborare i dati pervenuti o organizzare il
loro smistamento.
Dunque i registri sono una risorsa di enorme valore, anche come semplice
deposito dati: il programmatore assembly può disporre di punti di memorizzazione
direttamente sul posto della loro elaborazione, in alternativa a quelli classici
disponibili in RAM. E' chiaro che il dato di un registro è disponibile subito,
mentre quello della RAM deve essere da essa prelevato e portato dentro, con
notevole differenza sui tempi d'esecuzione.
Purtroppo il loro numero è piuttosto piccolo ma con una saggia amministrazione
ed un corretto utilizzo la programmazione non può che trarne grande vantaggio.
Il controllo dei registri si esercita attraverso le istruzioni , per esempio
quelle di scambio (come MOV AL,AH ) o quelle aritmetiche (come ADD AX,BX ) o
quelle logiche (come XOR DI,SI ); la conoscenza del set delle istruzioni aiuta
ad usare bene i singoli registri, al fine di accoppiarli ai compiti per i quali
sono meglio predisposti.
I Registri sono, di norma, specializzati, cioè sono in grado di compiere al
meglio certe operazioni anziché altre; usare un registro diverso da quello
specializzato non vanifica l'operazione (istruzione) desiderata ma la rende meno
efficiente e più costosa, nel senso che il programma finale avrà tempi
d'esecuzione più elevati ed occuperà più spazio in memoria).
Il numero e la dimensione dei registri si sono evoluti con la storia dei
processori, come la disponibilità di nuove istruzioni. In contrapposizione con i
registri a 16 bit del patriarca 8086 vanno tenuti nel dovuto conto quelli a 32
bit delle ultime generazioni.
In ogni caso i vecchi registri a 16 bit sono ancora perfettamente riconosciuti e
ampiamente utilizzati nella programmazione; nelle nuove architetture sono da
ritenersi un sottoinsieme dei nuovi (più esattamente la loro parte bassa).
Per questa ragione lo studio e l'utilizzo dei registri a 16 bit rimane del tutto
legittimo e didatticamente valido: spetterà al lettore sperimentare le nuove
istruzioni a 32 bit (certamente più potenti e, forse, un po' più impegnative) al
termine degli esperimenti condotti e suggeriti da questo Tutorial.
Nelle pagine successive spingeremo a fondo la conoscenza dei 14 gloriosi
registri a 16 bit dell'8086
Siamo pronti: questa è la prima
vera interfaccia con la programmazione Assembly.
Descrivere i Registri di una CPU e discuterne le istruzioni è un tutt'uno: i
primi sono spesso gli operandi delle seconde e le seconde tendono a
specializzare l'uso dei primi.
Come d'accordo per pura opportunità e per precisa scelta didattica
(probabilmente discutibile, ma adottata da tutti i docenti, almeno all'inizio)
prenderemo in considerazione i 14 registri a 16 bit dell'8086; il passaggio allo
studio e all'uso di quelli a 32 bit sarà comunque indolore e immediato.
Registri d'uso generale
I primi sottoposti a esame sono i 4 registri di uso generale (General
Purpose Registers); il loro nome è AX,
BX, CX e
DX ed hanno tutti la caratteristica di poter essere
utilizzati così come sono definiti (cioè a 16 bit) o in 2 metà a 8 bit.
Naturalmente il loro impiego nell'una o nell'altra dimensione dipende dal
contesto, cioè dall'istruzione che li coinvolge; nel secondo caso i nomi dei
registri a 8 bit conservano l'iniziale di quello che li contiene, seguita dalla
lettera L o H,
rispettivamente per la parte bassa (Low) e alta (High) del registro a 16 bit da
cui hanno origine.
Sebbene la cosa sia, al lato pratico, del tutto irrilevante la loro lettera
iniziale può essere interpretata come iniziale del compito che al registro
riesce meglio, cioè:
AX, Accumulatore: si tratta del registro più coinvolto dal set
delle istruzioni e nessuno come AX è altrettanto efficiente; il suo nome è
storicamente associato all'immagine di colui che prende su di se (accumula) il
maggior carico di lavoro; in concreto è coinvolto per default in alcune
importanti istruzioni:
- aritmetiche: come
MUL ,
IMUL,
DIV,
IDIV
nelle quali rappresenta in ingresso il moltiplicando e il dividendo e in uscita
il risultato.
- di spostamento dati dalla e verso la memoria, come
LODSB
(equivalente a MOV AL,DS:[SI] ) e
STOSB
(equivalente a MOV ES:[DI],AL)
- di uso particolare (come
IN AL,DX
o OUT DX,AL
nelle quali costituisce rispettivamente il registro destinazione e sorgente dei
dati da scambiare con dispositivi di Input/Output).
Le sue metà a 8 bit sono AL e
AH; le variazioni di ciascuna di esse influenzano ovviamente il contenuto
di AX.
BX, Base: il suo nome deriva dal fatto che, unico tra i 4 registri
di uso generale, può essere usato istituzionalmente come puntatore; le
istruzioni che utilizzano questa tecnica sono, per esempio: quelle di puntamento
diretto, come MOV
AL,DS:[BX].
quelle di puntamento appunto con registro base, come
MOV AL,DS:[SI+BX+00],
di solito con l'aiuto di un registro indice e di uno spiazzamento.
Le sue metà a 8 bit sono BL e
BH; le variazioni di ciascuna di esse influenzano ovviamente il contenuto
di BX.
CX, Contatore: il suo nome deriva dal fatto che numerose
istruzioni lo utilizzano per default come contatore di eventi; tra esse: tutte
quelle che consentono il prefisso
REP,
il compito del quale è proprio di ripetere l'istruzione fino a quando in
registro CX, decrementato ad ogni giro, raggiunge il valore zero.
l'istruzione LOOP,
e le sue simili, nelle quali fa da contatore dei cicli eseguiti: il ciclo viene
ripetuto fino a quando CX, decrementato ad ogni giro, raggiunge il valore zero.
Le sue metà a 8 bit sono CL e
CH; le variazioni di ciascuna di esse influenzano ovviamente il contenuto
di CX.
DX, Data: il suo nome vuole rimarcare la generica disponibilità ad
essere usato per memorizzare dati; in realtà anche per questo registro esistono
istruzioni che gli riservano un uso di default; tra esse: alcune aritmetiche,
come MUL
, IMUL,
DIV,
IDIV,
nelle quali, quando l'operando è una word, rappresenta la parte alta del
risultato.
nelle istruzioni di Input/OutpuT, come
IN AL,DX
o OUT DX,AL,
nelle quali rappresenta l'indirizzo del dispositivo periferico.
Le sue metà a 8 bit sono DL e
DH; le variazioni di ciascuna di esse influenzano ovviamente il contenuto
di DX.
E' bene sottolineare che i 4 registri di uso generale, AX, BX,
CX e DX, possono tranquillamente essere usati nel modo più diverso e
libero; tuttavia se si desidera ottimizzare il proprio codice è saggio tener
conto delle indicazioni suggerite, riservando a ciascuno di essi il compito che
meglio lo realizza.
Solo per completezza, a partire dal 80386 sono disponibili registri a 32 bit; i
4 registri di uso generale sono detti EAX,
EBX,
ECX e
EDX, e quelli precedentemente descritti sono ancora
riconosciuti come loro parte bassa.
Registri di segmento
Il processore 8086 prevede 4 registri di segmento (Segment
Registers); il loro nome è CS,
DS, ES e
SS, tutti a 16 bit, indivisibili.
Per segmento si intende un'area di memoria di 65536 (64k) locazioni consecutive,
utilizzata dal sistema per allocare i programmi, in attesa di ceder loro il
controllo.
A questo proposito
si sottolinea come una importante caratteristica dei files EXE che consiste nel
poter disporre di una quantità di memoria grande fino a 4 segmenti, di solito,
associati al codice (e puntati con CS), allo stack (SS), ai dati (DS)
e ad altri dati extra (ES).
Risulta comunque chiaro che i registri di segmento servono per individuare le
rispettive aree da 64k, nelle quali si svolgono attività vitali per la vita dei
programmi in esecuzione
Un altro concetto chiave legato a questi registri è quello di indirizzo logico
e indirizzo fisico. Il problema è sorto fin dall'inizio: i progettisti
dell'8086 si sono trovati a gestire un bus d'indirizzi di 20 linee con registri
a 16 bit; in pratica era evidente la necessità di porre un indirizzo valido sul
rispettivo bus ma non era possibile farlo perché gli strumenti a disposizione
(registri) erano troppo piccoli.
Da questa esigenza è nata la tecnica della segmentazione della memoria, un abile
sotterfugio per poter puntare ogni indirizzo con 2 registri a 16 bit: il primo
(detto appunto registro di segmento) individua un'area di 64kBytes
consecutivi, dentro lo spazio indirizzabile del processore, ed è uno di quelli
appena descritti.
il secondo (detto registro di offset) è in grado di scorrere ciascuna
delle 65536 locazioni contenute nel segmento, in virtù dei diversi possibili
valori che può assumere (essendo a 16 bit, 216=65536=64k), ed è uno di quelli
descritti successivamente (per poter continuare la descrizione ne prenderemo in
prestito uno, il registro puntatore d'istruzione IP).
Dunque con un indirizzo logico, cioè con una coppia di registri di tipo
Segment:Offset, per esempio CS:IP (notare la forma con cui
si esprime l' indirizzo logico) è possibile ottenere un indirizzo fisico; basta
imparare il meccanismo che la CPU usa, internamente, per ricostruirlo.
L'indirizzo fisico (cioè il numero binario a 20 bit che sarà posto sul
bus address) si ottiene sommando il valore di un registro di segmento
moltiplicato per 16 con il valore di un registro di offset; la coppia di
registri a 16 bit coinvolta nell'operazione può essere messa in evidenza nella
forma Segment:Offset, cioè nella forma tipica di un indirizzo logico.
Da notare che, per rendere leggibili gli indirizzi (sia fisici che logici), è
prassi comune accorpare i bit a 4 a 4 (nibble), sostituendo ogni quaterna con un
simbolo del sistema di numerazione esadecimale.
Così per esempio 0001 0010 0100 0100 0000 (numero assurdamente illeggibile,
espresso in binario) diventa 12440, espresso in esadecimale. Per distinguere con
certezza il tipo di codifica utilizzato si aggiunge una H (per Hex), cosicché
00010010010001000000 (binario) = 12440H (in esadecimale)
Lo schema seguente mostra questa tecnica: si osserva che moltiplicare per 16
significa aggiungere a destra del valore esadecimale del registro di segmento 4
bit di valore nullo (un nibble uguale a 0):
Indirizzo fisico (20 bit) = Registro di Segmentox16+Registro di Offset |
Se CS=1234H
e IP=0100H
|
Va sottolineato che, per ogni indirizzo fisico, i valori dei 2 registri Segment:Offset che concorrono a definire l'indirizzo logico possono assumere numerosissime combinazioni (in pratica ben 65536..); lo schema seguente mostra ad esempio altre 5 possibili alternative:
10000H + |
11110H
+ 1330H = --------- 12440H |
12000H
+ 0440H = --------- 12440H |
11FF0H
+ 0450H = --------- 12440H |
12440H
+ 0000H = --------- 12440H |
Dei registri di
offset parleremo successivamente; in questa riassumiamo i compiti dei 4 registri
di segmento; di solito è il Loader (caricatore dei programmi in memoria) del
Sistema Operativo che si occupa di caricare in questi registri i valori corretti
per l'esecuzione del programma stesso:
CS, Code Segment: punta l'area di 64k destinata a contenere i
bytes del codice eseguibile, la traduzione in linguaggio macchina delle nostre
istruzioni; in ogni momento (insieme a IP, come vedremo) punta il codice
operativo dell'istruzione che sta per essere eseguita. Non esistono istruzioni
per cambiarne direttamente il valore, sebbene la cosa sia possibile, in modo
indiretto, correttamente con
JMP,
CALL
o RET
e impropriamente (e a rischio) con la coppia
PUSH RS
/ POP CS.
Se si desidera avere una copia del valore di CS, per esempio in AX, possiamo
usare senza danno la sequenza di istruzioni
PUSH CS
/ POP AX.
SS, Stack Segment: punta l'area di 64k destinata a contenere i
bytes destinati allo stack, l'area nella quale il processore annota gli
indirizzi a cui tornare in caso di esecuzione di
CALL
o INT;
in ogni momento (insieme a SP, come vedremo) punta la prima locazione
disponibile ad essere utilizzata. Modificare il valore di questo registro è
assurdo e sconveniente: il risultato è il probabile crash del programma in
esecuzione (se non del computer stesso).
DS, Data Segment: punta l'area di 64k destinata a contenere i dati
del programma in esecuzione e può essere modificato senza rischio dal
programmatore, per esempio con la sequenza di istruzioni
MOV AX, nnnnH
/ MOV DS, AX
o con la coppia PUSH
AX /
POP DS;
numerose istruzioni danno per scontato che questo sia il registro di segmento
delle locazioni sorgente (MOVS
, LODS)
dei dati da esse trattati.
ES, Extra Segment: punta l'area di 64k destinata a contenere i
dati del programma in esecuzione (oltre a quelli puntati da DS) e può essere
modificato senza rischio dal programmatore, per esempio con la sequenza di
istruzioni MOV AX,
nnnnH /
MOV ES, AX
o con la coppia
PUSH AX
/ POP ES.
Di solito è utilizzato per puntare particolari zone di memoria: la
sovrapposizione di un segmento ad una determinata area ne facilita la gestione
(si pensi per esempio alla RamVideo); numerose istruzioni danno per scontato che
questo sia il registro di segmento delle locazioni destinazione (
MOVS,
STOS)
dei dati da esse trattati.
Con l'avvento dei processori a 32 bit (cioè con bus dati interno, e quindi
registri, a 32 bit), a partire dal 80386, i sistemi hanno cominciato a gestire
la memoria con altre tecniche e il problema della segmentazione ha perso
importanza; i registri di segmento sono utilizzati in 2 diverse modalità, dette
real mode e protected mode.
In ogni caso, poiché la parte offset dell'indirizzo logico può essere ora anche
a 32 bit, ciascun segmento può arrivare alla dimensione di 4 GBytes.
Il primo dei 2 modi (real mode, che si rifà alla descrizione appena
conclusa) è comunque garantito, per assicurare portabilità verso il basso, e
sarà da noi ampiamente utilizzato con soddisfazione, specialmente per puntare le
locazioni del primo mega di memoria, straordinariamente ricco di informazioni e
zone utili.
Solo per completezza, a partire dal 80386 sono disponibili altri 2 registri di
segmento, GS e
FS,
comunque a 16 bit.
Registri puntatore e offset
l processore 8086
prevede 5 registri ufficialmente adatti a costituire la parte offset di un
indirizzo logico; di solito sono distinti in registri puntatore (IP, SP e BP) e
registri indice (DI e SI), tutti a 16 bit, indivisibili.
Il concetto di offset è stato descritto nella pagina precedente: in sostanza,
traducendo dall'inglese, significa spiazzamento, spostamento (distanza) da un
punto iniziale, e rende bene l'idea del puntatore interno, in grado da
individuare senza dubbi una qualunque delle possibili 65536 locazioni di un
segmento.
La definizione dei registri di offset non può essere disgiunta
dall'accoppiamento con un registro di segmento, sebbene nessuno ci vieti di
usare ciascuno di questi registri come semplice deposito dati a 16 bit.
Come in occasione della definizione degli altri registri è comunque utile
ricordare che esiste un modo proprio di utilizzarli, cioè con gli accoppiamenti
suggeriti dal set delle istruzioni; con questo concetto ben presente vediamoli
dunque uno per uno.
Il registro puntatore IP (Instruction Pointer) fa coppia esclusiva
con CS, e viceversa; una coppia assolutamente fedele.
Il puntatore logico CS:IP è assolutamente indispensabile per la vita di
ogni programma in esecuzione, essendo lo strumento (Program Counter) con il
quale il processore punta il codice operativo dell'istruzione che sta per essere
da lui eseguita.
Si tratta dunque di un registro completamente gestito dal processore stesso, nel
senso che non esiste istruzione che possa modificarne direttamente il valore; in
condizioni normali viene aggiornato direttamente dalle logiche interne del
processore durante l'esecuzione dell'istruzione da esso puntata: il numero dei
suoi bytes viene semplicemente aggiunto al valore iniziale, cosicchè IP, al
termine dell'esecuzione, si ritrova correttamente a puntare l'istruzione
successiva.
In modo indiretto (ma sempre per intervento del processore) il suo valore può
essere cambiato anche radicalmente dall'esecuzione delle istruzioni
JMP,
CALL
o RET;
in nessun caso è conveniente manipolare in modo diverso il contenuto di questo
registro.
Il registro puntatore SP (Stack Pointer) fa coppia esclusiva con
SS (sebbene, all'occorrenza, SS se la possa vedere anche con BP, che
vedremo tra poco...).
Il puntatore logico SS:SP è lo strumento con il quale il processore
gestisce lo Stack, l'area di memoria nella quale annota gli indirizzi a cui
tornare in caso di esecuzione di
CALL
o INT;
in ogni momento punta la prima locazione disponibile ad essere utilizzata.
Anche questo registro è assolutamente indispensabile per la vita di ogni
programma in esecuzione, e la sua modifica, pur possibile in modo anche
esplicito, può rivelarsi sconveniente, se condotta con leggerezza e scarsa
consapevolezza: i rischi di crash del programma in esecuzione (se non del
computer stesso) sono molto elevati.
Il registro è comunque gestito automaticamente dal processore in occasione
dell'esecuzione di istruzioni che coinvolgono lo stack, come le coppie
CALL/RET,
INT/IRET
o PUSH/POP;
in occasione di queste operazioni il valore del registro SP viene
decrementato/incrementato di 2 bytes alla volta.
Il registro puntatore BP (Base Pointer) fa coppia con SS ma
non in assoluto.
Il puntatore logico SS:BP è talvolta usato dal programmatore per puntare
alcuni valori presenti nello stack: si tratta della tecnica di passaggio dei
parametri tramite stack, molto frequente nei linguaggi di programmazione ad alto
livello.
E' facile capire che, intervenendo in un'area di memoria di solito riservata al
processore, ogni operazione deve essere condotta con estrema attenzione,
lasciando inalterato lo stato dei dati gestiti dal processore e soprattutto il
puntatore ufficiale allo Stack, appunto SS:SP.
Per questa ragione, sebbene tollerata, l'uso improprio dello Stack deve essere
gestito con un secondo puntatore, SS:BP.
Come ogni altro registro a 16 bit può, comunque, essere usato senza rischio
anche in coppia con DS o ES, per puntare locazioni di memoria contenenti normali
dati.
Registri indice
I registri indice SI (Source Index) e
DI (Destination
Index) sono normali registri a 16 bit, sebbene il loro nome tradisca le funzioni
alla quale il set delle istruzioni li destina.
Le coppie di puntatori logici previste da numerose istruzioni del set sono
DS:SI e ES:DI, rispettivamente utilizzate per puntare le aree di
memoria da cui prendere (locazioni sorgente, come in
MOVS
, LODS)
e in cui mettere (locazioni destinazione, come in
MOVS
, STOS)
i dati.
Di solito queste potenti istruzioni provvedono anche a incrementare o
decrementare il valore corrente di questi registri: poichè si tratta di
operazioni piuttosto raffinate e impegnative si consiglia di consultarle
direttamente nelle pagine ad esse dedicate.
Per terminare ricordiamo che, comunque, è ammesso anche qualunque altro
accoppiamento, che risulterà certamente meno efficace di quelli consigliati;
entrambi i registri di segmento, DS e ES, possono essere usati in
coppia con i registri di offset DI e SI, ma anche con BP e
BX (quest'ultimo elevato al rango di puntatore).
Solo per completezza, a partire dal 80386 sono disponibili registri a 32 bit; i
registri puntatori sono detti ESI,
EDI,
EBP,
ESP e
EIP, e quelli descritti in questa pagina sono
ancora riconosciuti come loro parte bassa.
In aggiunta citiamo alcuni tra i nuovi registri, introdotti nel tempo: i 3
Control Register 80386:CR0, CR2, CR3.
il Control Register Pentium:CR4.
i 6 Debug Register 80386:DR0,
DR1, DR2, DR3, DR6, DR7.
i 5 Test Register 80486:TR3, TR4, TR5, TR6, TR7.
i Model Specific Register Pentium: MSR.
i Memory Type & Range Register PentiumPro: MTRR.
Registro dei flag
La rassegna dei registri del processore 8086 termina
con il registro dei flag; a qualcuno di noi verrà in mente qualche polveroso
film di guerra, di quelli di propaganda, in bianco e nero..., con l'omino che,
in mezzo alla pista d'atterraggio di una porta-aerei, si sbraccia con in mano 2
bandierine colorate per aiutare il pilota a non ...sfasciarsi sulla tolda...
Ecco ... queste sono le (Maritime Signal) Flags da cui quei 4 mattacchioni di
tecnici Intel hanno tratto l'idea e il nome: flag = segnalazione!
Si tratta di uno strano registro a 16 bit, dall'impiego completamente diverso da
tutti gli altri. Il suo contenuto non è da utilizzare nella consueta forma di
word, ma in funzione del valore sui singoli bit in esso contenuti; dei 16
disponibili solo 9 sono significativi, mentre i rimanenti 7 sono inutilizzati.
Di questi:
- 6 sono flag di stato, di solito influenzati dal risultato delle istruzioni
aritmetico logiche.
- 3 sono flag di controllo, di solito controllabili da programma con opportune
istruzioni dedicate.
Il compito di questo registro è straordinariamente importante: al lato pratico,
il suo contenuto consente al processore di prendere decisioni!
Come abbiamo sottolineato in occasione della descrizione delle istruzioni di
salto condizionato la CPU di solito decide saltando da una parte piuttosto che
da un'altra.
Il valore binario di una singola flag è dunque sufficiente per obbligare il
processore ad eseguire un codice invece di un altro; ne consegue che non
esistono (e sarebbero poco intelligenti) istruzioni che modifichino in blocco
questo registro, sebbene sia possibile scriverlo o leggerlo da o in un altro
registro a 16 bit con il solito trucco (per esempio: con
PUSHF/
POP AX
avremo in AX la copia esatta di tutti e 16 i bit).
In ogni caso è possibile salvarne il valore nello stack (con
PUSHF
) o recuperarlo dallo stack (con
POPF
).
In dettaglio ecco l'aspetto di questo registro; ciascun flag in esso contenuto è
da ritenersi settato se si trova al valore 1, e resettato se a 0
15 |
8 |
7 |
0 |
||||||||||||
0 | 0 | 0 | 0 | OF | DF | IF | TF | SF | ZF | 0 | AF | 0 | PF | 1 | CF |
Analizziamo lo scopo dei 6 flag di stato:
CF |
(Carry Flag, flag di Riporto): viene forzata a 1 se una somma/sottrazione ha prodotto riporto/prestito; influenza i seguenti salti condizionati: JC (salta se CF=1), JNC (salta se CF=0), JB (salta se CF=1), JAE (salta se CF=0), JA (salta se CF=0 e ZF=0), JBE (salta se CF=1 o ZF=1). |
PF | (Parity Flag, flag di Parità): viene forzata a 1 se il risultato di un'operazione contiene un numero pari di 1. |
AF | (Auxiliary Flag, flag di Riporto Ausiliario): viene forzata a 1 se un'operazione ha prodotto riporto tra il primo (bit0÷bit3) e il secondo nibble (bit4÷bit7). |
ZF | (Zero Flag, flag di Zero): viene forzata a 1 se un'operazione ha dato risultato nullo; influenza i seguenti salti condizionati: JZ (salta se ZF=1), JNZ (salta se ZF=0), JE (salta se ZF=1), JNE (salta se ZF=0), JG (salta se ZF=0 e SF=OF), JLE (salta se ZF=1 o SF<>OF), JA (salta se ZF=0 e CF=0), JBE (salta se ZF=1 o CF=1). |
SF | (Sign Flag, flag di Segno): viene forzata al valore corrente del bit più significativo del risultato di un'operazione; come è noto nei numeri con segno 0 significa positivo e 1 significa negativo; influenza i seguenti salti condizionati: JS (salta se SF=1), JNS (salta se SF=0), JG (salta se SF=OF e ZF=0), JGE (salta se SF=OF), JL (salta se SF<>OF), JLE (salta se SF<>OF o ZF=1). |
OF | (Overflow Flag, flag di Overflow): viene forzata a 1 se un'operazione ha prodotto trabocco, cioè ha superato il numero massimo per il registro coinvolto (256=100H a 8bit, 65536=10000H a 16bit, ecc); influenza i seguenti salti condizionati: JO (salta se OF=1), JNO (salta se OF=0). |
Analizziamo ora lo scopo dei 3 flag di controllo:
TF |
(Trap Flag, flag di Cattura): molto utile perchè, se forzata a 1 (di solito con INT 03H ) blocca nel punto predeterminato l'esecuzione di un programma in ambiente Debug; si presta dunque alla grande per l'esecuzione passo-passo (single-step) dei programmi. |
IF | (Interrupt Flag, flag d'Interruzione Mascherabile): se forzata a 0 consente di disabilitare (mascherare) la possibile interruzione da parte di uno dei dispositivi abilitati a ciò; può essere controllata da software con STI (che la forza a 1) o CLI (che la forza a 0). |
DF | (Direction Flag, flag di Direzione): indispensabile nella gestione delle stringhe ( MOVS , STOS o LODS ) per stabilire se i registri indice coinvolti (DI o SI) devono essere incrementati (DF=0) o decrementati (DF=1); può essere controllata da software con STD (che la forza a 1) o CLD (che la forza a 0). |
Solo per completezza, a partire dal 80286 sono disponibili 2 nuove flag e a partire dal 80386 altre 3; in questo ultimo caso anche questo registro (come gli altri) è a 32 bit, EFLAG, aumentando di conseguenza anche il possibile numero di flag:
31 |
24 |
23 |
|
16 |
15 |
8 |
7 |
|
0 | ||||||||||||||||||||||
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | VM | RF | 0 | NT | IOPL | IOPL | OF | DF | IF | TF | SF | ZF | 0 | AF | 0 | PF | 1 | CF |
I nuovi flag sono:
IOPL |
(I/O Privilege Level, livello di privilegio I/O), disponibile già con 80286, specifica (con i suoi 2 bit) uno dei 4 diversi livelli di privilegio richiesti nelle operazioni di input/output |
NT | (Nested Task Flag, flag di task annidato), disponibile già con 80286, controlla il funzionamento di una istruzione IRET ed è normalmente uguale a 0 in modo reale. |
RF | (Resume Flag, flag di ripresa) |
VM | (Virtual Mode Flag, flag del modo virtuale) |
IL MICROPROCESSORE: ARCHITETTURA
Prima di tirare le somme vediamo lo schema funzionale dell'8086: la sua architettura è più che sufficiente per dare l'idea di cosa succede dentro una CPU, senza rischiare di perder d'occhio il nostro obiettivo con l'analisi di architetture più complesse.
Prima di proseguire cerca di non banalizzare questa figura; osservala con
pazienza e cerca di memorizzare i suoi blocchi funzionali.
L'analisi della figura mostra le 2 parti principali di una CPU: alla prima
(unità di interfaccia con i bus, Bus Interface Unit, BIU) è affidato il compito
di acquisire e cedere i bytes da e verso l'esterno.
la seconda (unità di esecuzione, Execution Unit, EU) ha il compito di elaborarli
e (se operativi) di eseguirli, e non ha alcun rapporto con l'esterno.
Sulla base della figura precedente
cerchiamo di capire come il nostro processore riesca ad assumere ed eseguire i
bytes del programma che è chiamato ad interpretare.
Vale la pena di ricordare che i programmi che un processore può eseguire non
sono altro che una sequenza di bytes, freddi numeri binari da noi espressi (per
nostra comodità) in esadecimale.
Questi bytes sono di solito raccolti uno dopo l'altro nella memoria,
puntigliosamente definita di programma (per distinguerla da quella usata,
nell'ambito dell'esecuzione, per altri scopi, come lo stack o le tabelle di dati
o variabili di vario tipo); la memoria di programma può essere la classica RAM
ma anche la memoria a sola lettura (ROM) della scheda madre (BIOS) o della
scheda video o di qualche altro dispositivo.
Di sicuro c'è il fatto che il primo byte sarà sempre un codice operativo, cioè
un byte che rappresenta un'istruzione da eseguire; ad esso potrà seguire un
altro OpCode, ma anche uno o più bytes di dato, come succede per molte tra le
possibili istruzioni che il processore è in grado di eseguire.
Poiché un byte (8 bit) può assumere 256 (28) diversi valori può
sembrare che un processore sia in grado di eseguire solo 256 diverse operazioni.
Troppo poche per le ambizioni di un buon processore; è questa la ragione per la
quale le varie istruzioni sono spesso definite da più di un codice operativo.
Già con 2 OpCode si possono potenzialmente codificare 65536 (216)
diverse istruzioni, ora decisamente troppe; la verità sta nel mezzo: non tutte
le combinazioni possibili dei bit dei codici operativi multipli corrispondono ad
altrettante istruzioni!
In pratica il rapporto tra CPU e istruzione da eseguire è piuttosto articolato:
l'istruzione non è dentro il processore: è conservata in memoria, sotto forma di
uno o più bytes consecutivi.
il processore deve recuperare ciascun byte dalla memoria, ricostruendo
l'indirizzo della locazione corrispondente, con l'aiuto del contenuto dei
registri CS e IP.
solo quando l'informazione è dentro di lui può procedere all'interpretazione del
codice operativo e dei suoi eventuali operandi, cioè all'esecuzione
dell'istruzione.
in ogni caso il processore per fare questo dovrà attivare tutte le sue risorse
per tradurre in fatti gli ordini ricevuti.
Per comprendere in che modo ciò sia possibile dobbiamo imparare a conoscerlo
intimamente; dobbiamo aprire la scatola magica!
Come detto tutto il processo comincia nell'unità di interfaccia con i bus, Bus
Interface Unit, BIU: il suo primo compito consiste nella ricostruzione (con
l'aiuto del suo sommatore interno) dell'indirizzo fisico a partire da quello
logico, suggerito dal contenuto dei 2 registri puntatori di codice, CS:IP, con
il meccanismo descritto in precedenza.
non appena l'indirizzo è pronto e disponibile sul bus address interno la logica
di controllo dei bus lo trasferisce anche su quello esterno, attivando anche le
linee di controllo necessarie alla lettura di memoria.
dopo i tempi tipici della Ram di sistema (qualche nanosecondo) la memoria mette
sul bus dati il codice operativo contenuto nella locazione richiesta e il
processore, messosi istantaneamente in attesa subito dopo aver attivato le linee
di controllo, lo cattura e lo mette nei registri della coda delle istruzioni, a
disposizione della EU.
Questa sequenza (compito della BIU) è nota con il nome di pre-fetch (fase
precedente al prelievo del codice operativo); la BIU continua (in modo autonomo
e distinto dalla EU) ad eseguire il pre fetch, ripetendo velocemente tutto da
capo subito dopo aver incrementato il registro IP, e continuando a stoccare i
bytes letti dalla memoria, uno dopo l'altro, nella coda delle istruzioni.
In questo modo EU (che analizziamo tra un attimo) trova sempre bytes pronti ad
essere eseguiti.
L'idea geniale per ottimizzare i tempi è la coda delle istruzioni: mentre la BIU
si occupa di inserirvi i bytes letti dalla memoria la EU li estrae e li consuma
traducendo in fatti il comando binario ricevuto. Fin tanto che la coda è piena
si tagliano così i tempi morti.
Il vantaggio è reale solo se i bytes (prelevati dalla memoria e presenti nella
coda) sono relativi ad istruzioni consecutive; se la EU è costretta ad eseguire
istruzioni di salto, la BIU svuota completamente la coda e la riempie da capo
con i bytes della sequenza di istruzioni che comincia al nuovo indirizzo, il chè
costringe la EU ad attendere un po'...
E' facile capire che questo processo trae enorme vantaggio dalla presenza di una
cache interna (in pratica una specie di coda a da 8kBytes fino a 64kBytes).
Dunque, mentre la BIU prepara la materia prima (nella coda delle istruzioni)
l'unità di esecuzione, Execution Unit, EU), si occupa di interpretare i bytes
prelevati in sequenza dalla medesima coda, cioè di eseguire l'istruzione.
La sua Unità di Controllo di Esecuzione provvede ad organizzare tutto il lavoro,
che prevede 2 fasi distinte: la fase di fetch, durante la quale il byte
prelevato dalla coda viene trasferito nel suo instruction register (IR).
la fase di decode, durante la quale il byte è "fatto a pezzi", nel senso che
ogni codice operativo è frazionato in gruppi di 2 3 bit, in grado di dare
indicazioni utili per la sua successiva esecuzione, come registri coinvolti o
operazioni logiche o aritmetiche necessarie; ogni gruppo di bit è confrontato
con il contenuto di alcune tabelle interne, in grado di attivare i giusti
segnali di controllo necessari a garantire l'effetto suggerito dall'istruzione
corrispondente al codice operativo.
Non appena l'unità di controllo ha capito, parte la fase di execute, una
incombenza tipica dell'altra parte fondamentale (con i registri) della EU:
l'Unità Aritmetico Logica (ALU); il nome di questa struttura sottolinea la
assoluta semplicità dei compiti che un processore è in grado di eseguire.
Nonostante l'enfasi diffusa sulla potenza dei processori (per altro
indiscutibile) si sottolinea per l'ennesima volta che le sue capacità operative
sono effettivamente elementari: somme, differenze, prodotti e divisioni, qualche
funzione logica (and, or, xor), qualche manipolazione di numeri binari
(rotazioni e scorrimenti di bytes); niente che ciascuno di noi non possa fare
altrettanto bene senza particolare difficoltà. Solo che il processore fa questo
in meno di miliardesimo di secondo!
Quando si parla di istruzione di solito si fa riferimento ad una struttura articolata su 3 campi: il codice Mnemonico, il campo Destinazione e il campo Sorgente (questi ultimi separati da una virgola):
Mnemonico | Destinazione | Sorgente |
MOV | AX, | BX |
La sintassi delle istruzioni del
linguaggio assembly per 80x86 è sempre quella sopra indicata, ed è bene fissarla
bene in mente; l'esempio proposto è quello classico, e si legge: metti (MOV) il
contenuto del registro BX (sorgente) in AX (destinazione).
Nella stesura dei programmi sorgente è buona norma educarsi a mantenere un certo
ordine; sebbene al lato pratico questo fatto non dia valore aggiunto, se non
estetico, conviene far uso del tasto tabulatore, al fine di mettere eventuali
etichette sulla prima colonna, tutti gli mnemonici sulla seconda, e gli
eventuali operandi sulla terza; opzionalmente è possibile aggiungere un quarto
tab per scrivere un commento, di regola preceduto da un ; (punto e virgola).
L'esempio seguente cerca di far luce su questo suggerimento, comunque utilizzato
dall'Autore in tutti gli esempi di questo sito:
Esempio di Codice
Assembly |
|
Depo01 Depo02 |
DB 55H ;variabile a
8 bit DW 55AAH ;variabile a 16 bit |
---------- | --------------------------------------------- |
Inizio: | MOV AX,ES ;Preparo il
puntatore MOV ES,BX ;alla prima locazione MOV DI,0000H ;della Ram del Video MOV AL,ES:[DI] ;Estrazione carattere |
Lo stralcio di codice proposto
mette in evidenza che le parole in esso contenute possono essere scritte
indifferentemente in Maiuscolo o in minuscolo; è una questione di gusto, di
leggibilità e di personalità: l'Autore preferisce (ma può essere paranoia...)
dare dignità alle etichette e al commento, scrivendo entrambi i campi in
minuscolo ma con iniziale maiuscola, mentre predilige lo stampatello maiuscolo
per i campi mnemonico e operandi, al fine di evidenziare la parte principale del
programma.
Il campo etichetta : è molto importante perchè consente di associare un nome di
fantasia alla locazione corrispondente alla variabile (DB, o DW, o altro) o
all'istruzione desiderata.
Il nome dell'etichetta è usato come operando per consentire al processore di
localizzare la variabile in operazioni di scrittura e/o lettura, e l'istruzione
per saltarci (con JMP) o per chiamarla (con CALL) al fine di eseguirla
eseguirla.
L'etichetta può contenere tutte le lettere (a-z,A-Z), tutti i numeri (0-9) ma
non come primo carattere, e i simboli "_" (underscore), "$" (dollaro) e "?"
(punto di domanda).
Inoltre non deve essere uguale alle parole riservate dell'ambiente di
programmazione, come i nomi mnemonici delle istruzioni e delle PseudoOperazioni
o il nome dei registri (sono corrette Inizio Prima ma non DX o ESC).
Il campo mnemonico contiene parole da 2 a 7 lettere (di solito da 3...) che
descrivono sinteticamente il tipo di operazione da eseguire sugli operandi,
oppure l'eventuale PseudoOperazione, o direttiva, da passare all'assemblatore;
si definiscono mnemoniche perchè sono studiate per dare subito l'idea del loro
compito, come MOVe (Muovi, sposta) JuMP (salta) CALL (chiama) e così via.
Il campo Operando è quello che assume le forme più svariate possibili: può anche
rimanere vuoto, se l'istruzione non prevede interlocutori espliciti (in questo
caso gli operandi sono fissati automaticamente dalla sintassi).
quando sono presenti sono di solito uno o 2, e la loro tipologia è spesso del
tipo:
- tra registro e registro
- tra registro e memoria
- tra memoria e registro.
- tra registro e costante
- tra memoria e costante.
- tra accumulatore e costante.
L'operando costante è detto anche immediato, non per dare l'immagine di una cosa
istantanea, ma nel senso di non mediato, cioè in grado di esprimere se stesso
senza bisogno di altri: se si tratta di un numero decimale è sufficiente
digitarlo normalmente (1200); tutti i numeri normali si intendono decimali.
se deve esprimere un numero esadecimale deve avere una "H" dietro (2AH) e, se
comincia per lettera deve avere anche uno "0" davanti (0B800H): se non si
mettesse questo 0 l'assemblatore segnalerebbe errore, scambiando il numero per
etichetta, ovviamente non definita.
se, infine, il numero è binario deve avere una "B" dietro (01001100B), per le
ragioni esposte negli altri casi; la scrittura di un numero binario può sembrare
strana, ma in certe occasioni può aiutare a capire meglio il contesto del
programma, per esempio per evidenziare un sensore in una batteria di otto, o un
certo bit di un registro...
attenti a non scrivere per errore la "O" (vocale maiuscola) al posto dello "0"
(numero zero).
L'operando registro e l'operando memoria sono da intendere come il valore
contenuto in essi.
Il campo del commento è ininfluente dal punto di vista del prodotto
dell'assemblatore, ma per questo non deve essere sottovalutato: un buon
programmatore cerca sempre di annotare il suo pensiero presente per sé e per i
posteri.
Quando si scrive un'istruzione di solito gli operandi vengono coinvolti con un
meccanismo mentale automatico, senza pensarci più di tanto; i formalisti più
attenti amano però associare ogni nostra scelta d'istinto a definizioni spesso
poco memorabili, solo per il gusto di inquadrare tutto in rigidi schemi.
La premessa probabilmente sorprenderà i luminari ed è per questo che ho deciso,
comunque, di cercare un certo formalismo per esprimere questi concetti.
In altre parole il problema non consiste nello stabilire dove trovare il dato da
coinvolgere nelle operazioni del processore (cosa sacrosanta e del tutto
necessaria), ma come chiamare questa tecnica di ricerca, cioè stabilire qual è
il Modo di Indirizzamento.
Sebbene i processori moderni siano in grado di gestire in modo diretto grandi
quantità di memoria (quelli a 32 bit possono utilizzare puntatori in grado di
indirizzare 232=4.294.967.296 locazioni, cioè 4 Gbytes) in quasi
tutte le pagine di questo tutorial si darà ospitalità ai programmi che lavorano
con i registri a 16 bit, per altro naturalmente validi per tutta la famiglia
80x86; per questo i puntatori di memoria saranno in grado di servire aree di 216=65.536
locazioni (64 Kbytes), a noi ben note come Segmenti.
Dunque in funzione della tecnica d'accesso ad un determinato dato; vediamo i
possibili di modi di indirizzamento:
- tra registri: il dato è contenuto in un registro e viene semplicemente
travasato in un altro registro (es: MOV AX,BX); si tratta del metodo più veloce
possibile dato che il processore trova già dentro se stesso le informazioni
necessarie per eseguire l'istruzione; naturalmente non è possibile coinvolgere
registri di dimensione diversa.
- immediato: il dato è indicato direttamente dall'istruzione e viene
inserito nel registro specificato (es: MOV AX,1234H); sebbene la presenza del
numero nella sintassi dell'istruzione possa indurre un pensiero di immediatezza
anche nell'esecuzione, dobbiamo pensare che il dato costante è, in realtà,
scritto nella memoria di programma (cioè quella puntata da CS:IP) dopo il codice
operativo: per questo il processore è costretto ad eseguire uno o più accessi in
memoria (a seconda della dimensione del dato) per prelevarlo e porlo nella coda
d'istruzione, prima di sistemarlo: dentro se stesso, in un registro MOV AX,1234H).
- in memoria MOV [Depo02],1234H); in questo caso la costante assunta
dalla memoria di programma dovrà essere spostata nella memoria dati (cioè
puntata per default da DS:Offset, ma anche in altro segmento, se si impone
esplicitamente un override, MOV CS:[Depo02],1234H).
- diretto: il dato è localizzato dall'etichetta specificata direttamente
dall'istruzione, ed è coinvolto (in lettura o in scrittura) con un registro di
dimensione uguale a quella dell'operando diretto stesso (es: MOV AX, Depo02);
bisogna sottolineare che: l'etichetta non rappresenta l'indirizzo del dato ma il
dato stesso!, cioè il dato contenuto nella locazione puntata da quell'indirizzo;
per questo, sebbene la sintassi proposta come esempio sia perfettamente legale,
personalmente preferisco educarmi a scriverla così MOV AX,[Depo02]: poiché il
compilatore la riconosce allo stesso modo mi sembra una scelta più vicina alla
realtà. L'altro operando deve avere la stessa dimensione del dato associato
all'etichetta (con DB o DW o DD, nella riga di programma che definisce la
variabile); in caso contrario è necessario utilizzare l'operatore Byte o Word o
Dword (per esempio per trasferire in AL solo la parte bassa della word
memorizzata in Depo02 bisogna scrivere MOV AL,Byte Ptr [Depo02])
- indiretto tramite registro: il dato è localizzato con l'aiuto di un
registro di Offset, di solito SI o DI ma anche altri registri a 16 bit: la
sintassi di queste istruzioni (es: MOV AX,[SI]) è, in questo caso, meno ambigua,
dando con le parentesi quadre, il senso di contenuto della locazione puntata da
SI (o qualunque altro registro).
per altro, se l'operando sorgente è una costante, è assolutamente necessario
specificare anche la dimensione del dato, con l'operatore Byte o Word o Dword
Ptr, per evitare segnalazioni d'errore da parte dell'assemblatore, per esempio:
MOV Byte Ptr [Si],100)
- indiretto tramite registro e spiazzamento: il dato è localizzato
sommando il contenuto di un registro indice, SI o DI, o base, BX o BP, con il
numero esadecimale con segno a 8 o a 16 bit, specificato nell'istruzione, detto
displacement (spiazzamento) (es: MOV AX,[SI+10]): la somma del contenuto dei
registri con il numero fornito punta una locazione nel segmento DS se i registri
coinvolti sono SI, DI o BX, e nel segmento SS se il registro è BP.
l'utilità di questo metodo è evidente quando è necessario scorrere una tabella
in modo dinamico, a partire da un certo punto; per esempio con la MOV AX,[Depo02+SI]
basta cambiare il valore corrente di SI per assumere, una dopo l'altra, le word
contenute nella tabella Depo02
- indiretto tramite registro indice e base e spiazzamento: il dato è
localizzato sommando il contenuto di un registro indice, SI o DI, con uno base,
BX o BP, e con il numero esadecimale con segno a 8 o a 16 bit, specificato
nell'istruzione, detto displacement (spiazzamento) (es: MOV AX,[SI+BX+10]): la
somma del contenuto dei registri con il numero fornito punta una locazione nel
segmento DS se il registro base coinvolto è BX, e nel segmento SS se invece è
BP.
Questa ulteriore opzione rende ancora più versatile la gestione dinamica delle
tabelle.
In questo
paragrafo facciamo conoscenza del Debugger, l'ambiente ideale per ispezionare e
collaudare un programma, creato da noi o scelto tra l'infinita quantità di
eseguibili (COM o EXE) presenti nelle nostre cartelle.
Leggi con attenzione le descrizioni e contemporaneamente tieni aperta una
finestra DOS con il debug in funzione: potrai così verificare sulla tua pelle le
importanti nozioni che ti darò.
Questo strumento (Debug.exe, da sempre fornito con il sistema operativo DOS) è
molto spartano, quasi "repellente" per la sua estrema semplicità; ma questa sua
semplicità è proprio la sua forza.
Naturalmente ci sono strumenti di progetto ben più potenti ma Debug aiuta ad
entrare dentro il processore in modo essenziale e semplice, una condizione
indispensabile sia per il neofita che per il grande esperto.
Dedica con passione qualche ora a questo potente strumento di lavoro: non
snobbarlo e non supporre che dedicargli attenzione sia inutile perdita di tempo.
Tratta fin d'ora il Debug come un tuo amico! ... quando "sarai grande" questo
umile lavoro ti tornerà molto utile.
Le tecniche sostenute in questo capitolo possono sembrare inadeguate al rango di
programmatore al quale ambite, o troppo laboriose, o poco gratificanti... Forse
tutte queste obiezioni sono vere, ma sono convinto della loro reale utilità.
Nel
linguaggio comune un bug è una cosa fastidiosa, un pidocchio, una cimice, una
calamità. Gli anglosassoni sono molto immediati nel definire i concetti e,
spesso, associano ad eventi tecnici un'immagine forte e molto colorata; così un
errore di programmazione diventa un bug.
Lo strumento che consente di localizzare ed eliminare i bug è diventato Debugger,
lo "spulciatore".
La realtà è, a mio avviso, molto più nobile: il Debugger è molto di più, è un
amico inseparabile, in grado quasi sempre di aiutarti a risolvere i problemi e a
trovare gli errori più subdoli; la sua forza consiste nella capacità di simulare
l'esecuzione di qualunque programma eseguibile (sia di tipo COM che di tipo
EXE).
Se l'assemblatore ha ritenuto corretta la sintassi del sorgente e il linker ha
generato senza errore l'eseguibile non è detto che il nostro programma "giri"
come ci aspettiamo; al contrario sono spesso in agguato gli errori di concetto
che si manifestano di solito con devastante arroganza, bloccando tutto il
computer e costringendoci a resettarlo (premere il pulsante di reset o chiudere
l'applicazione con Ctrl-Alt-Canc). Sono i cosiddetti errori in run-time.
In questi casi il modo migliore per capire dove abbiamo sbagliato è ricompilare
il sorgente con dei break-point, cioè dei punti in cui il programma è costretto
a fermarsi.
I break-point di solito si fissano inserendo nel punto desiderato l'istruzione
INT 03H (Ricorda bene questo concetto!!); la loro presenza è del tutto
irrilevante nell'esecuzione normale di un programma, ma se l'eseguibile è fatto
girare sotto DEBUG esso si fermerà proprio nel punto previsto, consentendo
l'esecuzione passo-passo delle successive istruzioni, fino a localizzare quella
che ha creato l'errore run-time.
Dunque nella fase di collaudo e messa a punto di un programma il Debugger è uno
strumento di lavoro particolarmente utile; da questo punto di vista si occupa
del caricamento in memoria dell'eseguibile, in modo paragonabile a quello del
loader del sistema operativo e, dopo aver preparato i puntatori alla prima
istruzione (CS:IP) e alla prima locazione dello stack (SS:SP) cede il controllo
a noi, manifestando la sua disponibilità con un trattino posto sulla riga
successiva all'intestazione:
Con il Debugger è possibile aprire ed eseguire qualunque programma eseguibile: poichè è in grado di mostrarne il codice dissassemblato (molto vicino a quello sorgente originale) è facile scorrerne il contenuto, per cercare di capire o scoprire i trucchi del suo autore. (come pensi che facciano i "crackatori" di professione?).
Comando D - DUMP
Cominciamo con il comando
D, DUMP; già il nome merita una pausa di riflessione: il sostantivo dump
significa letteralmente bidone della spazzatura, posto sudicio, o più
elegantemente cestino dei rifiuti; il verbo dump significa invece rovesciare
La brillante verve anglosassone ha colpito ancora... quando si richiede un dump
qualcuno ci rovescia il cestino dei rifiuti sulla scrivania...; proprio quello
che facciamo noi quando, disperati, ci accorgiamo di avere cestinato, per
errore, un documento a cui tenevamo. Il comando D rovescia sul video una bella
fetta di memoria, mostrandone tutti i dettagli possibili.
Aldilà della metafora questo comando è veramente potente, sebbene lo si possa
apprezzare solo quando si ha effettivamente qualcosa da cercare; premendo la
lettera D, senza nessuna altra specifica, appare a video questa schermata:
E:\Users\ADMINI~1>debug |
NB: I numeri (esadecimali)
digitati dovranno essere forniti senza la 'H' finale!
E' importante ricordare fin d'ora che i numeri esadecimali si possono trattare
senza la "H" posteriore solo quando abbiamo a che fare con DEBUG; se è vero che
esso non capirebbe (segnalando errore...) comandi del tipo D 0100H è altrettanto
vero che l'assemblatore farebbe altrettanto se invece dimenticassimo di metterla
.
Detto questo, analizziamo la videata ottenuta; sono 8 righe così strutturate: il
campo di sinistra (in verde) mostra l'indirizzo segmentato della prima locazione
di memoria che andremo ad ispezionare: il puntatore è inteso espresso in
esadecimale.
il campo centrale (in celeste) elenca il contenuto delle 16 locazioni
consecutive, a partire da quella indirizzata dal primo campo, espresso in
esadecimale.
il campo di sinistra (in giallo) cerca di tradurre in forma Ascii il contenuto
delle medesime locazioni.
Valutiamo in modo critico le informazioni di questa immagine:
- l'indirizzo di segmento è, per tutti i puntatori, sempre lo stesso
(nell'esempio 17B2): il suo valore dipende esclusivamente da quello della prima
zona di ram libera al momento del caricamento del debugger in memoria; questo
ultimo riserva ai nostri esperimenti un intero segmento che comincia proprio da
17B2:0000.
- l'indirizzo di offset iniziale è 0100: il suo valore risente del fatto
che Debug ha riservato per se stesso le prime 256 locazioni, per ospitare, nella
logica dei programmi gestiti dal Dos, il proprio PSP).
- La sequenza dei numeri hex seguenti è esattamente la traduzione
esadecimale di quello che debug ha effettivamente trovato: il loro valore è
assolutamente imprevedibile e può essere cambiato in ogni momento, senza
provocare alcun danno, come vedremo nelle prossime pagine.
- la zona gialla dei caratteri Ascii è illeggibile, tanto più quanto
maggiore è la casualità dell'area di memoria analizzata; tutti i bytes che
corrispondono a caratteri Ascii stampabili sono mostrati per quello che sono,
mentre tutti gli altri (da 00H a 1FH, caratteri di controllo, e da 80H a FFH,
caratteri ascii estesi) sono visualizzati con un punto.
Per motivi di chiarezza la sequenza dei 16 bytes di ciascuna riga è divisa in 2
da un trattino. Se il comando D viene dato di nuovo, viene rovesciata a video la
successiva "cestinata" di 128 bytes, e così ad oltranza.
Se si desidera tornare all'inizio o se si vuole analizzare un'altra parte della
memoria basta specificare l'indirizzo subito dopo la lettera D.
Per esempio possiamo ritornare all'inizio digitando D 17B2:0100.
Vediamo ora un esempio che analizza la memoria video.
Proviamo ad eliminare il
prompt colorato, a pulire lo schermo e a lanciare Debug;
i comandi dos da dare sono, in sequenza:
prompt $p$g
CLS
debug
tutti confermati con invio.
ANALISI RAM-VIDEO (http://www.giobe2000.it/HW/Ramvideo/index.htm)
La Ram Video è una zona della memoria Convenzionale di Sistema; la sua dimensione è di 128kBytes, a partire dall’indirizzo fisico A0000H fino all’indirizzo fisico BFFFFH nel primo megaByte; la pratica della programmazione a basso livello consente di esprimere questi indirizzi fisici nei corrispondenti indirizzi logici, da A000:0000 a B800:7FFF (un indirizzo logico si esprime sempre nella forma Segmento:Offset).
Questa area di memoria è divisa in 3 parti:
- nella prima (da A000:0000 a A000:FFFF) trovano posto i 64K bytes destinati
alla gestione del video in Modo Grafico. L'unità di informazione è il pixel
(semplificando, ciascun byte (8 bit) di quest'area può rappresentare un solo
punto grafico, o meglio uno dei 28=256 colori che esso può assumere;
ma anche otto pixel, nel qual caso ciascuno di essi non può assumere solo 2
colori, bianco o nero, cioè acceso o spento).
- nella seconda (da B000:0000 a B000:7FFF) sono disponibili i 32K bytes dedicati
al modo Monocromatico, ora in disuso.
- nell'ultima parte (da B000:8000 a B000:FFFF) sono allocati i 32K bytes
necessari alla gestione del video in Modo Testo. L'unità di informazione è il
carattere (vedremo che per descrivere le caratteristiche di ciascuno di essi
saranno necessari 2 byte di questa area: carattere+colore).
I 32 kBytes della Ram Video destinata al Modo Testo sono divisi in 8 zone uguali, dette Pagine Video, ciascuna di 4 kBytes (4096 bytes). Ciascuna delle 8 Pagine Video, numerate da 0 a 7, copre quindi l'area di memoria Ram corrispondente agli indirizzi:
Numero Pagina |
Primo Address |
Range Indirizzi Fisici |
Pagina 0 |
B800:0000 |
da B8000 a B8FFF |
Pagina 1 |
B800:1000 |
da B9000 a B9FFF |
Pagina 2 |
B800:2000 |
da BA000 a BAFFF |
Pagina 3 |
B800:3000 |
da BB000 a BBFFF |
Pagina 4 |
B800:4000 |
da BC000 a BCFFF |
Pagina 5 |
B800:5000 |
da BD000 a BDFFF |
Pagina 6 |
B800:0600 |
da BE000 a BEFFF |
Pagina 7 |
B800:0700 |
da BF000 a BFFFF |
In Modo Testo la memoria RamVideo viene utilizzata a livello di byte (piuttosto che a livello di bit, tipico della gestione grafica); le informazioni necessarie per colorare i 128 pixel (8*16) punti necessari per "disegnare" sullo schermo un carattere, sono dunque affidate a 2 bytes.
Il concetto principale sta
dunque nel fatto che solo la Pagina 0 (cioè i primi 4096 bytes di questa area)
viene letta continuamente dalla scheda video e il suo contenuto viene
interpretato e tradotto direttamente sul monitor. Ogni modifica (scrittura)
eseguita sui primi 4000 bytes si traduce in una modifica in tempo reale
sull'aspetto del testo mostrato sul monitor. Possiamo dunque concludere: solo
quello che viene scritto in Pagina 0 produce effetto a video.
le eventuali modifiche eseguite sul contenuto delle rimanenti 7 Pagine (da
Pagina 1 a Pagina 7) non si vede!, per cui la presenza di queste Pagine sembra
inutile.
in realtà la fortuna di disporre di queste 7 pagine alternative ci offre la
possibilità di "salvare" in esse tutta o in parte la Pagina 0; questa operazione
risulta indispensabile per esempio quando si desidera scrivere un messaggio di
avviso o di errore sull'immagine corrente, senza perdere il testo originale
quando il messaggio viene tolto.
è sufficiente infatti salvare l'area coperta dal messaggio in una Pagina
alternativa prima di stampare il messaggio, per poi recuperarla in Pagina 0 non
appena il messaggio è stato letto.
Rimane da chiarire in che
modo i 4096 bytes di una pagina vengono usati per rappresentare i 2000 caratteri
da essa ospitati.
Bisogna sapere che ciascun carattere ha bisogno di 2 bytes (uno per il codice
Ascii e uno per il codice di colore) per cui possiamo concludere che sono
necessari 4000 bytes (dei 4096 disponibili).
Ogni Pagina Video ha quindi a disposizione più bytes di quanti ne servano
effettivamente:
- Il codice Ascii del carattere è il numero che lo rappresenta;
- Il codice di colore (detto anche attributo) del carattere è un byte costruito
dividendo gli otto bit in 3 campi, secondo il seguente schema:
bit7 |
bit6 |
bit5 |
bit4 |
bit3 |
bit2 |
bit1 |
bit0 |
flash |
Sfondo |
Primo Piano |
|||||
F |
S2 |
S1 |
S0 |
P3 |
P2 |
P1 |
P0 |
Ecco i colori di primo piano
bit3 |
bit2 |
bit1 |
bit0 |
Primo Piano |
||
0 |
0 |
0 |
0 |
Black |
0 |
Nero |
0 |
0 |
0 |
1 |
Blue |
1 |
Blu |
0 |
0 |
1 |
0 |
Green |
2 |
Verde |
0 |
0 |
1 |
1 |
Cyan |
3 |
Azzurro |
0 |
1 |
0 |
0 |
Red |
4 |
Rosso |
0 |
1 |
0 |
1 |
Magenta |
5 |
Magenta |
0 |
1 |
1 |
0 |
Brown |
6 |
Marrone |
0 |
1 |
1 |
1 |
LightGray |
7 |
Bianco |
1 |
0 |
0 |
0 |
darkGray |
8 |
Grigio |
1 |
0 |
0 |
1 |
LightBlue |
9 |
Blu Elettrico |
1 |
0 |
1 |
0 |
LightGreen |
10 |
Verde Chiaro |
1 |
0 |
1 |
1 |
LightCyan |
11 |
Celeste |
1 |
1 |
0 |
0 |
Ligh3d |
12 |
Rosa |
1 |
1 |
0 |
1 |
LightMagenta |
13 |
Magenta Chiaro |
1 |
1 |
1 |
0 |
Yellow |
14 |
Giallo |
1 |
1 |
1 |
1 |
White |
15 |
Bianco Brillante |
Ecco i colori di
sfondo
bit6 |
bit5 |
bit4 |
Sfondo |
||
0 |
0 |
0 |
Black |
0 |
Nero |
0 |
0 |
1 |
Blue |
1 |
Blu |
0 |
1 |
0 |
Green |
2 |
Verde |
0 |
1 |
1 |
Cyan |
3 |
Azzurro |
1 |
0 |
0 |
Red |
4 |
Rosso |
1 |
0 |
1 |
Magenta |
5 |
Magenta |
1 |
1 |
0 |
Brown |
6 |
Marrone |
1 |
1 |
1 |
LightGray |
7 |
Bianco |
Il campo più a sinistra è formato dal solo bit7: se esso vale 1 lo sfondo viene fatto lampeggiare, mentre con bit7=0 lo sfondo appare normalmente.
I colori combinati sono mostrati nelle 2 tabelle allegate e mostrano l'aspetto di scritte nei 16 colori di primo piano possibili sugli 8 colori di sfondo possibili, con il codice esadecimale per ottenerlo: I valori esadecimali da 00H a 7FH dei codici di colore senza flash (byte d'attributo) necessari nei Modi Testo, in Assembly, sono raccolti nella seguente Tabella
00H |
01H |
02H |
03H |
04H |
05H |
06H |
07H |
08H |
09H |
0AH |
0BH |
0CH |
0DH |
0EH |
0FH |
10H |
11H |
12H |
13H |
14H |
15H |
16H |
17H |
18H |
19H |
1AH |
1BH |
1CH |
1DH |
1EH |
1FH |
20H |
21H |
22H |
23H |
24H |
25H |
26H |
27H |
28H |
29H |
2AH |
2BH |
2CH |
2DH |
2EH |
2FH |
30H |
31H |
32H |
33H |
34H |
35H |
36H |
37H |
38H |
39H |
3AH |
3BH |
3CH |
3DH |
3EH |
3FH |
40H |
41H |
42H |
43H |
44H |
45H |
46H |
47H |
48H |
49H |
4AH |
4BH |
4CH |
4DH |
4EH |
4FH |
50H |
51H |
52H |
53H |
54H |
55H |
56H |
57H |
58H |
59H |
5AH |
5BH |
5CH |
5DH |
5EH |
5FH |
60H |
61H |
62H |
63H |
64H |
65H |
66H |
67H |
68H |
69H |
6AH |
6BH |
6CH |
6DH |
6EH |
6FH |
70H |
71H |
72H |
73H |
74H |
75H |
76H |
77H |
78H |
79H |
7AH |
7BH |
7CH |
7DH |
7EH |
7FH |
I valori esadecimali da 80H a FFH dei codici
di colore con flash (byte d'attributo) necessari nei Modi Testo, in Assembly,
sono raccolti nella seguente Tabella
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Naturalmente la simulazione non consente di vedere i codici dei caratteri scritti nello stesso colore dello sfondo, come nero su nero, 00H, blu su blu, 11H, e così via.
E' importante sottolineare
che l'azione del processore non produce alcun effetto visivo; questo compito
spetta alla scheda video. I 2 dispositivi hanno in comune la Ram Video cosicché
mentre la seconda ne "spazzola" il contenuto il primo può cambiarle le carte in
tavola.
Ecco cosa vedremo dopo aver dato anche il comando -d b800:0000:
E:\Users\ADMINI~1>debug |
Le prime coppie diverse da 20H/07H sono visibili
a partire dal 161° bytes (quello all'indirizzo B800:00A0): infatti: i primi 160
bytes sono gli 80 bytes 20H e 80 bytes 07H (colore bianco su nero); e
rappresentano gli 80 spazi della prima riga vuota del monitor.
I successivi bytes sono per metà al valore 07H perchè il colore della frase
presente sul monitor è, appunto, bianco su nero (=07H).
... mentre l'altra metà sono i bytes 43H, 3AH, 5CH, 41H, 52H, 43H, 48H, 2DH,
..., appunto la traduzione esadecimale dei caratteri che compongono la frase a
video, E:\Users\Admin~1>DEBUG
Ricordiamo che, in Assembly, la gestione dei
caratteri (in Modo Testo) è affidata a 2 bytes, uno per il codice Ascii e uno
per il codice di colore; in particolare il byte di colore viene immesso
direttamente nella locazione RamVideo desiderata.
Comando F - FILL
C
assemble A [address]
compare C range address
dump D [range]
enter E address [list]
fill F range list
go G [=address] [addresses]
hex H value1 value2
input I port
load L [address] [drive] [firstsector] [number]
move M range address
name N [pathname] [arglist]
output O port byte
proceed P [=address] [number]
quit Q
register R [register]
search S range list
trace T [=address] [value]
unassemble U [range]
write W [address] [drive] [firstsector] [number]
allocate expanded memory XA [#pages]
deallocate expanded memory XD [handle]
map expanded memory pages XM [Lpage] [Ppage] [handle]
display expanded memory status XS
Funzioni Statistiche
=SOMMA(argomento1;[argomento2];...;[argomento30];...)
La funzione SOMMA calcola la somma delle celle e dei valori indicati negli argomenti. Se in una cella abbiamo il valore Vero questo viene considerato pari a 1 mentre il valore Falso è uguale a 0.
=SOMMA(1;A1;B2:B4)
==> somma i valori nell'area B2:B4, A1 e 1
=SOMMA(A1:B20 A3:C6)
==> Somma i valori nell'intersezione tra A1:B20 e A3:B6 (solo per Excel)
La funzione in Excel ammette al massimo 30 argomenti.
SOMMA.SE e CONTA.SE PICCOLO e GRANDE