edutecnica

Linguaggio assembly 8086

        

Per programmare un microprocessore si usa un linguaggio simbolico chiamato linguaggio assembly (o assembler), costituito da istruzioni che rappresentano il funzionamento interno del processore e delle sue operazioni elementari.

Si dice che l'assembly è un linguaggio orientato alla macchina, perché non si può scrivere software in assembly se non si conosce l'architettura del microprocessore che si vuole programmare. Questo significa che esistono molteplici assembly, a secondo del microprocessore che si vuole programmare, anche se tutti gli assembly hanno come denominatore comune le caratteristiche enumerate nella pagina sui linguaggi macchina.

Per tradurre un programma sorgente scritto in assembly x86,si usa un programma assemblatore, che accetta istruzioni assembly mnemoniche e li traduce automaticamente in codice oggetto.L'assemblatore assegna,quindi,all'istruzione scritta il modo sintatticamente corretto il rispettivo codice macchina.L'assemblatore viene anche detto traduttore 1:1.,perché ad un'istruzione in assembly corrisponde un'istruzione in linguaggio macchina.

A questo proposito, bisogna dire che al suo interno, un elaboratore, memorizza dati ed istruzioni, codificandoli in esadecimale; questo per ovvi motivi:

(14)10=(1110)2=(E)16

come si vede, per codificare ad es. il numero 14 ci vogliono due cifre decimali, quattro cifre binarie e soltanto una cifra esadecimale; una informazione in codice esadecimale è meno ingombrante da rappresentare. Il computer memorizza in esadecimale non solo i dati ma anche le operazioni come l'addizione, la sottrazione o il salto tra due istruzioni; diventa dunque comodo associare dei codici mnemonici (facili da ricordare) alle operazioni usate per programmare la macchina.

Operazione Codice Termine mnemonico
Addizione 30H ADD
Sottrazione 31H SUB
Salto a 40H JMP

L'assembly,come tutti i linguaggi,possiede una grammatica che consente di costruire frasi(istruzioni)in accordo ad una sintassi,un'insieme di istruzioni,dirette al microprocessore,e un'insieme di pseudo-istruzioni o direttive,che sono i comandi diretti all'assemblatore.

