s.d.s.2006
Il linguaggio assembly 8086
Introduzione
Il linguaggio macchina
Il linguaggio naturale di un microprocessore è il linguaggio macchina. Nel linguaggio macchina
non esistono riferimenti astratti o simbolici e tutte le operazioni sono eseguite direttamente sui
registri o in locazioni assolute di memoria. La programmazione in linguaggio macchina è stata a
lungo l’unica possibile, all’inizio dell’epoca del calcolo elettronico, prima dell’introduzione degli
assemblatori e dei compilatori.
Il linguaggio macchina non è altro che l’insieme delle istruzioni definite per un particolare
processore. Ogni istruzione è identificata dal suo codice, di solito riportato in binario o esadecimale.
Il linguaggio macchina si compone di istruzioni alle quali fanno immediatamente seguito i
relativi operandi. Un esempio di linguaggio macchina per il processore Intel 8086 è il seguente
codice per il confronto del contenuto dell’accumulatore AX con la costante 2066:
00111101
00010010
00001000
Per facilitare la descrizione delle istruzioni e degli operandi, si fa uso della notazione esadecimale in
alternativa a quella binaria. L’esempio precedente assume in esadecimale questo aspetto:
3D
12
08
Resta però difficile lavorare anche con questa notazione. Il codice rimane indistinguibile dagli
operandi e solo a fatica, con l’aiuto di una tabella di conversione si riconosce in 3D l’istruzione di
confronto. Un certo impegno è anche necessario per la traduzione di 12 08 in
(1*16)+2+(8*256)=2066. (viene memorizzato prima il byte della parte bassa e poi il byte della
parte alta del numero considerato a 16 bit)
Oggi la programmazione in linguaggio macchina non ha praticamente più alcun interesse.
Ovunque possibile viene fatto uso di linguaggi ad alto livello. Nei casi in cui si vuole programmare
“il più vicino possibile” alle risorse della macchina si fa uso di un linguaggio assembler.
Assembler
Il codice binario (o la sua rappresentazione equivalente esadecimale) usato nel linguaggio macchina
è molto scomodo; per l’uomo è molto più facile raffigurare e lavorare con simboli e messaggi
piuttosto che con cifre.
I linguaggi assembler o assembly sono stati introdotti proprio per eliminare i problemi di uso
del linguaggio macchina. Le caratteristiche principali dei linguaggi assembler sono le seguenti:
1. In assembler le istruzioni non sono identificate da codici astratti ma da simboli letterali con
significato mnemonico. ADD significa ad esempio addizione.
2. Alle variabili viene fatto riferimento per nome e non per locazione assoluta di memoria.
3. E’ possibile definire istruzioni macro assembler, composte a loro volta da altre istruzioni, e
richiamarle nel programma.
L’istruzione assembler corrispondente all’esempio in linguaggio macchina sopra riportato è:
CMP AX, 2066
Pag. 1
s.d.s.2006
che senza dubbio è più facile da comprendere rispetto alla rappresentazione binaria e esadecimale
del linguaggio macchina.
L’assembler estende le istruzioni originali del linguaggio macchina, identificate da appositi
simboli, con proprie istruzioni di definizione dati, operative, ecc.
Il programma assembler viene tradotto in linguaggio macchina da un programma apposito,
chiamato assemblatore. Ogni istruzione assembler corrisponde a una istruzione in linguaggio
macchina.
Un ulteriore vantaggio dell’assembler rispetto al linguaggio macchina è che l’assemblatore
prima di tradurre il codice di ingresso effettua un controllo sulla sua correttezza. Questo
contribuisce ad identificare ed eliminare eventuali errori.
Il controllo degli errori da parte dell’assemblatore è formale e si riferisce solo all’aspetto
delle istruzioni. In assembler, a differenza dei linguaggi ad alto livello, tutto è permesso. In
assembler non esistono tipi di dati e la stessa distinzione tra dati e codice non è molto netta; nulla
impedisce di trattare il codice come se fossero dei dati e viceversa.
Il ricorso all’assembler può essere importante per generare delle funzionalità particolari e
non disponibili nell’ambito del linguaggio di cui si fa uso ( S.Operativi o programmazione in tempo
raale)
o nei casi in cui sia importante la dimensione del codice macchina generato
( microcontrollori con capacità di memoria limitata).
Consideriamo a titolo di esempio due programmi che svolgono la stessa funzione: entrambi
scrivono sullo schermo la parola “ciao”. Il primo programma è scritto in C, il secondo in assembler.
#include <stdio.h>
main() {
printf(“ciao \n”);
}
DSEG SEGMENT
Outstr db “ciao”,13,10,”$”
DSEG ENDS
;segmento dati
SSEG SEGMENT stack
dw
32 dup (?)
SSEG ENDS
;segmento catasta
CSEG SEGMENT
assume cs:cseg, ds:dseg, ss:sseg
start: mov bx,dseg
mov ds,bx
mov dx,offset outstr
mov ah,09h
int 21h
;segmento codice
mov ah,4Ch
int 21h
CSEG ENDS
END start
;termine programma
;richiamo MS-DOS
;DS=DSEG via bx
;puntatore a “ciao”
;uscita su schermo
;richiamo MS-DOS
; termine programma
; inizio a start
Pag. 2
s.d.s.2006
Il primo programma è molto più compatto del secondo, oltre che più facile da leggere e capire. Il
programma assembler è a prima vista certamente molto più complicato ed è necessario un certo
tempo per analizzarne e comprenderne le funzioni, che in questo caso sono comunque molto
semplici.
Entrambi i programmi funzionano secondo lo stesso principio, richiamando un modulo del
sistema operativo per la presentazione di una stringa sullo schermo. Nel programma in linguaggio C
i dettagli di questa chiamata sono nascosti al programmatore al quale è sufficiente scrivere
l’istruzione printf . Spetta al compilatore generare la chiamata al sistema operativo, aggiungere alla
stringa i codici di controllo “a capo” e “ritorno carrello”, ecc.
Nel programma assembler è necessario tenere esplicitamente conto di tutti questi aspetti. Le
chiamate al sistema operativo hanno luogo per mezzo dell’istruzione INT 21h. La stringa di uscita è
definita nell’area dati; con essa devono essere indicati esplicitamente i codici di “a capo” (ASCII
10) , “ritorno carrello” (ASCII 13) e termine stringa “$”.
La differenza nello spazio occupato in memoria dai due programmi è evidente quando si
passa a compilarli e collegarli. Le dimensioni in byte dei codici sorgente, oggetto (compilato) e
eseguibile (collegato) dei due programmi sono qui confrontate:
Cod. sorgente
Cod. compilato
Prog. eseguibile
Programma C
57 byte
578
15 KB
Programma assembler
682
194
610
La differenza nelle dimensioni del programma sorgente assembler è dovuta al maggior spazio
richiesto per le istruzioni e per i commenti necessari. Il codice compilato è però già più compatto
per il programma assembler. La differenza più rilevante si nota dopo che i programmi sono stati
collegati alle rispettive biblioteche e routine di servizio: il programma in assembler manca quasi
completamente di overhead, che invece caratterizza il programma in C. Il programma scritto in
assembler è più rapido a caricarsi e eseguirsi; entrambi i programmi producono lo stesso risultato e
non sono distinguibili solo sulla base di quest’ultimo.
Non esistono criteri assoluti per optare per un linguaggio ad alto livello oppure assembler.
Se con l’assembler è possibile scrivere programmi più efficienti, è anche vero che la loro stesura
prende molto più tempo rispetto allo scrivere programmi in un linguaggio avanzato. Anche la
documentazione e la manutenzione di programmi assembler sono più difficili e dispendiosi.
La programmazione in assembler resta comunque di attualità in tutti i casi dove con la
programmazione ad alto livello si raggiungono i limiti di capacità di memoria o velocità di
esecuzione di una macchina.
Alcune funzioni, in particolare quelle che agiscono direttamente sulle risorse del sistema,
non sono realizzabili se non in assembler.
Aspetto non trascurabile della programmazione assembler è il suo carattere didattico.
Indipendentemente dal numero e tipo di prodotti software installati in un sistema, il processore,
almeno con le architetture attuali, opera su istruzioni di macchina assimilabili a quelle di un
programma assembler. L’assembler aiuta quindi a comprendere meglio i meccanismi di
funzionamento della macchina.
Esistono diversi linguaggi assembler per lo stesso processore, così come di ogni linguaggio
ad alto livello esistono versioni differenti.
Il microprocessore 8086 (e famiglia)
Il microprocessore 8086, appartiene a una famiglia di processori prodotti dalla società Intel. Esso ha
una lunghezza di parola di 16 bit, ma può lavorare anche su byte (8 bit). Il bus dati dell’8086 è di 16
bit e la memoria indirizzabile 1 Mbyte.
Pag. 3
s.d.s.2006
Il set di istruzioni del microprocessore 8086 comprende circa 70 istruzioni di base
combinabili in un massimo di 30 modi diversi di accesso alla memoria.
L’8086 ha fatto la sua comparsa nel 1978. Un anno più tardi è uscito il tipo 8088, la cui
unica differenza rispetto all’8086 è di fare uso di un bus dati di 8 bit invece che di 16, pur
mantenendo intatta la possibilità di indirizzare 1 Mbyte di memoria. In tutto il resto, e in particolar
modo in riferimento agli aspetti di programmazione, i due microprocessori sono identici.
Nel 1983 la Intel ha prodotto il microprocessore 80286. Esso ha una lunghezza d parola e un
bus dati a 16 bit, ma la possibilità di indirizzare 224 byte di memoria (16 Mbyte).
Novità di questo nuovo processore è la possibilità di lavorare in due modi, reale e protetto.
Nel modo reale l’indirizzamento è limitato a 1 Mbyte di memoria e le istruzioni sono quelle
dell’8086 mantenendo quindi la compatibilità con quest’ultimo.
Nel modo protetto si possono indirizzare 16 Mbyte di memoria oltre che fare uso di nuove
istruzioni. L’8086 è stato progettato per facilitare la multiprogrammazione per mezzo sia di
istruzioni sia di una gestione più razionale dello spazio di memoria.
Nel 1986 compare l’Intel 80386. La sua lunghezza di parola e il suo bus sono a 32 bit; esso
ha inoltre la possibilità di indirizzare direttamente 4 Gbyte di memoria, l’intero spazio descrivibile
con 232 indirizzi.
I registri
Il microprocessore 8086 dispone di 14 registri per l’esecuzione delle operazioni aritmetiche e
logiche e per l’indirizzamento delle aree dati, di programma e di catasta. Alcuni registri sono
associati a particolari operazioni.
15
87
AH
BH
CH
DH
0
Accumulatore AX
Base
BX
Conteggio CX
Dati
DX
AL
BL
CL
DL
15
0
SP
BP
SI
DI
Puntatore catasta
Puntatore base
Indice sorgente
Indice destinazione
CS
DS
SS
ES
Segmento codice
Segmento dati
Segmento catasta
Segmento extra
IP
Contatore istruzione
ODITSZ A P C
Flag
Pag. 4
s.d.s.2006
I registri più flessibili e più frequentemente usati sono i registri dati AX, BX, CX, DX. Questi
registri possono essere considerati sia come entità uniche a 16 bit, sia come coppie di registri a 8 bit.
L’accumulatore AX è ad esempio composto dai registri a 8 bit AH e AL, singolarmente
indirizzabili. In maniera analoga si hanno BH, BL, CH, CL, DH, DL.
Il registro AX serve da accumulatore principale. Operazioni aritmetiche e logiche vengono
di solito effettuate su questo registro. Tutte le operazioni di ingresso e uscita vengono effettuate via
AX.
BX è il registro base. Questo è l’unico dei registri di uso generale che può anche essere
utilizzato per puntare a locazioni di memoria.
CX è il registro di conteggio. Il contenuto di CX viene automaticamente diminuito di 1 a
ogni ciclo nell’esecuzione di istruzioni iterative, facilitando in questo modo molte operazioni su
stringhe e di spostamento dati.
Il registro dati DX è anche utilizzato come puntatore in alcune istruzioni di ingresso/uscita.
I registri AX, BX, CX, DX possono essere usati liberamente per operazioni aritmetiche e logiche, a
eccezione di alcune operazioni quali la moltiplicazione MUL, la divisione DIV e altre ancora che
richiedono che alcuni degli operandi si trovino in registri prestabiliti.
Un seondo gruppo di registri è quello dei registri puntatori e indice. SP (stack pointer) è il
puntatore all’inizio della catasta; viene automaticamente modificato all’esecuzione delle istruzioni
PUSH, quando nuovi dati vengono messi in catasta, e POP quando i dati vengono tolti dalla
catasta. BP è il puntatore base e serve per accedere direttamente a dati che si trovano sulla catasta.
I due indici sorgente SI e destinazione DI puntano alla memoria dati. Essi vengono usati per
passare più dati in successione da una locazione iniziale a una finale, ad esempio nel trasferimento
di stringhe di caratteri.
I registri CS, DS, SS, ES puntano ai segmenti definiti in memoria. In particolare il segmento
di codice CS punta al segmento contenente le istruzioni del programma, DS (data segment) al
segmento dell’area dati e SS (stack segment) al segmento di catasta. Non è necessario che i
segmenti siano localizzati in zone diverse della memoria; le rispettive aree possono coincidere.
E’ infine a disposizione un altro registro di segmento, ES (extra segment). Esso è usato per
indirizzare un’area dati diversa da quella alla quale fa riferimento DS.
IP(instruction pointer) è il puntatore alla prossima istruzione di programma che deve essere
eseguita. IP è automaticamente incrementato ogni volta che una nuova istruzione viene letta dalla
CPU.
Le 9 flag del sistema 8086 sono raccolte in un registro collettivo che ha come nome stato
del processore. Ogni flag corrisponde ad un bit del registro di stato; i rimanenti 7 bit non sono
utilizzati. Le flag non sono indirizzabili singolarmente, per l’accesso ad esse occorre far uso delle
funzioni definite a questo scopo.
Le flag sono: Carry, Parità, Auxiliary carry, Zero, Sign, Trap, Interrupt, Direction e
Overflow. In generale, le flag servono sia per determinare le condizioni di esecuzione del processo,
sia per riportare il risultato di un’operazione e gli eventuali errori. Per questo i valori delle flag
vengono modificati mediante l’uso di apposite istruzioni e anche automaticamente durante
l’esecuzione di operazioni aritmetiche e logiche.
Le flag Carry, Auxiliary carry, Overflow e Sign riflettono il risultato dell’ultima
operazione aritmetica che è stata eseguita, segnalando eventuali condizioni di errore.
Le flag Parità e Zero indicano in maniera compatta le condizioni di parità sui bit di una
parola e se il risultato di una operazione è uguale a zero.
Le flag Direction, Interrupt e Trap sono sotto il controllo esclusivo dell’operatore.
Direction indica se le operazioni di trasferimento di blocchi di dati vanno eseguite per indirizzi
crescenti o decrescenti di memoria; Interrupt serve all’abilitazione o disabilitazione delle
interruzioni e Trap mette il processore in un modo di esecuzione di una sola istruzione alla volta.
Quest’ultima flag è di utilità in fase di controllo dei programmi.
Pag. 5
s.d.s.2006
Indirizzamento
Il microprocessore 8086 è in grado di indirizzare un massimo di 1 Mbyte di memoria. Dato che la
sua lunghezza di parola è di 16 bit, con i quali è possibile identificare 2 16 =65536 celle elementari di
memoria (byte), è stato scelto di impiegare un sistema un sistema di indirizzamento più complesso
basato sull’uso dei segmenti.
Lo spazio di memoria di 1 Mbyte è considerato diviso in 65536 segmenti, ciascuno di 16 byte di
lunghezza. Gli indirizzi effettivi di memoria sono calcolati combinando una parola di segmento e
una di offset (spostamento) all’interno del segmento. Il valore del segmento è moltiplicato per 16 e
addizionato alla parola di offset per ottenere il puntatore alla locazione assoluta di memoria.
15
00 0 0
0
indirizzo offset ( 16 bit)
segmento (16 bit)
0000
+
19
locazione effettiva di memoria
0
(20 bit)
In conseguenza di questo particolare metodo di indirizzamento gli indirizzi sono scritti con una
coppia di parole segmento:offset. Ad esempio in forma esadecimale come: 0A16:255E. La
locazione effettiva indirizzata è:
0255E
+0A160
________
0C7BE
Secondo questo principio diversi indirizzi segmentati possono corrispondere allo stesso indirizzo
assoluto. In riferimento all’esempio mostrato, anche 0C16:055E e 0825:446E puntano alla
medesima locazione.
Il calcolo delle locazioni effettive di memoria avviene internamente al processore ed è
completamente trasparente al programmatore. In altre parole, da programma vengono caricati i
valori nei segmenti e nei registri di puntamento alla memoria. Le locazioni effettive ne risultano
così definite e non è necessario calcolarle esplicitamente.
Questo tipo di indirizzamento è il risultato di una scelta di sistema. Il microprocessore 8086
doveva avere una lunghezza di parola di 16 bit; 64 Kbyte erano però troppo poche per le
applicazioni già previste in fase di sviluppo. 1 Mbyte di memoria di memoria indirizzabile è stato
considerato un valore realistico e più adatto alle applicazioni del processore.
E’ concepibile estendere il concetto degli indirizzi segmentati. Con due parole di 16 bit sono
indirizzabili un massimo di 232 locazioni, corrispondenti a 4 Gbyte di memoria. Questo metodo è
stato scartato nell’8086 probabilmente per evitare di appesantire il sistema con linee di bus più
grandi, con un tempo più lungo per il calcolo degli indirizzi ecc.
Pag. 6
s.d.s.2006
Nell’esecuzione della maggior parte delle operazioni il microprocessore 8086 fa uso di
registri predeterminati.
Il puntatore alla successiva istruzione da eseguire è dato dalla coppia CS:IP . Il puntatore
alla locazione attuale di catasta è SS:SP. Non è possibile ridefinire questi registri, ma solo
modificarne indirettamente il valore.
Per l’indirizzamento dell’area dati si ha maggiore flessibilità. Principalmente si fa uso del
segmento dati DS e del segmento extra ES insieme ai puntatori SI e DI o il registro base BX.
Struttura di un programma MS Macro Assembler
Nomi
Un nome è una sequenza di caratteri che serve a identificare univocamente variabili e procedure
definite in un programma. Esso può avere lunghezza qualsiasi; l’assembler fa riferimento ai primi
31 caratteri. Un nome deve iniziare con una lettera o uno dei segni “_”, “?”, “%”, “@”; esso non
può iniziare con un numero, anche se è permesso fare uso di numeri internamente al nome.
L’assembler interpreta allo stesso modo lettere maiuscole e minuscole che possono essere
così liberamente scambiate.
Un nome infine, non deve riprodurre uno dei termini predefiniti dall’assembler come ADD,
MOV, LABEL, LENGTH, ecc.
Formato delle istruzioni assembler
Una riga di programma assembler ha il seguente aspetto:
[nome]
istruzione
operandi
[;commenti]
Delle quattro parti che la compongono, solo l’istruzione di operazione e gli eventuali operandi sono
obbligatorie. Le parti rimanenti sono opzionali.
Il nome serve a identificare una parte di programma o una variabile:
out_string
DB
“Ciao”
;variabile
continue:
MOV AX,BX
;parte di programma
display_text PROC NEAR
;sottoprogramma
L’istruzione rappresenta la parte principale dell’intera linea di programma. Essa può essere sia
un’istruzione originale del linguaggio macchina 8086 sia un’istruzione Macro Assembler.
Gli operandi, se richiesti dall’istruzione, devono sempre essere indicati. Il commento è un testo
libero preceduto da un punto e virgola. Il commento serve a descrivere meglio l’istruzione
assembler che da sola è, per sua natura, difficilmente comprensibile. E’ buona norma che i
commenti siano concisi ma allo stesso tempo il più esplicativi possibile.
E’ superfluo ripetere nel commento il significato di una istruzione (lo si capisce infatti
dall’istruzione stessa). Piuttosto è meglio scrivere perché si fa uso proprio di una certa istruzione.
Evitare quindi di scrivere:
MOV
AX,BX
; copia BX in AX
scrivere piuttosto:
MOV
AX,BX
; parametro in AX
; per passaggio a sottoprogramma
Rappresentazione dei dati
La rappresentazione dei dati è una parte introdotta con il Macro Assembler. L’insieme di istruzioni
di macchina originali 8086 non comprende infatti alcuna definizione di struttura di dati, ma offre
soltanto istruzioni per operare su locazioni di memoria.
Pag. 7
s.d.s.2006
Nel Macro Assembler sono a disposizione un certo numero di direttive per la definizione dei
dati. I dati vengono incorporati nel codice macchina prodotto nella posizione e nel formato con il
quale essi vengono definiti.
Uno dei vantaggi maggiori del linguaggio assembler rispetto a quello di macchina è proprio
la possibilità di fare riferimento alla memoria con operatori simbolici (i nomi delle variabili) invece
che con indirizzi assoluti.
Per la definizione delle variabili sono a disposizione le seguenti istruzioni:
DB
define byte
DW define word
DD
define doubleword
DQ define quadword
DT
define tenword
Con DB si definiscono uno o più byte, singolarmente indirizzabili:
constant
textstring
all_zero
DB
DB
DB
10
“Questa è una riga di testo”
100 dup (0)
Nel primo esempio è allocato (riservato) un byte di memoria all’indirizzo stabilito dal contatore;
esso viene inizializzato con il valore 10.
Nel secondo esempio, la stringa “Questa è una riga di testo” è scritta sotto forma di codici
ASCII, a iniziare dal byte al quale punta textstring.
Nel terzo esempio viene generata una stringa lunga 100 byte, ognuno dei quali assume valore
iniziale 0. La direttiva dup serve a far ripetere 100 volte il valore indicato ( ).
DW (Define word) serve alla definizione di una variabile che ha lunghezza di una parola (16
bit):
total_count DW 0
Per total_count è allocata in memoria una parola con valore iniziale 0.
Nell’8086 le parole sono memorizzate in due byte adiacenti. Nel byte con indirizzo inferiore
è messa la parte meno significativa della parola, seguita dalla parte più significativa. La parola 1234
hex è pertanto memorizzata come 34 12.
Occorre fare pertanto attenzione quando si accede ai singoli byte di una parola in memoria
di fare riferimento alla parte effettivamente desiderata.
DD, DQ e DT servono rispettivamente per l’allocazione in memoria di 4 byte, 8 byte e 10
byte.
L’assemblatore è piuttosto rigido nel controllare, per quanto possibile, i tipi di dati. In fase
di assemblaggio viene segnalato un errore se si prova a trasferire una variabile definita come byte in
un registro della lunghezza di una parola o viceversa. Dato che le istruzioni operative, pur
mantenendo lo stesso nome, hanno aspetto differente nel codice macchina generato, l’assemblatore
ha bisogno di una dichiarazione esplicita dei tipi di dati per decidere quale istruzione vada scelta in
ogni caso specifico.
In assembler non è fatta alcuna distinzione tra dati variabili e costanti. Ogni dato in memoria
è singolarmente indirizzabile e modificabile.
L’istruzione EQU (nome EQU espressione) accetta quale espressione una costante, una
stringa di testo o un’espressione. L’espressione numerica viene calcolata prima di essere assegnata
alla variabile simbolica nome, un testo viene invece sostituito senza subire variazioni.
a_1
EQU 24
; a_1 prende il valore 24
pK
EQU 10*20 ; pK prende il valore 200
msg
EQU “esecuzione interrotta”
DOScall
EQU INT 21h
Pag. 8
s.d.s.2006
inc10
EQU
ADD AX,10
msg, DOScall e inc10 sono sostituiti dalle linee di testo indicate dopo EQU ogni volta che si
incontrano nel programma sorgente. L’uso di EQU contribuisce a rendere più leggibili i programmi
una volta che le proprie istruzioni mnemoniche siano state correttamente scelte.
Segmenti
Il microprocessore 8086 fa uso di 4 segmenti per indirizzare le sezioni di memoria in cui sono
contenuti i dati, il codice del programma e la catasta.
Con l’istruzione Macro Assembler SEGMENT è possibile dichiarare separatamente le aree
da associare ai segmenti. La fine di un segmento è indicata da ENDS (End Segment)
AreaDati
SEGMENT
…
AreaDati
ENDS
ProgArea
ProgArea
SEGMENT
…
ENDS
La definizione esplicita di tutti i segmenti non è obbligatoria. Un programma può consistere di un
solo segmento.
I registri DS e ES sono inizializzati dal sistema operativo all’atto del caricamento di un programma
in modo da puntare all’inizio dell’area dedicata al processo, che non corrisponde a quella dove sono
contenuti i dati. Se si vuole che DS punti effettivamente all’area dati, occorre caricare in esso
l’indirizzo relativo con le istruzioni seguenti:
MOV AX, AreaDati
MOV DS,AX
; DS=AreaDati
Nell’istruzione di spostamento dati MOV la destinazione è indicata per prima. Il passaggio
attraverso AX è necessario in quanto non è possibile caricare direttamente una costante in un
registro di segmento ma occorre passare per uno degli altri registri.
Il caricamento diretto nel registro CS non è permesso in alcun caso. Questo registro può
essere modificato solo da istruzioni di controllo dell’esecuzione del programma che agiscono
contemporaneamente sul registro IP, quali ad esempio JMP, CALL, RET ecc.
Una direttiva molto importante per le operazioni sui segmenti è quella di ASSUME. Questa
direttiva serve ad indicare all’assemblatore quale segmento, tra quelli dichiarati, è da considerare
associato ai registri di segmento. La direttiva ASSUME precede le istruzioni di movimento dati:
ASSUME
DS: AreaDati , CS: ProgArea
ASSUME indica all’assemblatore in quale segmento si trovano le variabili in modo che possano
essere calcolati i puntatori ad esse relativi.
Direttive Semplificate
Per definire i segmenti è possibile utilizzare una modalità semplificata
.MODEL SMALL ; o altri modelli
.DATA
…
; i dati
Pag. 9
s.d.s.2006
.CODE
inizio: MOV AX,@DATA
MOV DS,AX
; inizializza DS al segmento dati
…
…
.STACK
…
END inizio
Istruzioni di base
Movimento dati
Istruzione fondamentale per il movimento dei dati da una locazione di memoria a un’altra è MOV,
che può operare sia su un byte (8 bit) che su una parola (16 bit).
MOV ha come operandi registri, puntatori alla memoria oppure costanti. L’assemblatore riconosce
dagli operandi se l’operazione è a 8 o a 16 bit e genera l’istruzione di macchina opportuna.
Nell’istruzione MOV al primo operando corrisponde la destinazione e al secondo il punto di
origine, come nell’esempio seguente dove un byte viene spostato da CH in AL:
MOV AL,CH
CH non viene modificato in seguito a questa operazione. In maniera analoga ha luogo lo
spostamento di una parola da registro a registro:
MOV BX,AX
In questo caso il valore di AX viene copiato in BX.
In linguaggio macchina è possibile far riferimento a locazioni assolute di memoria, come nel
caso seguente dove una parola è copiata dalla memoria in AX:
MOV AX,[04h]
In questa operazione è sottointeso il segmento DS e la parola copiata si trova alla locazione assoluta
DS:04.
In un programma MS Macro Assembler i riferimenti alle locazioni di memoria riservate alle
variabili sono simbolici e in questo caso si fa uso del puntatore a byte, BYTE PTR o a parola
WORD PTR:
alfa DW 1234h
MOV AL, BYTE PTR alfa
Il contenuto del byte meno significativo, a causa della registrazione in memoria a rovescio nella
variabile alfa , 34h, è copiato in AL.
Modi di indirizzamento della memoria
Con l’istruzione MOV AL,[300h] si copia in AL il contenuto della locazione di memoria DS:300h.
Il segmento DS è sottointeso. Tale modo di indirizzare la memoria è denominato diretto.
Un altro modo di indirizzare la memoria è quello indiretto tramite registri.
Si utilizzano in questo caso i seguenti registri:
[BX]
[SI]
[DI]
[BP]
Pag. 10
s.d.s.2006
Per esempio l’istruzione MOV AH,[BX] copia il contenuto della locazione di memoria indirizzata
da DS:BX in AL.. L’effettivo indirizzo dell’operando sorgente si ottiene dal segmento DS *16 +
offset dove per offset si intende il contenuto di BX.
E’ come se fosse scritto MOV AH,DS:[BX] .
Nel caso in cui si volesse forzare il processore ad utilizzare un altro registro di segmento, questo
deve essere esplicitamente indicato:
MOV AH,CS:[BX]
MOV AH,ES:[BX]
Anche i registri SI e DI possono essere utilizzati con la stessa modalità di BX.
MOV AL, [SI]
MOV BL,[DI] il registro DS è sempre sottointeso.
L’indirizzamento indiretto può essere utilizzato per ll’operando di destinazione.
L’istruzione MOV [BX], AL copia il contenuto di AL nella locazione indirizzata da DS*16+BX .
Il registro BP, a differenza degli altri, fa sempre riferimento all’area di stack che è indirizzata
attraverso il registro di segmento SS .
MOV BX,[BP] è trattato come MOV BX,SS:[BP]
Istruzioni aritmetiche
Le istruzioni aritmetiche più frequentemente usate sono: ADD, SUB, MUL, DIV, INC, DEC,
CMP.
Le istruzioni di addizione ADD e sottrazione SUB sono complementari; esse operano su un
qualsiasi registro o locazione di memoria. Il primo operando contiene in seguito il risultato
dell’operazione.
Addizione della costante 5 ad AX, con risultato in AX:
ADD AX,5
Addizione del contenuto di CX a quello di BX, con risultato in BX:
ADD BX,CX
Per l’addizione del contenuto di un registro a quello di una locazione di memoria valgono le
considerazioni già fatte a proposito di MOV. Per i riferimenti alle variabili in memoria vengono
usati i puntatori BYTE PTR e WORD PTR.
Sottrazione del contenuto di BH da quello di AH, con risultato in AH:
SUB AH,BH
INC (incremento) e DEC (decremento) operano rispettivamente addizionando e sottraendo 1 al
registro o alla locazione di memoria indicata. Queste istruzioni sono eseguite più rapidamente delle
corrispondenti ADD reg/mem ,1 e SUB reg/mem ,1; inoltre prendono meno spazio in memoria.
Incremento di SI di 1:
INC SI
La moltiplicazione MUL e la divisione DIV fanno uso di alcuni registri predefiniti. In entrambi i
casi si distingue tra operazioni a 8 e a 16 bit; eventuali condizioni di overflow o di errore sono
segnalate per mezzo delle flag aritmetiche. Moltiplicazione e divisione fanno sempre uso
dell’accumulatore principale AX come uno degli operandi.
Per la moltiplicazione tra due numeri a 8 bit uno degli operandi è messo in AL, l’altro può
trovarsi in qualsiasi registro o locazione di menoria. Nell’istruzione il registro AL è sottointeso:
MUL BL ; AL*BL -> AX
Pag. 11
s.d.s.2006
Il risultato di una moltiplicazione con operando a 8 bit può avere lunghezza fino a 16 bit ed è messo
in AX. Se gli 8 bit superiori del risultato sono diversi da 0, le flag CF e OF assumono valori 1 ad
indicare la presenza di cifre significative.
In maniera analoga avviene la moltiplicazione tra due parole a 16 bit. Una parola è messa in
AX , l’altra è in un registro o locazione di memoria qualsiasi. Il risultato è diviso: i 16 bit inferiori
sono in AX , i 16 bit superiori in DX. Se il contenuto della parola più significativa DX, è diverso da
0, CF e OF prendono valore 1.
Nell’8086 sono possibili due operazioni di divisione, di una parola a 16 bit per un byte e di una
parola doppia (a 32 bit) per una parola a 16 bit. La divisione ha luogo sempre su numeri interi senza
segno e produce un quoziente e un resto anch’essi interi.
Nella divisione a 8 bit il dividendo è messo in AX e il divisore, registro o locazione di
memoria, è esplicitamente indicato:
DIV CL
In seguito a questa operazione il quoziente (intero) viene riportato in AL e il resto in AH.
Nella divisione a 16 bit, il dividendo ha una lunghezza di 32 bit. I suoi 16 bit più
significativi sono messi in DX e quelli meno significativi in AX. Effettuata l’operazione, il
quoziente è ritornato in AX e il resto in DX.
DIV WORD PTR alfa
L’operazione di divisione è sempre riferita ad un registro o una locazione di memoria.
L’assembler ha bisogno anche in questo caso di un riferimento univoco al tipo di
indirizzamento in memoria, per BYTE o per WORD, in modo da generare le istruzioni macchina
corrispondenti.
Istruzioni logiche
Il microprocessore 8086 dispone di istruzioni che permettono di operare sui registri con le più
importanti funzioni logiche: AND, OR, NOT, XOR, di spostamento (shift) SHL e SHR e di
rotazione ROL e ROR.
Le istruzioni AND, OR, NOT e XOR agiscono contemporaneamente su tutti i bit di un
registro o locazione di memoria.
L’istruzione AND è usata in particolare per mascherare o estrarre singoli bit da una parola.
Come per le funzioni matematiche, gli operandi delle istruzioni logiche sono registri e
locazioni di memoria. Il primo degli operandi indicati è anche quello destinato a ricevere il risultato
dell’operazione:
AND AX,BX
Solo nel caso di NOT l’operando è uno solo:
NOT DX
Nel seguente esempio l’istruzione AND è utilizzata per estrarre i primi 4 bit più significativi del
registro AX , azzerando i successivi:
AND AX,0F000h
( h serve a indicare che la costante rappresenta un valore esadecimale).
L’istruzione di OR esclusivo XOR, è usata per azzerare il contenuto di un registro in
alternativa a MOV reg,0:
XOR AX,AX
Le istruzioni di spostamento (shift) a destra e a sinistra, SHL e SHR e di rotazione (rotate) a
destra e a sinistra, ROR e ROL richiedono che il numero di bit di spostamento o rotazione si trovi
nel registro CL.
Uno spostamento a sinistra di 4 bit del contenuto di AX viene effettuato nel modo seguente:
MOV CL,4
SHL AX,CL
Pag. 12
s.d.s.2006
Per lo spostamento e la rotazione di un solo bit è sufficiente la forma diretta dell’istruzione,
passando all’assemblatore l’operando 1:
SHR AX,1
Non è possibile fornire un operando costante diverso da 1; in tal caso occorre far uso del registro
CL.
Operazioni su catasta
La catasta (stack) è una struttura dati utilizzata per memorizzare risultati intermedi di operazioni,
per trasferire dati ad altri moduli del programma e per salvare lo stato dei registri quando
l’esecuzione del programma principale viene interrotta per passare il controllo ad un
sottoprogramma.
La catasta è una zona riservata della memoria che può essere localizzata ovunque. L’accesso
alla catasta funziona secondo il principio LIFO ( Last In First Out), l’ultimo dato a entrare è il
primo a uscire. I registri relativi alla catasta sono il segmento SS (stack segment) e SP (stack
pointer), puntatore alla posizione dell’ultimo elemento messo su catasta. SP è sempre riferito a SS;
non è possibile cambiare questa definizione.
Nell’implemetazione dell’8086 la catasta cresce da una posizione iniziale verso locazioni
inferiori di memoria. Sulla catasta possono essere poste solo parole di 16 bit.
Le operazioni principali su catasta sono due, PUSH e POP. Con l’istruzione PUSH un
registro, oppure due byte di memoria, sono copiati nella locazione di memoria SS:SP dopo che SP
è stato diminuito di due (lo stack cresce verso locazioni inferiori di memoria).
Operazione inversa a PUSH è POP che toglie parole dalla catasta e le mette nel registro o
nella locazione di memoria indicata come operando. Dopo un’operazione di POP , SP è
incrementato di 2.
Esempio di istruzioni di uso dello stack:
PUSH AX
POP ES
; ES=AX
Dato che le operazioni sono limitate a parole di 16 bit, non è possibile eseguire ad esempio PUSH
BH. Inoltre, anche sulla catasta le parole sono registrate con la parte meno significativa per prima.
Le flag sono copiate in catasta con un’operazione unica PUSHF. Similmente POPF legge
una parola dalla catasta e assegna di conseguenza i relativi valori alle 9 flag.
La catasta è usata anche per il passaggio dei dati a sottoprogrammi. Dato che al richiamo del
sottoprogramma sulla cima della catasta si trovano i puntatori di ritorno al processo chiamante ( CS
e IP ) , non è possibile accedere ai dati contenuti in essa con operazioni di POP. Si fa in questo caso
uso del puntatore di base BP caricato con il valore di SP. Le istruzioni relative hanno di solito il
seguente aspetto:
…
PUSH BP
MOV BP,SP
MOV AX, [BP] +6 ; primo parametro
MOV BX, [BP] + 8 ; secondo parametro
…
POP BP
In seguito all’operazione PUSH BP sulla catasta si trovano in ordine, BP,IP,CS, l’ultimo parametro
messo sulla catasta con PUSH, il penultimo parametro e così via. Per accedere a questi parametri è
quindi necessario addizionare a BP una costante relativa alla loro posizione sulla catasta. Al termine
del sottoprogramma, il valore originale di BP è ripristinato con un’istruzione POP.
Pag. 13
s.d.s.2006
Operazioni di salto
Le operazioni di salto servono ad interrompere l’esecuzione sequenziale di un programma per
proseguirla da un altro punto. Le operazioni di salto vengono utilizzate per realizzare cicli iterativi e
nella scelta tra più alternative di esecuzione.
I salti possono essere incondizionati e condizionati. Con un salto incondizionato il controllo
viene direttamente trasferito a un’altra parte del programma:
JMP Label_1
In questo caso, l’esecuzione procede dalla locazione indicata Label_1. L’assemblatore converte i
riferimenti simbolici in salti a locazioni assolute.
I salti condizionati avvengono, come indica il nome, solo se è verificata una particolare
condizione. Questa condizione è segnalato dallo stato delle flag; nel caso più frequente vengono
usate la flag di carry CF e le flag di indicazione del risultato di un confronto.
L’istruzione di salto condizionato relativo alla flag CF si chiama JC (junp if carry):
JC
cont1
Se il bit di carry ha il valore true (1) , viene effettuato un salto a cont1.
La condizione relativa a CF può anche essere che il salto debba avere luogo solo quando la
flag ha il valore false (0). L’istruzione usata si chiama JNC (jump if not carry):
JNC cont2
In questo caso, se il bit di carry ha valore false, viene effettuato un salto a cont2.
Nel caso di un salto condizionato combinato con un’operazione di confronto, è necessario
che le due istruzioni vengano eseguite in immediata successione. In questo esempio AX è
confrontato con BX; se i loro valori sono diversi è richiesto un salto a cont3:
CMP AX,BX
JNE cont3
JNE significa jmp if not equal, cioè salta se i due termini non sono uguali. Il confronto vero e
proprio avviene con l’istruzione CMP; l’informazione sullo stato di uguaglianza è passata
all’istruzione successiva mediante la flag zero. L’istruzione CMP esegue una operazione di
sottrazione senza caricare il risultato, ma settando il flag Zero se gli operandi sono uguali.
Nel set di istruzioni 8086 sono disponibili altre funzioni di salto condizionato: JE (Jmp if
equal), JAE (Jmp if Above or Equal), JB (Jump if Below) , e molti altri. Alcune di queste istruzioni
funzionano allo stesso modo come JAE e JNB ( Junp if Not Below).
L’istruzione CMP richiede due operandi. Essa può prendere molte forme a seconda del tipo
di confronto che ha luogo, tra registri oppure di un registro con un dato costante o il contenuto di
una locazione di memoria.
Nella forma più semplice e più frequentemente usata vengono confrontati registri tra di loro,
oppure registri e costanti:
CMP AX,BX
CMP BX,5
Le istruzioni di confronto segnalano il risultato di una operazione per mezzo delle sei flag
aritmetiche.
Consideriamo il seguente esempio. I valori di AX e BX vengono confrontati tra loro. Se il
valore contenuto in BX è maggiore di quello contenuto in AX occorre eseguire la parte di codice
P1, in caso contrario P2:
CMP AX,BX
JBE P2
P1:
...
…
Pag. 14
s.d.s.2006
P2:
JMP cont1
…
…
cont1:
Non esistono chiamate condizionate a sottoprogrammi. Se si ha la necessità di richiamare un
sottoprogramma solo al verificarsi di una certa condizione occorre far uso di qualche riga di codice
in più per aggirare la chiamata quando la condizione non è verificata.
Istruzioni di Input / Output
Il processore 8086 vede i dispositivi di I/O in modo separato dalla memoria e quindi con istruzioni
apposite. Lo spazio di indirizzamento dei dispositivi di Ingresso/Uscita è di 216 =65536 locazioni
denominate port. Originariamente i port hanno dimensione di un byte (8 bit ).
Le istruzioni per comunicare con i port sono:
IN
AL,DX
OUT DX,AL
La prima legge dal port il cui indirizzo è contenuto in DX e copia il valore letto nel registro AL.
La seconda invia il contenuto del registro AL nel port il cui indirizzo è contenuto nel registro DX.
Si noti che il registro DX a 16 bit è compatibile con il numero totale dei port 65536 (da 0 a 65535).
Soltanto con indirizzi di port inferiori o al massimo uguali a 255 si può scrivere l’indirizzo
del port direttamente nell’istruzione, senza usare DX:
IN AL,61h
Si può sempre comunque utilizzare il modo generale:
MOV DX,61h
IN
AL,DX
Sottoprogrammi
Un tipo particolare di salto incondizionato è il richiamo di un sottoprogramma con l’istruzione
CALL:
CALL DISPLAY
…
DISPLAY: …
…
RET
All’istruzione di CALL il processore provvede a salvare in catasta i valori di CS e IP. Nuovi valori
per CS:IP corrispondenti alla locazione del sottoprogramma chiamato (nell’esempio DISPLAY),
sono caricati nei rispettivi registri in modo che l’esecuzione possa procedere dalla nuova posizione.
Il ritorno da sottoprogramma avviene con l’istruzione di chiusura RET. Con RET vengono
letti due byte da catasta e assegnati a CS e IP. L’esecuzione continua quindi nel programma
originale dall’istruzione successiva a quella di richiamo del sottoprogramma.
E’ ovvio che al momento di eseguire RET , il puntatore alla catasta deve far riferimento ai
byte corrispondenti a CS e IP. In caso contrario il controllo del programma passa ad una locazione
errata che quasi certamente non contiene dati interpretabili come valide istruzioni del programma,
provocando quasi subito l’arresto della macchina.
Occorre quindi far sempre attenzione, se si fa uso della catasta all’interno di un
sottoprogramma, a far corrispondere ad ogni istruzione PUSH una di POP prima di uscire dal
sottoprogramma stesso con l’istruzione RET.
Pag. 15
s.d.s.2006
Se la catasta è stata usata per passare dati al sottoprogramma, si può far uso dell’istruzione
RET [n]. In questo caso n parole vengono cancellate dalla catasta durante l’operazione di ritorno,
dopo la lettura di CS e IP.
Gestione delle interruzioni
Un’ interruzione (interrupt) è una sospensione dell’esecuzione regolare di un programma per
permettere l’esecuzione di un programma di servizio.
Un’interruzione può essere causata da una condizione fisica quale l’arrivo di un segnale su
un port di ingresso, una condizione di errore quale la divisione per zero ecc. Le interruzioni si
distinguono in hardware e software, a seconda che queste siano chiamate in seguito ad un evento
fisico oppure un’istruzione di programma. Il richiamo da programma (software interrupt) si effettua
con:
INT n
; n compreso tra 0 e 255
L’esecuzione salta in questo caso al sottoprogramma il cui indirizzo si trova alla locazione 0:4*n e
0:4*n+2. Nel sistema 8086, lo spazio di memoria compreso tra 0:0 e 0:3FF hex è infatti occupato
dagli indirizzi o vettori , dei sottoprogrammi di servizio delle interruzioni.
I vettori 0-4 sono predefiniti nell’architettura dell’8086 per la gestione di stati di errore o di
esecuzione. Gli altri vettori sono liberi, ma nei sistemi MS-DOS alcuni di essi sono utilizzati per le
routine interne del sistema operativo. Alcuni vettori sono a disposizione del programmatore.
Prima di passare il controllo al sottoprogramma di servizio all’interruzione, vengono salvati
sulla catasta i valori di CS, di IP e delle flag.
Questi valori sono automaticamente ripristinati con l’esecuzione dell’istruzione IRET
(Interrupt Return) posta al termine del sottoprogramma di servizio. E’ compito del sottoprogramma
di servizio salvare, e in seguito ripristinare, eventuali altri registri di cui esso fa uso durante
l’esecuzione.
Le Istruzioni MACRO
Con una macro è possibile definire proprie istruzioni, anche di una certa complessità, che vengono
trattate dall’assemblatore come se fossero istruzioni originali del sistema.
In un certo senso, una macro svolge funzioni analoghe a quelle di un sottoprogramma. In entrambi i
casi gruppi di istruzioni che devono essere eseguiti più volte allo stesso modo all’interno di un
programma sono isolati e richiamati come una procedura unica. La differenza tra una macro e un
sottoprogramma è che le istruzioni della macro vengono copiate e assemblate una per una ogni
volta che è fatto riferimento ad essa.
Il vantaggio dell’uso delle macro è che si risparmiano chiamate a sottoprogrammi che a loro
volta comportato la messa di parametri in catasta, la modifica dei registri CS e IP e il loro
successivo ripristino. Lo svantaggio consiste nel fatto che il programma assume dimensioni tanto
maggiori quanto più viene fatto uso di macro. In pratica conviene definire brevi gruppi di istruzioni
come macro e formare sottoprogrammi per blocchi più lunghi.
Una macro può essere definita in un qualsiasi punto del programma, è sufficiente che la
definizione preceda il primo richiamo che ad essa viene fatto. La macro è identificata dal suo nome,
seguito dall’identificatore MACRO.
La seguente macro converte il valore contenuto nei 4 bit più bassi (0-3) del registro AL nel codice
ASCII corrispondente, 0-9 e A-F. Se il valore di questi bit è minore o uguale a 9 il codice ASCII è
dato dalla sua somma con 30hex; se il valore in ingresso è maggiore di 9, occorre addizionare
ancora 7 al risultato per ottenere il codice ASCII relativo alle lettere A-F (41hex-46hex).
Conv_ascii
MACRO
;input in AL
;output in AL
LOCAL cont_1
Pag. 16
s.d.s.2006
Cont_1
AND AL,0Fh
ADD AL,30h
CMP AL,39h
JBE cont_1
ADD AL,7h
NOP
ENDM
;mascheramento bit 4-7
;conversione ascii 0-9
;carattere <=9 ?
;carattere ascii tra 0 e 9
;conversione ascii A-F
;nessuna operazione
Questa macro è chiamata da programma facendo semplicemente uso del suo nome:
Conv_ascii
L’assemblatore, nell’incontrare la macro, sostituisce una per una, nel punto indicato le istruzioni in
essa definite.
Pag. 17