(Incompleto) MINI CORSO DI ASSEMBLER 8086

(tratto dal sito:http://www.giobe2000.it/Tutorial/Cap01/Pag/cap01-01.asp)

ARCHITETTURA

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
dall'indirizzo logico CS:IP si ricava l'indirizzo fisico:
   
   
CS*16 +  1234H*10H +  12340H +
       IP =      0100H =   0100H =
   ----------------------------------
  indirizzo fisico >>>   12440H

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 +
  2440
H =
---------
 12440
H

 11110H +
  1330
H =
---------
 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 AXPOP 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!
 

IL FORMATO DELLE ISTRUZIONI 80x86

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
--------------------------------------------------
Etichetta Mnemon  Operandi     ;Commento                

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.

 

IL COMANDO DEBUG

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
-d
17B2:0100 4D 00 00 53 00 00 00 00-00 00 00 00 00 00 00 00 M..S............
17B2:0110 43 4F 4D 53 50 45 43 3D-45 3A 5C 57 34 00 A1 17 COMSPEC=E:\W4...
17B2:0120 57 53 5C 53 59 53 54 45-4D 33 32 5C 43 4F 4D 4D WS\SYSTEM32\COMM
17B2:0130 41 4E 44 2E 43 4F 4D 00-41 4C 4C 55 53 45 52 53 AND.COM.ALLUSERS
17B2:0140 50 52 4F 46 49 4C 45 3D-45 3A 5C 50 52 4F 47 52 PROFILE=E:\PROGR
17B2:0150 41 7E 32 00 41 50 50 44-41 54 41 3D 45 3A 5C 55 A~2.APPDATA=E:\U
17B2:0160 73 65 72 73 5C 41 44 4D-49 4E 49 7E 31 5C 41 70 sers\ADMINI~1\Ap
17B2:0170 70 44 61 74 61 5C 52 6F-61 6D 69 6E 67 00 43 4F pData\Roaming.CO
-d
17B2:0180 4D 4D 4F 4E 50 52 4F 47-52 41 4D 46 49 4C 45 53 MMONPROGRAMFILES
17B2:0190 3D 45 3A 5C 50 52 4F 47-52 41 7E 31 5C 43 4F 4D =E:\PROGRA~1\COM
17B2:01A0 4D 4F 4E 7E 31 00 43 4F-4D 50 55 54 45 52 4E 41 MON~1.COMPUTERNA
17B2:01B0 4D 45 3D 43 4F 52 4D 41-53 45 52 56 45 52 32 30 ME=CORMASERVER20
17B2:01C0 30 38 00 46 50 5F 4E 4F-5F 48 4F 53 54 5F 43 48 08.FP_NO_HOST_CH
17B2:01D0 45 43 4B 3D 4E 4F 00 48-4F 4D 45 44 52 49 56 45 ECK=NO.HOMEDRIVE
17B2:01E0 3D 45 3A 00 48 4F 4D 45-50 41 54 48 3D 5C 55 73 =E:.HOMEPATH=\Us
17B2:01F0 65 72 73 5C 41 64 6D 69-6E 69 73 74 72 61 74 6F ers\Administrato

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
 

80H

81H

82H

83H

84H

85H

86H

87H

88H

89H

8AH

8BH

8CH

8DH

8EH

8FH

90H

91H

92H

93H

94H

95H

96H

97H

98H

99H

9AH

9BH

9CH

9DH

9EH

9FH

0A0H

0A1H

0A2H

0A3H

0A4H

0A5H

0A6H

0A7H

0A8H

0A9H

0AAH

0ABH

0ACH

0ADH

0AEH

0AFH

0B0H

0B1H

0B2H

0B3H

0B4H

0B5H

0B6H

0B7H

0B8H

0B9H

0BAH

0BBH

0BCH

0BDH

0BEH

0BFH

0C0H

0C1H

0C2H

0C3H

0C4H

0C5H

0C6H

0C7H

0C8H

0C9H

0CAH

0CBH

0CCH

0CDH

0CEH

0CFH

0D0H

0D1H

0D2H

0D3H

0D4H

0D5H

0D6H

0D7H

0dottoH

0D9H

0DAH

0DBH

0DCH

0DDH

0DEH

0DFH

0E0H

0E1H

0E2H

0E3H

0E4H

0E5H

0E6H

0E7H

0E8H

0E9H

0EAH

0EBH

0ECH

0EDH

0EEH

0EFH

0F0H

0F1H

0F2H

0F3H

0F4H

0F5H

0F6H

0F7H

0F8H

0F9H

0FAH

0FBH

0FCH

0FDH

0FEH

0FFH

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
-d B800:0000
B800:0000 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0010 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0020 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0030 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0040 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0050 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0060 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0070 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
-D
B800:0080 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0090 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:00A0 45 07 3A 07 5C 07 55 07-73 07 65 07 72 07 73 07 E.:.\.U.s.e.r.s.
B800:00B0 5C 07 41 07 44 07 4D 07-49 07 4E 07 49 07 7E 07 \.A.D.M.I.N.I.~.
B800:00C0 31 07 3E 07 64 07 65 07-62 07 75 07 67 07 20 07 1.>.d.e.b.u.g. .
B800:00D0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:00E0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:00F0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .

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