Implementa inoltre un paradigma di programmazione di tipo imperativo o procedurale secondo il teorema di Jacopini-Bohm che afferma:
per codificare una qualsiasi procedura sono necessarie 3 strutture di controllo,la sequenza,la selezione e l'iterazione.
(l'informatica è tutta qui)

Il modello generale di un'istruzione in assembly è il seguente:

[etichetta [:]] COP [Destinazione [, Sorgente] ] [;Commento]

come si vede dalla sistassi l'unica istruzione obbligatoria è il codice operativo (COP) tutti gli argomenti restanti sono opzionali (dato che sono contenuti in parentesi quadre).

L'etichetta è opzionale, serve solo a ad associare un nome simbolico ad una variabile o all'indirizzo di una istruzione del programma. Bisogna tener conto che l'etichetta può essere accompagnata dal carattere ":" che è obbligatorio nel caso che l'etichetta indichi l'indirizzo di una istruzione, mentre non si mette quando l'etichetta indica identifica una locazione di memoria assegnata ad una variabile.

Il COP può avere al massimo due operandi (destinazione, sorgente).
Anche la presenza di eventuali commenti preceduti dal punto e virgola non sono obbligatori anche se consigliati.


Istruzioni per il processore 8086

      

Il set di istruzioni per l'assembly x86 può essere classificato per categoria

Istruzioni di uso generale
MOV Sposta i dati dalla memoria al processore e viceversa
NOP Indica che non bisogna eseguire alcuna operazione
PUSH Inserisce un dato nello stack
POP Preleva un dato dallo stack
XCHG Scambia il contenuto di due registri oppure di un registro e di una locazione di memoria, oppure un registro ed il contenuto di una locazione di memoria. Le dimensioni degli argomenti devono essere le stesse.

Istruzioni per l'input/output
IN Legge il valore da un porto
OUT Scrive un dato su un porto

addizione
ADD Addizione
ADC Addizione con carry
DAA Addizione con aggiustamento decimale nel caso gli operandi siano in codice BCD
INC incremento

Sottrazione
SUB Sottrazione
SBB Sottrazione con prestito
CMP Confronto
NEG Negazione (complemento a 2)
DEC Decremento
DAS Aggiustamento decimale nel caso gli operandi siano in codice BCD

Moltiplicazione
MUL Moltiplicazione
IMUL Moltiplicazione con numeri interi

Divisione
DIV Divisione con numeri naturali
IDIV Divisione con numeri interi
CBW Conversione di byte in parola (word)
CWD Conversione di word in doppia word

Istruzioni per operazioni logiche
AND And logico
NOT Not logico o complemento a 1
OR Or logico o inclusivo
TEST Confronto logico
XOR Or esclusivo

Istruzioni per operazioni di shift e rotazione
RCL Rotazione a sinistra con carry
RCR Rotazione a destra con carry
ROL Per la rotazione a sinistra
ROR Per la rotazione a destra
SAL Shift aritmetico a sinistra
SAR Shift aritmetico a destra
SHL Shift logico a sinistra
SHR Shift logico a destra

Istruzioni per le operazioni sui flag
CLC Azzera i flag di carry
CLD Azzera i flag di direzione
CLI Azzera i flag di interrupt
CMC Complemento del flag di carry
STC Setta il flag di carry
STD Setta i flag di direzione
STI Setta i flag di interrupt

Istruzioni per le operazioni di controllo dei trasferimenti
CALL Chiamata ad una procedura
JMP Salto
RET Ritorno da una procedura

Nei controlli con trasferimento condizionato, l’esecuzione del salto è determinato dal risultato di precedenti istruzioni di confronto oppure dal valore attuale dei registri di flag.

Trasferimenti condizionati
JA Salta se maggiore nel caso di numeri naturali
JAE Salta se maggiore o uguale con numeri naturali
JB Salta se minore nel caso di numeri naturali
JBE Salta se minore o uguale nel caso di numeri naturali
JC Salta se il carry è settato
JCXZ Salta se il registro CX è uguale a zero
JE Salta se gli operandi sono uguali
JG Salta se maggiore con i numeri interi
JGE Salta se maggiore o uguale con i numeri interi
JL Salta se minore con i numeri interi
JLE Salta se minore o uguale con i numeri interi
JNC Salta se i carry è resettato
JNE Salta se diverso
JNO Salta se non c’è overflow
JNS Salta se il segno è positivo
JO Salta se c’è overflow
JS Salta se il segno è negativo
JPE Salta se la parità è pari
JPO Salta se la parità è dispari

Istruzioni per il controllo dei cicli
LOOP Esegue un ciclo
LOOPNE Esegue un ciclo se uguale
LOOPNE Esegue un ciclo se non è uguale

Modi di indirizzamento

        

I modi di indirizzamento usati per i microprocessori della classe x86 sono:

1 Indirizzamento immediato

L'istruzione contiene il valore per cui il sorgente è un valore costante. Ad es.

ADD AL,7

significa addizionare 7 al valore del registro AL

CMP BL, 55

confronta il contenuto del registro BL con 55.

La costanti possono essere essere espresse in una delle seguenti quattro basi: decimale, binario, ottale ed esadecimale.

Se si vuole esprimere una costante in una base diversa dalla base 10, basta aggiungere alla costante

il suffisso B se la base è binaria, ad es. 1101B
O se la base è ottale ad es. 232O
H se la costante è esadecimale, ad esempio 7FH

2 Indirizzamento con registro

In questo caso gli operandi sono contenuti in specifici registri e, nel caso dei registri accumulatori, possiamo usare sia la lunghezza word (16bit) che la lunghezza byte (8bit) chiaramente sia l'operando sorgente che l'operando destinazione devonop avere la stessa dimensione. Ad es.

MOV AX, BX
ADD DL, AL
CMP AX, DX

con l'istruzione  MOV CX,BH che è sbagliata perché i due registri non hanno la stessa dimensione.

3 Indirizzamento diretto (o assoluto)

Uno dei due operandi si trova nel segmento dato ed identificato da un etichetta. Per esempio:

MOV AX, pippo
MOV pluto, AL
MOV pippo, 5

L' istruzione  MOV 5, AL  è' errata, perché l' operando di destinazione non può essere una costante; anche l' istruzione

MOV marco, andrea

è errata perché i due operandi non possono essere entrambi locazione di memoria.

4 Indirizzamento indiretto

L' operando si trova ad un indirizzo puntato dai registri base o indice quali: BP, BX, SI, BI Per esempio nell' istruzione

MOV CX, [DI]

Se non si vuole utilizzare la regola di default, occore esplicitare anche il segmento (regola del segment override). Per esempio nell' istruzione

MOV AX, DS: [BP]

Il contenuto del registro BP viene sommato al contenuto del registro del segmento DS, anziché a SS come imporrebbe il default.
Quando si usa questo metodo di indirizzamento, nel caso in cui non si possa ricavare dall' istruzione le dimensioni dell' operando, si verifca una situazione di errore.
Per esempio, nell' istruzione INC [SI] non si può ricavare dall'istruzione stessa la dimensione della locazione di memoria che deve essere incrementata; lo stesso problema si ha con l' istruzione MOV [BX], 5.
Per risolvere il problema occorre comunicare al processo un informazione aggiuntiva, nel seguente modo:

INC word ptr [SI]      se la variabile ha dimensione word, oppure

MOV byte ptr [BX], 5            se la variabile puntata ha dimensione byte.

Altri prefissi sono dword ptr se si punta alla doppia word , oppure qword ptr se si punta alla quadrupla word.
Quindi quando non si può ricavare dall' istruzione la dimensione degli operandi, questa deve essere esplicita.
Una variante al metodo precedente consiste nell' aggiungere o togliere un valore costante. Per esempio

MOV AX [SI + 5]     oppure    MOV, [DI - 2]

al valore contenuto nel registro viene aggiunto o tolto il valore costante indicato, prima di addizionarlo al segmento moltiplicato per 16.

5  Indirizzamento indicizzato

Nell' indirizzamento indicizzato vengono usati i registri SI e DI. In questo caso il registro rappresenta l' offset. Per esempio, nell' istruzione

MOV AX, T [SI]

La variabile T deve essere immaginata come un array ed è la base del blocco di partenza, mentre SI è l'offset.

Se SI contiene il valore 2, viene referenziata la locazione etichettata con T + 2.
Anche in questo caso valgono le regole indicate nel punto precedente, con la possibilità di aggiungere o togliere un valore costante al contenuto del registro.

6 Indirizzamento con base ed indice

In questo tipo di indirizzamento l'indirizzo richiesto viene indicato tramite un registro (BX o BP) un registro indice (SI o DI) ed uno spiazzamento opzionale, come ad es.

MOV [AX,[BX+SI+costante]]


Modelli di memoria

        

Nei sistemi Intel, non è consentito all'utente di decidere in quale zona di memoria allocare il programma, in essi, la gestione della memoria segmentata l'utente decide il numero ed il tipo di segmenti da usare, l'utente stesso, deve esplicitamente richiedere la memoria al modulo che effettua il linking, indicando il modello di memoria da usare. I modelli di memoria consentiti sono.

.TINY L'utente deve organizzare il programma mettendo in un unico segmento sia i dati sia il codice: di conseguenza, la dimensione massima di un programma è uguale alla massima dimensione di un segmento, cioè 64K. Questo è l'unico modello di memoria che consente di creare i file con estensione .com.

.SMALL L'utente deve organizzare il programma utilizzando due segmenti: uno per il codice e uno per i dati.

.COMPACT Il programma è organizzato con un segmento per il codice e più segmenti per i dati.

.MEDIUM Il programma è organizzato con un solo segmento per i dati e più segmenti per il codice.

.LARGE Il programma è organizzato con più segmenti per i dati e più segmenti per il codice, ma non è consentito definire variabili che abbiano una dimensione pari a 64K

.HUGE Il programma è organizzato con più segmenti per i dati e più segmenti per il codice ed è consentito definire variabili che abbiano una dimensione pari a 64K.

Per quelle che sono le nostre esigenze, in tutti i programmi che seguono utilizzeremo il modello .SMALL. La truttura del nostro programma assembler 8086 avrà di solito il seguente schema

Tutti i programmi sono stati testati con il compilatore tasm. allegato in questo file zip. Quasi sempre ricorrono le seguenti direttive:

.STACK [dimensione]
Questa direttiva definisce il segmento di stack, indicando al posto dell'argomento "dimensione" un numero che rappresenta la quantità desiderata, espressa in byte; la massima dimensione nei sistemi MS_DOS è 64K. Il valore della dimensione viene specificato ad esempio come
.STACK 100H
che richiede uno stack segment di 256 locazioni (100 in esadecimale). Si tratta di una direttiva non obbligatoria ma è opportuno che ci sia soprattutto quando si usano le istruzioni che utilizzano il segmento di stack (PUSH,POP, indirizzamenti indicizzati o indiretti che usano il registro BP)

.DATA
Questa direttiva definisce il primo segmento per i dati . La dimensione è calcolata automaticamente come totale dello spazio usato da tutte le variabili.

.CODE
La direttiva .CODE definisce il primo segmento per il codice eseguibile. La dimensione viene calcolata automaticamente come totale dello spazio usato dalle singole istruzioni.

Solitamente (di default) non si impone di indicare nelle istruzioni gli indirizzi in modo completo Segmento:Offset, ma solamente l'offset, occorre a tal proposito inizializzare il registro DS con l'indirizzo di partenza assegnato al segmento dati.
Questo valore è deciso dal sistema al momento del caricamento e viene messo nella variabile d'ambiente @DATA. Poiché non è possibile passare direttamente il contenuto della variabile @DATA al registro DS (in quanto non è disponibile l'istruzione per fare questo), occorre usare un altro registro di appoggio per il quale questa assegnazione è possibile, normalmente il registro AX tramite la seguente coppia di istruzioni.
MOV AX,@DATA
MOV DS,AX

Le due istruzioni suddette servono per inizializzare correttamente il registro di segmento DS, in modo da usare nel codice, come indirizzi, solo gli offset.
Per terminare il programma, useremo invece le due seguenti istruzioni
MOV AX,4C00H
INT 21H

L'effetto della prima istruzione è quello di mettere la costante esadecimale 4C nel registro AH e la costante 00 in AL. La secona istruzione INT 21H richiede uno dei servizi del DOS: il 4C (in ogni chiamata al DOS il numero del servizio neve essere contenuto in AH) perché è quello che restituisce il controllo al sistema operativo. In AL viene messo il codice di errore: il valore zero indica che tutto si è svolto correttamente.
Seguirà la pseudo-istruzione END che rappresenta la fine fisica del programma ; comanda, infatti all'assemblatore di terminare la fase di assemblaggio.

TITLE X
.model small
.stack
.data messaggio db "Ciao a tutti sto imparando l'assembly","$"
.code
mov ax,@DATA
mov ds,ax
mov ah,09
lea dx,messaggio
int 21h
mov ax,4c00h
int 21h
end

viene messo il valore 9 nel registro AX che predispone il processore alla stampa a video di una stringa.
La stringa da stampare viene posta nel registro DX attraverso l'istruzione
LEA DX,MESSAGGIO
L'interrupt 21H si occupa di attuare l'operazione di stampa. Come si vede, l'assembly, al contrario di certi linguaggi ad alto livello, non è case-sensitive, cioè, è indifferente alle maiuscole/minuscole. Importante è invece l'operazione di concatenamento della stringa in output con il carattere $ indicatore della terminazione della stringa stessa.
Dopo aver salvato il codice in un file di testo con nome ed estensione x.asm nella cartella TASM dove sono contenuti il files del compilatore, assemblare il programma dalla riga di comando con
C:\TASM>tasm x
linkare il programma col comando:
C:\TASM>tlink x
eseguire il programma digitando:
C:\TASM>x

Se invece di una intera stringa vogliamo stampare a video un singolo carattere, occorrerà usare il metodo illustrato nel seguente programma
TITLE Y
.model small
.stack
.data
.code
mov ax,@DATA
mov ds,ax
mov dl,66
mov ah,02
int 21h
mov ax,4c00h
int 21h
end

Per stampare a video il contenuto di un registro basta inserire il contenuto da stampare nel registro DL , settare a 2 il valore di AH e invocare l'interruzione 21.
MOV AH,2
MOV DL,66
INT 21H

In questo caso viene stampato a video il carattere 'B', che corrisponde al 66° elemento del codice ASCII.
Ogni registro ha due parti, ad esempio, AX ha una parte bassa AL e una parte alta AH. Sia AL che AH sono costituiti da 8 bit e possono dunque computare fino a 256=28 .
Editare e salvare il seguente file col nome y.asm.
C:\TASM>tasm y
linkare il programma col comando:
C:\TASM>tlink y
eseguire il programma digitando:
C:\TASM>y

Se invece di stampare un carattere, vogliamo stampare un numero (una cifra) possiamo usare l'accorgimento che si vede nel seguente programma
TITLE Z
.model small
.stack
.data
op db 3
.code
mov ax,@DATA
mov ds,ax
mov dl,op
add dl,48
mov ah,02
int 21h
mov ax,4c00h
int 21h
end

L'operando op=3 viene messo nel registro DL; allo stesso registro viene poi aggiunto 48 (48 è il codice ASCII che rappresenta lo 0 a cui seguono gli altri numeri fino al 9). In questo modo, il programma, invece di stampare il 3° carattere del codice ASCII stamperà il 3. Possiamo poi compilare e linkare il programma scritto come visto negli esempi precedenti.


Compilatore

        

Come abbiamo visto, per sviluppare programmi scritti in codice assembler sono necessari alcuni strumenti software essenziali: l'editor di testo, il programma assemblatore, il linker.

L'editor di testi o text editor è un programma che permette di scrivere un documento costituito esclusivamente da testo; l'editor di testi più comunemente usato per programmare sui sistemi Windows è il blocco note (Noteblock). Alla fine della scrittura del codice sorgente in linguaggio assembler il documento realizzato deve essere salvato con estensione .asm. Nel programma sorgente ogni carattere viene codificato nel codice macchina secondo il set di caratteri ASCII.

Come assemblatore può essere usato tipicamente MASM (Microsoft Macro Assembler) oppure, come usiamo noi, il TASM (Turbo Assembler) .L'assemblatore è' un programma di traduzione di procedure scritte in linguaggio assembly : esso assegna alle frasi sintatticamente corretto il rispettivo codice macchina.l'input al programma è costituito dalla procedura dell'utente scritta in linguaggio assemblativo (sorgente)con l'impiego di un editor di testo e memorizzato sul disco in un file .asm.

Il programma assemblatore traduce sia i dati che le istruzioni del programma sorgente come un unico insieme di dati,detto data set di input,e fornisce in uscita il data set di output scritto in codice oggetto. Fisicamente,ad una lista di byte del programma sorgente corrisponde in uscita dal programma assemblatore, una diversa lista di byte del programma oggetto (File.obj).

L'assemblatore, fa uso di una variabile chiamata termine location counter utilizzata per determinare gli indirizzi occupati dai singoli byte del data set di output.Il valore iniziale di questo parametro di solito è 0.
Nella prima passata il programma assemblatore genera una tabella che mette in corrispondenza i nomi simbolici assegnati alle etichette con il valore corrente del location counter.Questa tabella,detta symbol table,contiene anche i valori attribuiti ai nomi simbolici posti in ciascun campo operando delle istruzioni e definite da opportune pseudo-istruzioni.
La seconda passata si esaurisce semplicemente assegnando ai nomi simbolici i valori ottenuti dalla symbol table.L'assemblatore deve essere in grado di fornire messaggi di errore nel caso non venga rispettata la sintassi. In generale se un programma non ha errori di sintassi non significa necessariamente che "giri",ma semplicemente che la sintassi è corretta.


Linker

        

L'assemblatore generalmente traduce il programma sorgente calcolando gli indirizzi a partire dall'indirizzo 0. Il programma oggetto può quindi essere eseguito correttamente solo se viene allocato in memoria a partire proprio dalla locazione di indirizzo 0.
Questo in genere non è possibile in quanto le prime locazioni vengono utilizzate dal sistema operativo stesso e il programma utente è allocato in memoria centrale solo a partire da un altro indirizzo. Indichiamo con IBR l'indirizzo della locazione di memoria a partire dalla quale il programma è allocato in memoria per la sua esecuzione.Affinchè il programma possa essere eseguito correttamente tutti gli indirizzi che compaiono all'interno del programma oggetto devono essere incrementati della quantità IBR.
L'indirizzo IBR si chiama indirizzo base di rilocazione.Queste operazioni di rcalcolo sugli indirizzi sono svolte dal programma linker(collegatore).Oltre a questo il linker costruisce un programma unico a partire dai programmi oggetto separati (che possono essere procedure esterne e librerie)prodotti dall'assemblatore,completando, con l'aiuto delle informazioni che si trovano nel programma sorgente stesso,i riferimenti che erano rimasti in sospeso.Esso giustappone il programma, le eventuali routine esterne e le librerie una di seguito all'altro;inoltre,dove necessario, trasforma gli indirizzi in modo da tenere conto di questo concatenamento.