Breve panoramica sulla famiglia 80x86
La famiglia 80x06 è nata nel 1981 con il processore 8086 e prosegue attualmente con la linea
Pentium. Ciò che accomuna tutti questi processori è che sono tutti compatibili all'indietro, ma
ciascuna nuova generazione ha aggiunto via via nuove caratteristiche e maggior potenza di
calcolo rispetto ai suoi predecessori. Oggi rimangono ben pochi computer in uso, dotati di
processori 8088, 8086, 286, 386 o 486 poiché si tratta di computer lenti e superati. Anche i
computer dotati di processore Pentium della prima generazione stanno ormai diminuendo, mano
a mano che il software diviene sempre più esigente in termini di risorse e velocità. Pentium
MMX, Pentium II e Pentium III rappresentano i processori maggiormente in uso oggi.
Rappresentazione dei numeri in notazione binaria
Prima di cominciare a capire come programmare in Assembly, è opportuno cercare di
comprendere come i numeri sono rappresentati all'interno della CPU. I numero sono
rappresentati in notazione binaria, ovvero in base 2. Ci sono diversi termini che sono utilizzati
per descrivere numeri di differenti dimensioni, e sono descritti qui di seguito.
1 BIT: 0
Un bit è il tipo di dato più semplice che esiste. Può assumere valore zero oppure uno.
1 NIBBLE: 0000
4 BITS
Un nibble è quattro bit, o mezzo byte. Notate che può assumere il valore massimo di 15 (1111 =
15). Esso è la base della notazione esadecimale (base 16), che viene utilizzata per rappresentare i
numeri nei programmi Assembly. I numeri esadecimali vanno da 1 a F e sono seguiti da un h per
indicare che il numero è espresso in questa notazione. Ad esempio: Fh = 15 decimale. I numeri
esadecimali che cominciano con una lettera sono preceduti da uno 0 (zero).
1 BYTE 00000000
2 NIBBLES
8 BITS
Un byte è 8 bit o 2 nibble. Un byte ha un valore massimo di FFh (255 decimale). Poiché un byte
è 2 byte, la rappresentazione esadecimale è formata da due cifre consecutive seguite da h. Ad
esempio: 3Dh. Un byte è anche la dimensione dei registri a 8-bit, che vedremo in seguito.
1 WORD 0000000000000000
2 BYTES
4 NIBBLES
16 BITS
Una word è costituita da due byte affiancati. Una word può assumere un valore massimo di
FFFFh (65536). Poiché una word è composta da quattro nibble, è rappresentata da quattro cifre
esadecimali. Una word è anche la dimensione dei registri a 16-bit.
Registri
I registri sono locazioni di memoria all'interno della CPU dove un numero può essere
memorizzato e manipolato. Vi sono tre dimensioni di registro: 8-bit, 16-bit, e (dal 386 in poi)
32-bit. Vi sono quattro differenti tipi di registri: registri general-purpose, registri di segmento,
registri indice e registri di stack. Prima descriveremo i registri principali. I registri di stack e
segmento saranno affrontati in seguito.
Registri General-Purpose
Registri General-Purpose
Sono registri a 16-bit. Vi sono quattro registri general-purpose: AX, BX, CX, DX. Sono
suddivisi in registri a 8-bit. AX è suddiviso in AH, che contiene il byte alto e AL che contiene il
byte basso. Con il processore 386 e superiori vi sono anche registri a 32-bit. Questi hanno gli
stessi nomi dei registri a 16-bit ma con una E come prefisso (es.: EAX). Si possono utilizzare
AL, AH, AX ed EAX separatamente e considerare come registri separati per alcune oprazioni.
Se AX contenesse 24689 decimal:
AH
01100000
AL
01110001
AH varrebbe 96 e AL varrebbe 113. Se aggiungessimo 1 ad AL, questo varrebbe 114 e AH
rimarrebbe invariato. SI, DI, SP e BP possono essere utilizzati come registri general-purpose, ma
hanno usi più specifici. Questi non sono suddivisi in due metà.
Registri Indice
Questi sono a volte chiamati registri puntatore e sono registri a 16-bit. Sono utilizzati
principalmente con le istruzioni che operano con le stringhe di caratteri. Vi sono tre registri
indice: SI (source index), DI (destination index) e IP (instruction pointer). Con il processore 386
e superiori vi sono anche registri indice a 32-bit: EDI e ESI. Nota: Come registro indice per le
stringhe si può usare anche il registro BX.
IP è un registro indice ma non può essere manipolato direttamente poiché contiene l'indirizzo
dell'istruzione successiva durante l'esecuzione.
Registri di Stack
BP e SP sono registri di stack e sono utilizzati quando si ha a che fare con lo stack. Li vedremo
in seguito.
Segmenti e Offset
I primi progettisti del processore 8008 decisero che nessuno avrebbe mai avuto bisogno di
utilizzare più di un megabyte di memoria, così costruirono il chip in modo che non potesse
indirizzare memoria oltre il megabyte. Il problema è che per indirizzare un intero megabyte sono
necessari almeno 20 bit. I registri hanno solo 16 bit, ma i progettisti non ritennero di utilizzare
due registri perché credevano che lo spazio di indirizzamento che si otteneva con 32 bit fosse
esagerato per chiunque. Pertanto idearono quello che sembrava una soluzione intelligente per
risolvere il problema: segmento e offset. Ecco come viene ottenuto l'indirizzamento con due
registri, ma senza utilizzare 32 bit.
OFFSET = SEGMENT * 16
SEGMENT = OFFSET / 16 (the lower 4 bits are lost)
Un registro contiene il segmento e un altro registro contiene l'offset. Unendo insieme i due
registri si ottiene un indirizzo a 20-bit.
SEGMENT
0010010000010000---OFFSET
----0100100000100010
20-bit Address
00101000100100100010
DS contiene il segmento e SI contiene l'offset. Poiché entrambi hanno dimensione 16-bit gli
indirizzi si accavallano. Nello schema è mostrato come la coppia di indirizzi DS:SI è utilizzata
per ottenere l'indirizzo di 20 bit. La notazione standard per una coppia Segmento/Offset è
SEGMENT:OFFSET.
I registri di segmento sono CS, DS, ES, SS. Con il 386 e processori successivi esistono anche FS
e GS.
I registri di offset sono BX, DI, SI, BP, SP, IP. Con il processore 386 che opera in mpdalità
protetta, QUALSIASI registro generale può essere utilizzato come registro di offset. (Eccetto IP,
che non può essere manipolato direttamente).
Lo Stack
Poiché esistono soltanto sei registri che possono essere utilizzati per la maggior parte delle
operazioni, è naturale chiedersi come superare questa apparente limitazione. E' semplice. Esiste
una speciale area di memoria, denominata stack, in cui possono essere salvati e recuperati i
valori desiderati.
Quest'area di memoria è organizzata come una pila di piatti. L'ultimo che viene messo è il primo
ad essere prelevato. Questo tipo di organizzazione è denominata Last On First Off (LOFO)
oppure Last In First Out (LIFO).
Come lo stack è organizzato
Via via che nuovi dati sono posti nello stack, questo cresce verso il basso. Come si vede dalla
figura, lo stack ha inizio ad un indirizzo alto e cresce verso il basso. E' opportuno accertarsi di
non mettere troppi dati nello stack, se non si vuole rischiare l'errore di stack overflow.
Introduzione alle istruzioni Assembly
In Assembly sono disponibili moltissime istruzioni, ma sono soltanto una ventina quelle che
bisogna assolutamente conoscere e che vengono utilizzate più spesso. La maggior parte delle
istruzioni sono identificate da un codice mnemonico di tre caratteri e sono seguite da uno o più
operandi separati da virgole. Per esempio per mettere un dato in un registro, si può utilizzare
l'istruzione MOV.
mov
mov
mov
mov
ax,10
bx,20
cx,30
dx,40
; put 10 into ax
; put 20 into bx
; put 30 into cx
; put 40 into dx
Notate che in Assembly tutto quello che viene scritto dopo il ; (punto e virgola) viene ignorato.
Questo è utile per poter commentare il codice.
Push e Pop: Due istruzioni per utilizzare lo Stack
Abbiamo già parlato dello stack, ma non di come fare ad inserire e prelevare i dati. Ci sono due
semplici istruzioni che è necessario conoscere: PUSH e POP. Ecco la sintassi per utilizzarli:
PUSH: Inserisce un dato in cima allo stack
Sintassi:
push dato
POP: Preleva un dato dallo stack e lo memorizza in uno specifico registro o variabile.
Sintassi:
pop registro (o variabile)
Questo esempio di codice mostra come utilizzare le istruzioni PUSH e POP.
push cx
push ax
pop cx
pop ax
; put cx on the stack
; put ax on the stack
; put value from stack into cx
; put value from stack into ax
Notate che i valori di CX e AX vengono scambiati. C'è un istruzione specifica per scambiare i
valori contenuti in due registri: XCHG, che può essere usato per rimpiazzare il precedente
frammento di codice con la sola istruzione "xchg ax,cx".
Tipi di operando
Ci sono tre tipi di operando in Assembly: immediati, registro, e memoria. Un operando
immediato è un valore numerico conosciuto al momento della compilazione ed è sempre
costante. Un operando di tipo registro è un valore contenuto in un registro come ad esempio AX
o SI. Un operando in memoria è un valore che viene mantenuto e acceduto in memoria.
Some Instructions that you will need to know
Questo è un elenco di alcune importanti istruzioni che è necessario conoscere prima di poter
comprendere e scrivere programmi in Assembly.
MOV: copia un valore da una locazione ad un'altra.
Sintassi:
MOV destinazione, sorgente
Ad esempio:
mov ax,10
; moves an immediate value into ax
mov bx,cx
; moves value from cx into bx
mov dx,Number
; moves the value of Number into dx
INT: richiama una funzione DOS o del BIOS.
Sintassi:
INT interrupt number
Ad esempio:
int 21h
int 10h
; Calls DOS service
; Calls the Video BIOS interrupt
La maggior parte degli interrupt hanno più di una funzione. Questo significa che è necessario
fornire un numero specificatore alla funzione che si desidera richiamare. In genere questo
numero viene specificato in AH. Ad esempio per stampare un messaggio sullo schermo bisogna
fare così:
mov ah,9
int 21h
; subroutine number 9
; call the interrupt
Ma prima bisogna specificare cosa stampare. Questa funzione richiede che:
la coppia DS:DX contenga il puntatore a dove si trova il contenuto della stringa. La stringa deve
essere terminata con il simbolo di dollaro ($). Questo sarebbe semplice da ottenere se il registro
DS potesse essere manipolato direttamente; bisogna invece utilizzare il registro AX nel modo
seguente.
L'esempio mostra la tecnica da utilizzare:
mov dx,OFFSET Message
; DX contains offset of message
mov
mov
mov
int
ax,SEG Message
; AX contains segment of message
ds,ax
; DS:DX points to message
ah,9
; function 9 - display string
21h
; call dos service
Le direttive OFFSET e SEG servono per ottenere il segmento e l'offset dove si trova il
messaggio, e non il suo contenuto. Ora che sappiamo come scrivere il codice per visualizzare il
messaggio, non resta che dichiarare il messaggio. Nel segmento dati dichiariamo il messaggio
nel modo seguente:
Message DB "Hello World!$"
Notate che la stringa è terminata con il simbolo di dollaro. Cosa significa 'DB'? DB è un codice
mnemonico che sta per "declare byte" e il messaggio è un array di byte (un carattere ASCII
occupa un byte). I dati possono essere dichiarati di varie dimensioni: byte (DB), word (DW) e
double (DD).
Ecco alcuni esempi di dichiarazione di dati:
Number1 db ?
Number2 dw ?
Il simbolo di punto di domanda alla fine significa che il dato non è inizializzato, cioè non ha un
valore iniziale. Se si vuole specificare un valore di inizializzazione, è sufficiente scrivere:
Number1 db 0
Number2 dw 1
In questo caso Number1 assume valore 0 e Number2 è uguale a 1, quando il programma è
mandato in esecuzione.
Se si dichiara una variabile come word, non si può copiare il valore di questa variabile in un
registro a 8-bit, e allo stesso modo non si può dichiarare una variabile come byte e copiarne il
contenuto in un registro a 16-bit. Per esempio:
mov al,Number1
mov ax,Number1
; ok
; error
mov bx,Number2
mov bl,Number2
; ok
; error
Tutto quello che è necessario ricordarsi è che si può soltanto copiare byte in registri a 8-bit e
word in registri a 16-bit.
Il primo programma Assembly
Ora che abbiamo illustrato le istruzioni fondamentali e come dichiarare i dati, vediamo un
programma Assembly completo, che possa essere compilato.
Listing 1: 1STPROG.ASM
; This is a simple program which displays "Hello World!"
; on the screen.
.model small
.stack
.data
Message db "Hello World!$"
; message to be display
.code
mov dx,OFFSET Message
; offset of Message is in DX
mov ax,SEG Message
; segment of Message is in AX
mov ds,ax
; DS:DX points to string
mov
int
mov
int
ah,9
21h
ax,4c00h
21h
END start
; function 9 - display string
; call dos service
; return to dos DOS
;end here
Istruzioni per la compilazione
Queste sono le istruzioni per compilare e linkare un programma. Se avete un assemblatore
diverso da TASM o A86, consultate il vostro manuale di istruzioni.
Turbo Assembler
tasm file.asm
tlink file [/t]
L'opzione /t genera un eseguibile .COM, ed è utilizzabile solo se il modello di memoria è
dichiarato .small nel file sorgente.
A86
a86 file.asm
Questo comando compila il programma, generando un eseguibile .COM, indipendentemente dal
modello di memoria dichiarato.
Qualche semplice esempio
Il modo in cui abbiamo specificato l'indirizzo del messaggio da stampare era un po' complicato.
Servivano ben tre linee di codice, non del tutto semplici da ricordare.
mov dx,OFFSET MyMessage
mov ax,SEG MyMessage
mov ds,ax
Possiamo rimpiazzare tutto questo con una singola linea. Questo rende il codice più semplice da
leggere e da ricordare.
mov dx,OFFSET MyMessage
Perché funzioni, bisogna anche aggiungere queste istruzioni all'inizio del programma.
mov ax,@data
mov ds,ax
Nota: per l'assemblatore A86, la prima delle due linee è leggermente diversa:
mov ax,data
Questa tecnica si basa sul fatto che tutti i dati in un segmento hanno il medesimo SEG value.
Impostando questo valore in DS, evita di doverlo ricaricare ogni volta che desideriamo
indirizzare un altro dato nello stesso segmento.
Input da tastiera
Utilizzeremo l'interrupt 16h, funzione 00h per leggere la tastiera. Questa chiamata legge un dato
dal buffer della tastiera. Se nessun dato è presente, aspetta fino a quando non è disponibile.
Restutuisce lo SCAN code in AH e la traduzione ASCII in AL.
xor ah,ah
int 16h
; function 00h - get character
; interrupt 16h
Tutto quello di cui dobbiamo preoccuparci per ora è il valore ASCII che si trova in AL.
Nota: XOR effettua un OR booleano esclusivo. E' comunemente utilizzato per azzerare un
registro o una variabile.
Stampare un carattere
Il problema è che abbiamo il tasto premuto in AH. Come facciamo a visualizzarlo? Non
possiamo usare la funzione 9h perché avremmo dovuto aver già definito una stringa
terminandola con il simbolo di dollaro. Questo è invece ciò che dobbiamo fare.
; after calling function 00h of interrupt 16h
mov dl,al
mov ah,02h
int 21h
; move al (ascii code) into dl
; function 02h of interrupt 21h
; call interrupt 21h
Se desiderate salvare il valore di AH, è necessario fare PUSH di AX prima della chiamata e
recuperarlo in seguito con POP.
Controllo del flusso
In Assembly c'è un insieme di comandi per il controllo del flusso, come in molti altri linguaggi.
Prima di tutto l'istruzione fondamentale:
jmp label
Tutto quello che fa è di saltare alla locazione specificata e continuare l'esecuzione del codice da
quel punto. Per esempio:
jmp ALabel
.
.
.
ALabel:
Cosa dobbiamo fare se vogliamo valutare delle condizioni? Abbiamo letto un valore dalla
tastiera ma ora vogliamo fare qualche operazione. Ad esempio stampiamo qualcosa se questo
valore è uguale ad una certa costante. Come facciamo? E' semplice. Utilizziamo comandi di salto
condizionato.
Firstly you compare the operand with the data and then use the correct command.
cmp ax,3
je correct
; is AX = 3?
; yes
Ecco un elenco di questi comandi:
Istruzioni di salto su condizione
JA
JAE
JB
JBE
JNA
JNAE
JNB
JNBE
JZ
JE
JNZ
JNE
JC
Salta se il primo operando è maggiore del secondo operando
Come sopra, ma salta anche se gli operandi hanno lo stesso valore
Salta se il primo operando è minore del secondo operando
Come sopra, ma salta anche se gli operandi hanno lo stesso valore
Salta se il primo operando non è maggiore del secondo operando (JBE)
Salta se il primo operando non è maggiore del o uguale al secondo operando (JNB)
Salta se il primo operando non è minore del secondo operando (JAE)
Salta se il primo operando non è minore del o uguale al secondo operando (JA)
Salta se i due operandi sono uguali
Come JZ
Salta se i due operandi NON sono uguali
Come JNZ
Salta se il flag di carry è impostato
CMP: compara un registro o una variabile con un valore
Sintassi:
CMP registro o variabile, valore
jxx destination
Un esempio d'uso è il seguente:
cmp al,'Y'
je ItsYES
; compare the value in al with Y
; if it is equal then jump to ItsYES
Ogni istruzione richiede una certa quantità di spazio nel segmento codice. Si ottiene un avviso
dall'assemblatore se si cerca di saltare a locazioni lontane più di 127 byte in entrambe le
direzioni. Si può risolvere il problema, modificando una sequenza come questa:
cmp ax,10
je done
; is AX 10?
; yes, lets finish
in qualcosa come questa:
cmp ax,10
; is AX 10?
jne notdone
; no it is not
jmp done
; we are now done
notdone:
Questo risolve il problema, ma in genere è più opportuno organizzare il codice usando chiamate
a procedure.
Ora vedremo un programma che dimostra input, output e controllo del flusso.
Listing 2: PROGFLOW.ASM
; a program to demonstrate program flow and input/output
.model tiny
.code
org 100h
start:
mov dx,OFFSET Message
; display a message on the screen
mov ah,9
; using function 09h
int 21h
; of interrupt 21h
mov dx,OFFSET Prompt
; display a message on the screen
mov ah,9
; using function 09h
int 21h
; of interrupt 21h
jmp First_Time
Prompt_Again:
mov dx,OFFSET Another
; display a message on the screen
mov ah,9
; using function 09h
int 21h
; of interrupt 21h
First_Time:
mov
mov
int
xor
dx,OFFSET Again
; display a message on the screen
ah,9
; using function 09h
21h
; of interrupt 21h
ah,ah
; function 00h of
int
mov
mov
mov
16h
bl,al
dl,al
ah,02h
;
;
;
;
interrupt 16h gets a character
save to bl
move al to dl
function 02h - display character
int 21h
; call DOS service
cmp bl,'Y'
je Prompt_Again
cmp bl,'y'
je Prompt_Again
;
;
;
;
is
if
is
if
al=Y?
yes then display it again
al=y?
yes then display it again
theEnd:
mov dx,OFFSET GoodBye
; print goodbye message
mov ah,9
; using function 9
int 21h
; of interrupt 21h
mov ah,4Ch
int 21h
.DATA
CR equ 13
LF equ 10
Message
Prompt
Again
Another
GoodBye
DB
DB
DB
DB
DB
; terminate program
; enter
; line-feed
"A Simple Input/Output Program$"
CR,LF,"Here is your first prompt.$"
CR,LF,"Do you want to be prompted again? $"
CR,LF,"Here is another prompt!$"
CR,LF,"Goodbye then."
end start
Alcune istruzioni che si devono conoscere
Quello che segue è un elenco di alcuni istruzioni Assembly fondamentali che è molto importante
conoscere e che sono utilizzate molto spesso.
ADD: Aggiunge il contenuto del secondo operando al valore del primo operando
Sintassi:
ADD operando1,operando2
Questo esempio aggiunge il valore di operando2 a quello di operando1. Il risultato è
memorizzato nell'operando1. Un tipo di dato immediato non può essere utilizzato come
operando1, ma può essere utilizzato come operando2.
SUB: Sottrae il contenuto del secondo operando dal valore del primo operando
Sintassi:
SUB operand1,operand2
Questo esempio sottrae il valore di operando2 da quello di operando1. Il risultato è memorizzato
nell'operando1. Un tipo di dato immediato non può essere utilizzato come operando1, ma può
essere utilizzato come operando2.
MUL: Moltiplica due interi senza segno (sempre positivi)
IMUL: Moltiplica due interi con segno (positivi o negativi)
Sintassi:
MUL registro o variabile
IMUL registro o variabile
Questo esempio moltiplica AL o AX per il valore del registro o della variabile specificati. Se si
specifica un operando della dimensione di un byte, AL viene moltiplicato per l'operando e il
risultato è posto nel registro AX. Se invece si specifica un operando della dimensione di una
word, AX viene moltiplicato per l'operando e il risultato è posto in DX:AX.
Con i processori 386, 486 e Pentium può essere utilizzato anche il registro EAX e il risultato
viene posto in EDX:EAX.
DIV: Divide due interi senza segno (sempre positivi)
IDIV: Divide due interi con segno (positivi o negativi)
Sintassi:
DIV registro o variabile
IDIV registro o variabile
L'istruzione di divisione funziona in modo simile alla moltiplicazione, dividendo il valore
contenuto in AX per il registro o la variabile specificati. Il risultato è memorizzato in due diverse
locazioni. AL contiene il quoziente della divisione mentre il resto è posto in AH. Se l'operando è
un registro a 16-bit, il numero alla locazione DX:AX viene diviso per l'operando e il quoziente e
il resto sono posti rispettivamente in AX e DX.
Introduzione alle procedure
In Assembly una procedura è l'equivalente di una funzione in C o in Pascal. Una procedura
fornisce un modo semplice per incapsulare un calcolo complesso in modo che possa essere
utilizzato senza preoccuparsi di come funziona internamente. Con procedure attentamente
strutturate, è possibile realizzare programmi di grosse dimensioni, nascondendo i dettagli
implementativi e ottenendo codice più leggibile.
Ecco come si definisce una procedura:
PROC AProcedure
.
.
; some code to do something
.
ret
; if this is not here then your computer will crash
ENDP AProcedure
Invocare una procedura è altrettanto semplice. Tutto quello che si deve fare è quanto segue:
call AProcedure
Il programma che segue è un esempio di come usare una procedura. E' simile al primo
programma visto, tutto quello che fa è stampare la frase "Hello World!" sullo schermo.
Listing 3: SIMPPROC.ASM
; This is a simple program to demonstrate procedures. It should
; print Hello World! on the screen when ran.
.model tiny
.code
org 100h
Start:
call Display_Hi
mov ax,4C00h
int 21h
; Call the procedure
; return to DOS
Display_Hi PROC
mov dx,OFFSET HI
mov ah,9
int 21h
ret
Display_Hi ENDP
HI DB "Hello World!"
; define a message
end Start
Procedure che passano parametri
Le procedure non sarebbero così utili se non si potessero passare parametri per modificarli o
utilizzarli all'interno della procedura. Vi sono tre diversi modi di passare un parametro: in un
registro, in memoria e sullo stack.
Di seguito ci sono tre esempi che svolgono tutti lo stesso compito. Stampano il carattere ASCII
254 in un punto specifico dello schermo.
Le dimensioni (in byte) dei tre programmi una volta compilati sono rispettivamente: 38 per il
parametro passato con registro, 69 per il parametro passato in memoria e 52 per il parametro
passato sullo stack.
Passare i parametri nei registri
Il vantaggio di passare i parametri nei registri è la semplicità. Basta copiare i parametri nei
registri prima di invocare la procedura.
Listing 4: PROC1.ASM
; this a procedure to print a block on the screen using
; registers to pass parameters (cursor position of where to
; print it and colour).
.model tiny
.code
org 100h
Start:
mov
mov
mov
mov
dh,4
dl,5
al,254
bl,4
;
;
;
;
row to print character on
column to print character on
ascii value of block to display
colour to display character
call PrintChar
mov ax,4C00h
int 21h
; print our character
; terminate program
PrintChar PROC NEAR
push bx
push cx
; save registers to be destroyed
xor bh,bh
mov ah,2
int 10h
; clear bh - video page 0
; function 2 - move cursor
; row and col are already in dx
pop bx
xor
mov
mov
int
bh,bh
ah,9
cx,1
10h
pop cx
; restore bx
;
;
;
;
display page - 0
function 09h write char & attrib
display it once
call bios service
; restore registers
ret
; return to where it was called
PrintChar ENDP
end Start
Passare i parametri in memoria
Il vantaggio di questo metodo è l'estrema semplicità, ma ha lo svantaggio di rendere il
programma più ingombrante e lento.
Per passare i parametri attraverso la memoria, è sufficiente copiare i parametri in variabili situate
in memoria. Si può usare una variabile nello stesso modo in cui si usa un registro, ma bisogna
tenere conto che le istruzioni che utilizzano i registri sono molto più veloci.
Listing 5: PROC2.ASM
; this a procedure to print a block on the screen using memory
; to pass parameters (cursor position of where to print it and
; colour).
.model tiny
.code
org 100h
Start:
mov
mov
mov
mov
Row,4
; row to print character
Col,5
; column to print character on
Char,254
; ascii value of block to display
Colour,4
; colour to display character
call PrintChar
mov ax,4C00h
int 21h
; print our character
; terminate program
PrintChar PROC NEAR
push ax cx bx
xor
mov
mov
mov
int
bh,bh
ah,2
dh,Row
dl,Col
10h
mov
mov
xor
mov
mov
int
al,Char
bl,Colour
bh,bh
;
ah,9
;
cx,1
;
10h
;
; save registers to be destroyed
; clear bh - video page 0
; function 2 - move cursor
; call Bios service
pop bx cx ax
display page - 0
function 09h write char & attrib
display it once
call bios service
; restore registers
ret
; return to where it was called
PrintChar ENDP
; variables to store data
Row db
Col db
Colour db
Char db
?
?
?
?
end Start
Passare i parametri sullo stack
Questo è il modo più potente e flessibile di passare i parametri. Il problema è che risulta un poco
più complicato.
Listing 6: PROC3.ASM
; this a procedure to print a block on the screen using the
; stack to pass parameters (cursor position of where to print it
; and colour).
.model tiny
.code
org 100h
Start:
mov dh,4
;
mov dl,5
;
mov al,254 ;
mov bl,4
;
push dx ax bx
row to print string on
column to print string on
ascii value of block to display
colour to display character
; put parameters onto the stack
call PrintString ; print our string
pop bx ax dx
mov ax,4C00h
int 21h
;restore registers
;terminate program
PrintString PROC NEAR
push bp
mov bp,sp
push cx
; save bp
; put sp into bp
; save registers to be destroyed
xor
mov
mov
int
bh,bh
; clear bh - video page 0
ah,2
; function 2 - move cursor
dx,[bp+8]
; restore dx
10h
; call bios service
mov
mov
xor
mov
mov
int
ax,[bp+6]
bx,[bp+4]
bh,bh
;
ah,9
;
cx,1
;
10h
;
pop cx
pop bp
; character
; attribute
display page - 0
function 09h write char & attrib
display it once
call bios service
; restore registers
ret
; return to where it was called
PrintString ENDP
end Start
Stack di una procedura con due parametri
Per prelevare un parametro dallo stack, tutto quello che serve fare è conoscere l'indirizzo dove si
trova. L'ultimo parametro si trova all'indirizzo BP+2 e il precedente all'indirizzo BP+4
Cosa sono i "Memory Models"?
In molti esempi abbiamo utilizzato la direttiva .MODEL per specificare il modello di memoria
da utilizzare. Cosa significa?
Sintassi:
.MODEL MemoryModel
Dove MemoryModel può essere SMALL, COMPACT, MEDIUM, LARGE, HUGE, TINY o
FLAT.
Tiny
Significa che esiste un unico segmento per codice e dati. Questo tipo di programma può
essere un eseguibile .COM.
Small
Significa che per default tutto il codice si trova in un unico segmento e tutti i dati
dichiarati si trovano in un altro solo segmento. Questo implica che tutte le procedure e le
variabili sono indirizzate come NEAR utilizzando solo gli offset.
Compact
Significa che per default tutto il codice si trova in un unico segmento ma i dati possono
essere dichiarati in più segmenti diversi. Questo implica che i dati sono indirizzati
utilizzando sia il segmento sia l'offset. Gli elementi di codice (procedure) sono NEAR e
le variabili sono FAR.
Medium
E' l'opposto di compact. I dati sono NEAR e le procedure sono FAR.
Large
Questo significa che sia le procedure sia le variabili sono FAR. Bisogna indirizzarle
entrambe utilizzando sia il segmento, sia l'offset.
Flat
Questo modello di memoria si usa quando il processore opera in modalità protetta e la
memoria non è segmentata. E' utilizzabile solo con sistemi operativi a 32-bit, come
Windows 9x e Windows NT.
Macro (in Turbo Assembler)
Nota: Tutti gli esempi sono in Turbo Assembler.
Le macro sono molto utili per raggruppare sequenze di istruzioni usate frequentemente, per le
quali non è opportuno realizzare una procedura. Le macro sono sostituite al momento della
compilazione con il codice che contengono.
Questa è la sintassi per definire una macro:
Name_of_macro macro
;
;
;
endm
a sequence of instructions
Questi due esempi illustrano le macro per salvare e ripristinare di gruppi di registri:
SaveRegs macro
push
push
push
push
ax
bx
cx
dx
endm
RestoreRegs macro
pop
pop
pop
pop
dx
cx
bx
ax
endm
Notate che i registri sono prelevati in ordine inverso a quello con cui sono stati salvati nello
stack. Per usare una macro in un programma è sufficiente utilizzare il nome della macro come
una qualsiasi istruzione.
SaveRegs
; some other instructions
RestoreRegs
Questo esempio mostra come si può usare una macro per risparmiare righe di codice. Questa
macro stampa semplicemente un messaggio sullo schermo.
OutMsg macro SomeText
local PrintMe,SkipData
jmp SkipData
PrintMe db SomeText,'$'
SkipData:
push ax dx cs
mov dx,OFFSET cs:PrintMe
mov ah,9
int 21h
pop cs dx ax
endm
L'unico problema con le macro è che usandole troppo spesso, la dimensione del codice del
programma tende ad aumentare e si rischia di incontrare errori di duplicazione delle label e di
nomi delle variabili. Il modo corretto di risolvere questo problema è usare la direttiva LOCAL
che consente di dichiarare nomi che valgono solo all'inerno delle macro.
Sintassi:
LOCAL name
Dove name è il nome di una variabile locale o di una label.
Macro con parametri
Un'altra utile proprietà delle macro è che possono avere parametri. Il numero di parametri è
limitato solo dalla lunghezza della linea di codice.
Sintassi:
Name_of_Macro macro par1,par2,par3
;
; commands go here
;
endm
Questo esempio somma il primo e il secondo parametro e pone il risultato nel terzo:
AddMacro macro num1,num2,result
push ax
; save ax from being destroyed
mov ax,num1
; put num1 into ax
add ax,num2
; add num2 to it
mov result,ax
; move answer into result
pop ax
; restore ax
endm
Istruzioni per le stringhe
In Assembly vi sono diverse utili istruzioni per operare con le stringhe. Segue l'elenco delle
istruzioni e la sintassi per utilizzarle:
MOV* Move String: copia un byte, una word o una double word dall'indirizzo DS:SI
all'indirizzo ES:DI
Sintassi:
movsb
movsw
movsd
; move byte
; move word
; move double word
CMPS* Compare string: confronta un byte, una word o una double word all'indirizzo DS:SI con
un byte, una word o una double word all'indirizzo ES:DI
Sintassi:
cmpsb
cmpsw
; compare byte
; compare word
cmpsd
; compare double word
Nota: Questa istruzione è normalmente utilizzata con il prefisso REP.
SCAS* Search string: cerca il valore in AL, AX o EAX nella stringa all'indirizzo ES:DI
Sintassi:
scasb
scasw
scasd
; search for AL
; search for AX
; search for EAX
Nota: Questa istruzione è normalmente utilizzata con il prefisso REPZ o REPNZ.
REP Prefisso utilizzabile davanti ad un'istruzione per le stringhe per ripetere l'istruzione tante
volte quanto specificato nel registro CX.
Sintassi:
rep StringInstruction
STOS* Copia un byte, una word o una double word da AL, AX o EAX all'indirizzo ES:DI
Sintassi:
stosb
stosw
stosd
; move AL into ES:DI
; move AX into ES:DI
; move EAX into ES:DI
LODS* Copia un byte, una word o una double word dall'indirizzo ES:DI in AL, AX o EAX
Sintassi:
lodsb
lodsw
lodsd
; move ES:DI into AL
; move ES:DI into AX
; move ES:DI into EAX
L'esempio successivo mostra come usare queste istruzioni.
Listing 11: STRINGS.ASM
.model small
.stack
.code
mov
mov
mov
mov
mov
int
ax,@data
; ax points to of data segment
ds,ax
; put it into ds
es,ax
; put it in es too
ah,9
; function 9 - display string
dx,OFFSET Message1 ; ds:dx points to message
21h
; call dos function
cld
mov
mov
mov
rep
; clear direction flag
si,OFFSET String1
; make ds:si point to String1
di,OFFSET String2
; make es:di point to String2
cx,18
; length of strings
movsb
; copy string1 into string2
mov ah,9
; function 9 - display string
mov dx,OFFSET Message2 ; ds:dx points to message
int 21h
; call dos function
mov dx,OFFSET String1
; display String1
int 21h
; call DOS service
mov dx,OFFSET Message3 ; ds:dx points to message
int 21h
; call dos function
mov dx,OFFSET String2
; display String2
int 21h
; call DOS service
mov si,OFFSET Diff1
; make ds:si point to Diff1
mov di,OFFSET Diff2
; make es:di point to Diff2
mov cx,39
; length of strings
repz cmpsb
; compare strings
jnz Not_Equal
; jump if they are not the same
mov ah,9
; function 9 - display string
mov dx,OFFSET Message4 ; ds:dx points to message
int 21h
; call dos function
jmp Next_Operation
Not_Equal:
mov ah,9
; function 9 - display string
mov dx,OFFSET Message5 ; ds:dx points to message
int 21h
; call dos function
Next_Operation:
mov di,OFFSET SearchString
; make es:di point to string
mov cx,36
; length of string
mov al,'H'
; character to search for
repne scasb
; find first match
jnz Not_Found
mov
mov
int
jmp
ah,9
; function 9 - display string
dx,OFFSET Message6 ; ds:dx points to message
21h
; call dos function
Lodsb_Example
Not_Found:
mov ah,9
; function 9 - display string
mov dx,OFFSET Message7 ; ds:dx points to message
int 21h
; call dos function
Lodsb_Example:
mov ah,9
; function 9 - display string
mov dx,OFFSET NewLine
; ds:dx points to message
int 21h
; call dos function
mov
mov
xor
mov
cx,17
; length of string
si,OFFSET Message
; DS:SI - address of string
bh,bh
; video page - 0
ah,0Eh
; function 0Eh - write character
NextChar:
lodsb
int 10h
; AL = next character in string
; call BIOS service
loop NextChar
mov ax,4C00h
int 21h
; return to DOS
.data
CR equ 13
LF equ 10
NewLine db CR,LF,"$"
String1 db "This is a string!$"
String2 db 18 dup(0)
Diff1
db "This string is nearly the same as Diff2$"
Diff2
db "This string is nearly the same as Diff1$"
Equal1
db "The strings are equal$"
Equal2
db "The strings are not equal$"
Message db "This is a message"
SearchString db "1293ijdkfjiu938uHello983fjkfjsi98934$"
Message1
Message2
Message3
Message4
Message5
Message6
Message7
db
db
db
db
db
db
db
"Using String instructions example program.$"
CR,LF,"String1 is now: $"
CR,LF,"String2 is now: $"
CR,LF,"Strings are equal!$"
CR,LF,"Strings are not equal!$"
CR,LF,"Character was found.$"
CR,LF,"Character was not found.$"
end
Scoprire la versione del DOS
In molti programmi è necessario scoprire la versione del DOS. Questo perché si sta utilizzando
una funzione DOS che è disponibile soltanto da una certa versione in poi.
Per conoscere la versione è sufficiente fare come segue:
mov ah,30h
int 21h
; function 30h - get MS-DOS version
; call DOS function
Questa funzione restituisce il major number di versione in AL e il minor number in AH. Per
esempio se la versione fosse 4.01, AL varrebbe 4 e AH varrebbe 01. Il problema è che dal DOS 5
in poi, il comando SETVER può modificare la versione che deve essere restituita. Il modo per
aggirare il problema è il seguente.
mov ah,33h
mov al,06h
int 21h
; function 33h - actual DOS version
; subfunction 06h
; call interrupt 21h
Questo frammento di codice restituisce la versione corrente del DOS, indipendentemente dal
fatto che sia stato utilizzato SETVER per modificare il numero di versione. La major version
viene posta in BL e la minor version in BH. Ovviamente funziona solo con le versioni del DOS
dalla 5 in poi.
Push e Pop multipli
Si possono inserire e prelevare dallo stack più di un registro con una sola linea di codice, sia con
TASM, sia con A86. Questo rende il codice più semplice da comprendere.
push ax bx cx dx
pop dx cx bx ax
; save registers
; restore registers
Quando TASM e A86 compilano queste linee, le traducono in istruzioni di PUSH e POP
separate. Questa notazione server soltanto per maggior concisione e comprensibilità.
Nota: Perché le istruzioni mostrate sopra siano compilate correttamente da A86, bisogna separare
i registri con una virgola (,).
Le istruzioni PUSHA/PUSHAD e POPA/POPAD
PUSHA è un'utile istruzione che inserisce nello stack tutti i registri general-purpose. In pratica
PUSHA corrisponde alle seguenti istruzioni:
temp
push
push
push
push
push
push
push
push
= SP
ax
cx
dx
bx
temp
bp
si
di
Il vantaggio principale è di essere un comando molto conciso. Inoltre viene eseguito molto più
velocemente delle corrispettive istruzioni. POPA esegue la funzione inversa, prelevando tutti
questi registri dallo stack. PUSHAD e POPAD svolgono le stesse funzioni di PUSHA e POPA,
ma utilizzando i registri a 32-bit: ESP, EAX, ECX, EDX, EBX, EBP, ESI e EDI.
Usare lo Shift per moltiplicazione e divisione
Le istruzioni MUL e DIV sono molto lente e dovrebbero essere utilizzate solo quando la velocità
non è una richiesta. Per effettuare moltiplicazioni e divisioni più velocemente si possono
utilizzare gli operatori di shift a destra o a sinistra. Ogni operazione di shift moltiplica o divide
per una potenza di 2. Funziona allo stesso modo degli operatori << e >> del linguaggio C.
Ci sono quattro differenti modi di utilizzare l'istruzione di shift.
SHL Moltiplica per due un operando senza segno
SHR Divide per due un operando senza segno
SAR Divide per due un operando con segno
SAL Moltiplica per due un operando con segno
La sintassi è identica per tutti e quattro le modalità:
SHL operando1,operando2
Nota: Con il processore 8086 operando2 può assumere solo il valore 1. Con il processore
286/386 il valore di operando2 deve essere minore o uguale di 31.
Cicli
Usare l'istruzione Loop è preferibile rispetto ad utilizzare un JMP con label. Si imposta il numero
di ripetizioni da effettuare nel registro CX ed ogni volta che il programma incontra l'istruzione
Loop, CX viene automaticamente decrementato e viene fatto un salto alla label specificata come
argomento. Il salto deve essere limitato a 127 byte in avanti o 128 byte all'indietro.
Sintassi:
mov cx,100
Label:
.
.
.
Loop Label:
; 100 times to loop
; decrement CX and loop to Label
Questo produce esattamente lo stesso risultato del codice seguente:
mov cx,100
Label:
dec cx
jnz Label
; 100 times to loop
; CX = CX-1
; continue until done
Quale ritenete più comprensibile? Utilizzare DEC/JNZ risulta essere più veloce dal 486 in poi ed
è utile quando non è possibile utilizzare il registro CX.
L'istruzione JNZ salta se il flag zero non è impostato. Azzerando il registro CX, il flag zero viene
impostato.
Come usare un debugger
E' tempo di utilizzare un debugger per capire cosa un programma sta effettivamente facendo. A
questo scopo si può usare il Turbo Debugger. Ma prima abbiamo bisogno di un programma su
cui lavorare.
Listing 12: DEBUG.ASM
; example program to demonstrate how to use a debugger
.model tiny
.code
org 100h
start:
push ax
push bx
push cx
; save value of ax
; save value of bx
; save value of cx
mov ax,10
mov bx,20
mov cx,3
; first parameter is 10
; second parameter is 20
; third parameter is 3
Call ChangeNumbers
pop cx
pop bx
pop ax
; restore cx
; restore bx
; restore dx
mov ax,4C00h
int 21h
; exit to dos
ChangeNumbers PROC
add ax,bx
; adds number in bx to ax
mul cx
; multiply ax by cx
mov dx,ax
; return answer in dx
ret
ChangeNumbers ENDP
end start
Ora compiliamolo come eseguibile .COM e poi digitiamo:
td name of file
Il Turbo Debugger viene eseguito, e vengono mostrate le istruzioni che compongono il nostro
programma.
Per esempio le prime linee del programma vengono mostrate come:
cs:0000 50
cs:0001 53
cs:0002 51
push ax
push bx
push cx
Alcune combinazioni di tasti per il Turbo Debugger:
F5
Size Window
F7
Next Instruction
F9
Run
ALT F4
Step Backwards
I numeri che vengono copiato nei registri sono differenti da quelli del codice sorgente perché
sono rappresentati in forma esadecimale (base 16) poiché questa notazione è semplice da
convertire in forma binaria, ed è anche più concisa e comprensibile.
Sulla destra dello schermo c'è una finestra che mostra i valori contenuti dei registri. In questo
momento tutti i registri principali sono vuoti. Premendo F7 per eseguire la prima linea del
programma. Poiché la prima linea inserisce il valore del registro AX nello stack, si vede che lo
stack pointer (SP) viene modificato. Continuate a premere F7 fino ad arrivare alla linea mov
ax,000A, e oltrepassatela.
Ora, guardando nella finestra che mostra il contenuto dei registri, si vede che AX contiene A.
Procedendo passo passo, BX assume il valore 14 e CX assume il valore 3. Poi AX assume il
valore 1E che è A+14, e così via istruzione dopo istruzione. Infine CX, BX e AX sono impostati
al loro valore iniziale di zero.
Altre operazioni di output in modalità testo
Vi sono altri modi per ottenere l'output in modalità testo. Questo primo programma è un esempio
di come muovere il cursore per visualizzare una stringa di testo in una posizione a piacere.
Listing 12: TEXT1.ASM
.model tiny
.code
org 100h
start:
mov
mov
mov
xor
int
dh,12
dl,32
ah,02h
bh,bh
10h
mov dx,OFFSET Text
mov ah,9
int 21h
;
;
;
;
;
cursor col
cursor row
move cursor to the right place
video page 0
call bios service
; DS:DX points to message
; function 9 - display string
; all dos service
mov ax,4C00h
int 21h
; exit to dos
Text DB "This is some text$"
end start
L'esempio successivo mostra come scrivere sullo schermo utilizzando la funzione 40h
dell'interrupt 21h.
Listing 13: TEXT2.ASM
.model small
.stack
.code
mov ax,@data
mov ds,ax
; set up ds as the segment for data
mov
mov
mov
mov
int
ah,40h
;
bx,1
;
cx,17
;
dx,OFFSET Text
21h
;
function 40h - write file
handle = 1 (screen)
length of string
; DS:DX points to string
call DOS service
mov ax,4C00h
int 21h
; terminate program
.data
Text DB "This is some text"
end
Il programma che segue mostra come impostare e richiamare la funzione 13h dell'interrupt 10h "write string". Questa funzione ha la caratteristica di poter scrivere una stringa in un posto a
piacere sullo schermo con uno specifico colore. Tuttavia è complessa da impostare.
Listing 14: TEXT3.ASM
.model small
.stack
.code
mov ax,@data
mov es,ax
mov
mov
mov
xor
mov
mov
mov
mov
int
; set up ds as the segment for data
; put this in es
bp,OFFSET Text
ah,13h
;
al,01h
;
bh,bh
;
bl,5
;
cx,17
;
dh,5
;
dl,5
;
10h
;
; ES:BP points to message
function 13 - write string
attrib in bl,move cursor
video page 0
attribute - magenta
length of string
row to put string
column to put string
call BIOS service
mov ax,4C00h
int 21h
; return to DOS
.data
Text DB "This is some text"
end
Il programma successivo mostra come scrivere sullo schermo usando rep stosw per scrivere nella
memoria video.
Listing 15: TEXT4.ASM
.model small
.stack
.code
mov ax,0B800h
; segment of video buffer
mov
xor
mov
mov
mov
cld
rep
es,ax
di,di
ah,4
al,"G"
cx,4000
stosw
;
;
;
;
put this into es
clear di, ES:DI points to video memory
attribute - red
character to put there
; amount of times to put it there
; direction - forwards
; output character at ES:[DI]
mov ax,4C00h
int 21h
; return to DOS
end
Il programma successivo mostra come scrivere una stringa nella memoria video.
Listing 15: DIRECTWR.ASM
; write a string direct to video memory
.model small
.stack
.code
mov ax,@data
mov ds,ax
mov
mov
mov
mov
mov
xor
ax,0B800h
; segment of video buffer
es,ax
; put this into es
ah,3
; attribute - cyan
cx,17
; length of string to print
si,OFFSET Text
; DX:SI points to string
di,di
Wr_Char:
lodsb
mov es:[di],al
inc di
mov es:[di],ah
inc di
;
;
;
;
put next character into al
output character to video memory
move along to next column
output attribute to video memory
loop Wr_Char
; loop until done
mov ax,4C00h
int 21h
; return to DOS
.data
Text DB "This is some text"
end
Modalità 13h
La modalità 13h è disponibile soltanto sulle schede VGA, MCGA e superiori.
Verificare che la modalità 13h sia consentita
E' opportuno comunicare all'utente se il suo computer non è in grado di supportare la modalità
13h, invece di lasciare che il programma vada in crash. Ecco come fare.
Listing 16: CHECK13.ASM
.model small
.stack
.data
NoSupport db "Mode 13h is not supported on this computer."
db ,"You need either a MCGA or VGA video"
db ,"card/monitor.$"
Supported db "Mode 13h is supported on this computer.$"
.code
mov ax,@data
mov ds,ax
; set up DS to point to data segment
; use ax
call Check_Mode_13h
jc Error
; check if mode 13h is possible
; if cf=1 there is an error
mov ah,9
; function 9 - display string
mov dx,OFFSET Supported ; DS:DX points to message
int 21h
; call DOS service
jmp To_DOS
; exit to DOS
Error:
mov ah,9
; function 9 - display string
mov dx,OFFSET NoSupport ; DS:DX points to message
int 21h
; call DOS service
To_DOS:
mov ax,4C00h
int 21h
; exit to DOS
Check_Mode_13h PROC
; Returns: CF = 1 Mode 13h not possible
mov ax,1A00h
int 10h
cmp al,1Ah
je Mode_13h_OK
stc
Mode_13h_OK:
; Request video info for VGA
; Get Display Combination Code
; Is VGA or MCGA present?
; mode 13h is supported
; mode 13h isn't supported CF=1
ret
Check_Mode_13h ENDP
end
E' sufficiente includere questo codice per verificare se la modalità 13h è supportata, per essere
certi di poterla utilizzare.
Impostare la modalità video
Impostare la modalità è molto semplice. Ecco come fare.
mov ax,13h
int 10h
; set mode 13h
; call BIOS service
Una volta che ci troviamo in modalità 13h e abbiamo completato le operazioni, è necessario
re-impostare la modalità precedente.
Questo viene fatto in due fasi. Prima dobbiamo salvare la modalità video e poi re-impostarla al
valore originario.
VideoMode db ?
.
.
mov ah,0Fh
; function 0Fh - get current mode
int 10h
; Bios video service call
mov VideoMode,al ; save current mode
; program code here
mov al,VideoMode ; set previous video mode
xor ah,ah
; clear ah - set mode
int 10h
; call bios service
mov ax,4C00h
; exit to dos
int 21h
; call dos function
Ora che sappiamo come entrare in modalità 13h, facciamo qualche operazione. Per prima cosa
scriviamo alcuni pixel sullo schermo.
Funzione 0Ch: Write Graphics Pixel
Questa funzione imposta un punto colorato sullo schermo alle coordinate grafiche specificate.
INPUT:
AH = 0Ch
AL = Colore del punto
CX = Colonna dello schermo (coordinata x)
DX = Riga dello schermo (coordinata y)
OUTPUT:
Nulla eccetto il pixel sullo schermo.
Questo programma dimostra come disegnare dei punti. Disegna quattro pixel rossi nel mezzo
dello schermo.
Listing 17: PIXINT.ASM
; example of plotting pixels in mode 13 using bios services ; INT 10h
.model tiny
.code
org 100h
start:
mov ax,13
int 10h
; mode = 13h
; call bios service
mov
mov
mov
mov
int
ah,0Ch
al,4
cx,160
dx,100
10h
;
;
;
;
;
inc
int
inc
int
dec
int
dx
10h
cx
10h
dx
10h
; plot pixel downwards
; call BIOS service
; plot pixel to right
; call BIOS service
; plot pixel up
; call BIOS service
function 0Ch
color 4 - red
x position = 160
y position = 100
call BIOS service
xor ax,ax
int 16h
; function 00h - get a key
; call BIOS service
mov ax,3
int 10h
; mode = 3
; call BIOS service
mov ax,4C00h
int 21h
; exit to DOS
end start
Alcune ottimizzazioni
Questo metodo non è troppo veloce e potremmo renderlo molto più veloce. Come? Scrivendo
direttamente nella memoria video. Ciò si ottiene abbastanza facilmente.
Il segmento di memoria VGA si trova all'indirizzo 0A000h. Per conoscere l'indirizzo di ciascun
pixel, utilizzate questa semplice formula per ottenere l'offset.
Offset = X + ( Y * 320 )
Tutto quello che dobbiamo fare è scrivere un valore a questa locazione, ottenendo così un pixel.
Il numero rappresenta il colore desiderato. Ci sono due istruzioni che possiamo usare per scrivere
un pixel sullo schermo. La prima possibilità è quella di utilizzare stosb per impostare il valore
nel registro AL all'indirizzo ES:DI; la seconda possibilità è quella di sfruttare una nuova forma
dell'istruzione MOV, come segue:
mov es:[di], color
Quale sarebbe meglio utilizzare? La scelta deve essere guidata dalla velocità di esecuzione,
ovvero dal numero di cicli necessari per svolgere l'operazione. Minore è il numero di cicli, più
veloce è l'operazione.
Instruction
STOSB
MOV AL to SEG:OFF
Pentium
486
3
1
386
286
86
5
4
3
1
4
3
11
10
Se si utilizza l'istruzione MOV, bisogna ricordarsi di incrementare DI (operazione che STOSB
effettua automaticamente).
Esempi
Qui di seguito sono riportati gli esempi illustrati durante le lezioni.
Indirizzamento immediato
// INDIRIZZAMENTO IMMEDIATO
// L'operando sorgente è un valore numerico.
asm
{
// Il registro AX viene impostato a zero
XOR EAX,EAX
// Il registro AX viene caricato impostando separatamente
// la parte alta (AH) e la parte bassa (AL) con due operandi
// a 8 bit
MOV AH,00
MOV AL,04
// Il registro AX viene caricato con una singola istruzione
// utilizzando un operando a 16 bit
// (ogni numero esadecimale che inizia con una lettera deve
// essere preceduto dallo 0)
MOV AX,0FFFFH
// La dimensione in bit dell'operando sorgente viene adattata
// a quello dell'operando destinazione
MOV AX,302
// Lo stesso vale per operandi a 8 bit
MOV AL,-40
}
Indirizzamento a registro
// INDIRIZZAMENTO A REGISTRO
// L'operando sorgente è contenuto in uno dei registri.
asm
{
// Il contenuto di AX viene trasferito nel registro DX
XOR EAX,EAX
MOV AX,01234H
MOV DX,AX
// Si possono usare registri sorgente e destinazione a 8 bit
XOR EAX,EAX
MOV AL,056H
MOV DL,AL
}
Indirizzamento diretto
// INDIRIZZAMENTO DIRETTO
// L'operando sorgente viene prelevato all'indirizzo di memoria
// specificato. In genere l'operando è una label.
DWORD dwDato = 0x12345678; // dwDato DD 012345678H
WORD wDato = 0x1234;
// wDato DW 01234H
BYTE bDato = 0x12;
// bDato DB 012H
asm
{
// EAX viene caricato con il contenuto della locazione
// di memoria dwDato (32 bit)
XOR EAX,EAX
MOV EAX,dwDato
// AX viene caricato con il contenuto della locazione
// di memoria wDato (16 bit)
XOR EAX,EAX
MOV AX,wDato
// AL viene caricato con il contenuto della locazione
// di memoria bDato (8 bit)
XOR EAX,EAX
MOV AL,bDato
}
Indirizzamento indiretto con registro
// INDIRIZZAMENTO INDIRETTO CON REGISTRO
// L'operando sorgente viene prelevato all'indirizzo di memoria
// contenuto in un registro.
DWORD dwDato = 0x12345678; // dwDato DD 012345678H
WORD wDato = 0x1234;
// wDato DW 01234H
BYTE bDato = 0x12;
// bDato DB 012H
asm
{
// EAX viene caricato con il contenuto della locazione
// di memoria dwDato (32 bit)
XOR EAX,EAX
LEA EBX,dwDato
MOV EAX,[EBX]
// AX viene caricato con il contenuto della locazione
// di memoria wDato (16 bit)
XOR EAX,EAX
LEA EBX,wDato
MOV AX,[EBX]
// AL viene caricato con il contenuto della locazione
// di memoria bDato (8 bit)
XOR EAX,EAX
LEA BX,bDato
MOV AL,[EBX]
}
Indirizzamento relativo con registro base
// INDIRIZZAMENTO RELATIVO CON REGISTRO BASE
// L'indirizzo effettivo dell'operando sorgente viene determinato
// sommando uno spiazzamento al contenuto di un registro base.
char msg[] = "Ibis redibis non morieris";
// msg DB 'Ibis redibis non morieris','$'
asm
{
// Imposta AL con il secondo carattere del messaggio
XOR EAX,EAX
LEA EBX,msg
MOV AL,[EBX]+1
// Imposta AL con il terzo carattere del messaggio
XOR EAX,EAX
LEA EBX,msg
MOV AL,2[EBX]
// Imposta AL con il quarto carattere del messaggio
XOR EAX,EAX
LEA EBX,msg
MOV AL,[EBX+3]
}
Indirizzamento diretto con registro indice
// INDIRIZZAMENTO DIRETTO CON REGISTRO INDICE
// L'indirizzo dell'operando viene calcolato sommando un indirizzo
// di memoria al contenuto di un registro indice.
char msg[] = "Ibis redibis non morieris";
// msg DB 'Ibis redibis non morieris','$'
asm
{
// Imposta AL con il quarto carattere del messaggio
XOR EAX,EAX
MOV EBX,3
MOV AL,msg[EBX]
}
Indirizzamento con registri base e indice
// INDIRIZZAMENTO CON REGISTRI BASE E INDICE
// L'indirizzo dell'operando viene calcolato sommando il contenuto
// del registro base con il contenuto del registro indice.
char msg[] = "Ibis redibis non morieris";
// msg DB 'Ibis redibis non morieris','$'
asm
{
// Imposta AL con il quarto carattere del messaggio
XOR EAX,EAX
LEA EBX,msg
MOV ECX,3
MOV AL,[EBX][ECX]
}
Somma esadecimale con indirizzamento immediato
// SOMMA ESADECIMALE CON INDIRIZZAMENTO IMMEDIATO
// Somma di tre numeri utilizzando l'indirizzamento immediato.
asm
{
// Azzera EAX
SUB EAX,EAX
// Carica AL con il valore 23H
MOV AL,23H
// Somma 0AH ad AL
ADD AL,0AH
// Somma 10H ad AL
ADD AL,10H
// Il risultato è in AL
}
Sottrazione esadecimale con indirizzamento diretto
//
//
//
//
SOTTRAZIONE ESADECIMALE CON INDIRIZZAMENTO DIRETTO
I numeri da sottrarre vengono referenziati tramite il loro
indirizzo e vengono sottratti utilizzando la tecnica di
indirizzamento diretto.
WORD
WORD
WORD
WORD
NUM1 = 0x1234;
NUM2 = 0x24;
NUM3 = 0xABC;
RISULTATO;
//
//
//
//
NUM1 DW 1234H
NUM2 DW 24H
NUM3 DW 0ABCH
RISULTATO DW ?
asm
{
// Azzera EAX
SUB EAX,EAX
// Carica NUM1 nel registro AX
MOV AX,NUM1
// Sottrae NUM2 a NUM1
SUB AX,NUM2
// Sottrae NUM3 al risultato precedente
SUB AX,NUM3
// Memorizza il risultato in RISULTATO
MOV RISULTATO,AX
}
Somma in precisione multipla con indirizzamento diretto
// SOMMA IN PRECISIONE MULTIPLA CON INDIRIZZAMENTO DIRETTO
WORD
WORD
WORD
WORD
WORD
WORD
NUM1 = 0x1234;
NUM2 = 0x5678;
NUM3 = 0x9ABC;
NUM4 = 0xDEF0;
RIS_LSW;
RIS_MSW;
//
//
//
//
//
//
NUM1 DW
NUM2 DW
NUM3 DW
NUM3 DW
RIS_LSW
RIS_MSW
asm
{
// Azzera EAX e EBX
SUB EAX,EAX
1234H
5678H
9ABCH
0DEF0H
DW ?
DW ?
SUB EBX,EBX
// Carica il primo numero nel registro AX
MOV AX,NUM1
// Somma il secondo numero al primo
ADD AX,NUM2
// Se il Carry è a 1, viene sommato a BX
ADC BX,00H
// Somma il terzo numero al registro AX
ADD AX,NUM3
// Se il Carry è a 1, viene sommato a BX
ADC BX,00H
// Somma il quarto numero al registro AX
ADD AX,NUM4
// Se il Carry è a 1, viene sommato a BX
ADC BX,00H
// La somma AX viene copiata in RIS_LSW
MOV RIS_LSW,AX
// La somma BX viene copiata in RIS_MSW
MOV RIS_MSW,BX
}
Somma in precisione multipla con indirizzamento con
registro indice
// SOMMA IN PRECISIONE MULTIPLA CON INDIRIZZAMENTO CON REGISTRO INDICE
// - Una tabella in memoria contiene gli addendi.
// - Un ciclo di programma scandisce la tabella.
// - L'indirizzamento con registro indice permette di accedere
//
alle righe della tabella.
WORD TABELLA[] = {0x1234, 0x5678, // TABELLA DW 1234H, 5678H
0x9ABC, 0xDEF0, //
DW 9ABCH, 0DEF0H
0x1111, 0x2222, //
DW 1111H, 2222H
0x3333, 0x4444}; //
DW 3333H, 4444H
WORD RIS_LSW;
// RIS_LSW DW ?
WORD RIS_MSW;
// RIS_MSW DW ?
asm
{
// Azzera EAX, EBX, ECX, EDX
SUB EAX,EAX
SUB EBX,EBX
SUB ECX,ECX
SUB EDX,EDX
// Indirizzo di TABELLA in EBX
LEA EBX,TABELLA
// Indice a 0
MOV ESI,00H
// Primo numero in AX
MOV AX,TABELLA[ESI]
// Numero di righe della tabella
MOV CX,08H
ANCORA:
// Incrementa l'indice di una WORD (2 BYTE)
ADD ESI,02H
// Somma l'i-esimo elemento della tabella
ADD AX,TABELLA[ESI]
// Se il Carry = 1, viene sommato a DX
ADC DX,00H
// se CX non è 0, continua
LOOP ANCORA
// Tasferisce AX in RIS_LSW
MOV RIS_LSW,AX
// Tasferisce DX in RIS_MSW
MOV RIS_MSW,DX
}
Somma di numeri decimali con indirizzamento indiretto con
registro
// SOMMA DI NUMERI DECIMALI CON INDIRIZZAMENTO INDIRETTO CON REGISTRO
BYTE ADDENDI[] = {1,2,3,4,5,6,7,8,9,10};
// ADDENDI DB 1,2,3,4,5,6,7,8,9,10
BYTE RISULTATO; // RISULTATO DB ?
asm
{
// Azzera EAX, EBX, ECX, EDX
SUB EAX,EAX
SUB EBX,EBX
SUB ECX,ECX
SUB EDX,EDX
// Indirizzo di ADDENDI in EBX
LEA EBX,ADDENDI
// Imposta il contatore
MOV CX,10
// Primo numero in AX
MOV AL,ADDENDI
AGAIN:
// Incrementa l'indice di 1 BYTE
ADD EBX,01H
// Somma l'i-esimo addendo
ADD AL,[EBX]
// Conversione decimale della somma
DAA
// se CX non è 0, continua
LOOP AGAIN
// Copia il risultato in RISULATATO
MOV RISULTATO,AL
}
Moltiplicazione per somme ripetute
// MOLTIPLICAZIONE PER SOMME RIPETUTE
BYTE MOLTIPLICANDO = 0x2A; // MOLTIPLICANDO DB 2AH
BYTE MOLTIPLICATORE = 0x78; // MOLTIPLICATORE DB 78H
WORD PRODOTTO;
// PRODOTTO
DB ?
asm
{
// Azzera EAX, EBX, ECX, EDX
SUB EAX,EAX
SUB EBX,EBX
SUB ECX,ECX
SUB EDX,EDX
// Moltiplicatore in CL
MOV CL,MOLTIPLICATORE
SUB CL,01H
// Moltiplicando in AL
MOV AL,MOLTIPLICANDO
CICLO1:
// Somma il moltiplicando al risultato parziale
ADD AL,MOLTIPLICANDO
ADC AH,00H
// Se CL non è 0, continua
LOOP CICLO1
// Risultato finale in PRODOTTO
MOV PRODOTTO,AX
}
Moltiplicazione, elevamento al quadrato e al cubo con
l'istruzione di moltiplicazione
// MOLTIPLICAZIONE, ELEVAMENTO AL QUADRATO E AL CUBO CON
// L'ISTRUZIONE DI MOLTIPLICAZIONE
WORD NUM1 = 0x1234; // NUM1 DW 1234H
WORD NUM2 = 0x00CD; // NUM2 DW 00CDH
DWORD RIS1;
// RIS1 DD ?
DWORD RIS2;
// RIS2 DD ?
DWORD RIS3;
// RIS3 DD ?
asm
{
// Azzera EAX, EBX, ECX, EDX
SUB EAX,EAX
SUB EBX,EBX
SUB ECX,ECX
SUB EDX,EDX
// MOLTIPLICAZIONE
// Azzera il registro di overflow
SUB DX,DX
// NUM1 in AX
MOV AX,NUM1
// Moltiplicazione di NUM1 con NUM2
MUL NUM2
// AX in LSB di RIS1
MOV WORD PTR RIS1,AX
// DX in MSB di RIS1
MOV WORD PTR RIS1+2,DX
// ELEVAMENTO AL QUADRATO
// Azzera il registro di overflow
SUB DX,DX
// NUM1 in AX
MOV AX,NUM1
// NUM1 moltiplicato per se stesso
MUL NUM1
// AX in LSB di RIS2
MOV WORD PTR RIS2,AX
// DX in MSB di RIS2
MOV WORD PTR RIS2+2,DX
// ELEVAMENTO AL CUBO
// Azzera il registro di overflow
SUB DX,DX
// NUM2 in AX
MOV AX,NUM2
// AX = AX * NUM2
MUL NUM2
// AX = AX * NUM2
MUL NUM2
// AX in LSB di RIS3
MOV WORD PTR RIS3,AX
// DX in MSB di RIS3
MOV WORD PTR RIS3+2,DX
}
Divisione con variabili di tipo doubleword
// DIVISIONE CON VARIABILI DI TIPO DOUBLEWORD
DWORD
WORD
WORD
WORD
DIVIDENDO = 0x2A8B7654; // DIVIDENDO DD
DIVISORE = 0X5ABC;
// DIVISORE DW
QUOZIENTE;
// QUOZIENTE DW
RESTO;
// RESTO
DW
asm
{
// Azzera EAX, EDX
SUB EAX,EAX
SUB EDX,EDX
02A8B7654H
5ABCH
?
?
// LSB di DIVIDENDO in AX
MOV AX,WORD PTR DIVIDENDO
// MSB di DIVIDENDO in DX
MOV DX,WORD PTR DIVIDENDO+2
// (DX AX) diviso per DIVISORE
DIV DIVISORE
// AX in QUOZIENTE
MOV QUOZIENTE,AX
// DX in RESTO
MOV RESTO,DX
}
Estrazione di radice quadrata
// ESTRAZIONE DI RADICE QUADRATA
WORD NUMERO = 0x1CE4; // NUMERO DW 1CE4H
WORD RADICE;
// RADICE DW ?
asm
{
// Azzera EAX, ECX, EDX
SUB EAX,EAX
SUB ECX,ECX
SUB EDX,EDX
// Numero da sottrarre
MOV CX,01H
CICLO2:
// Sottrazione dell'i-esimo numero dispari
SUB NUMERO,CX
// Esce dal ciclo se NUMERO < 0
JL FINE
// Incrementa il valore della radice quadrata
INC DX
// Calcola il successivo numero dispari
ADD CX,02H
// Ripete il ciclo
JMP CICLO2
// Il risultato è in RADICE
FINE: MOV RADICE,DX
}
Tabella di lookup per logaritmi
//
//
//
//
TABELLA DI LOOKUP PER LOGARITMI
Il programma determina il logaritmo di un numero, dopo aver
memorizzato in forma tabellare i valori della funzione logaritmo
per un predefinito insieme di numeri.
WORD TABELLA[] = {0,3010,4771,6021,6990,7782,8451,9031,9542,10000};
// TABELLA DW 0,3010,4771,6021,6990,7782,8451,9031,9542,10000
BYTE NUMERO = 7; // NUMERO
DB 7
WORD LOGARITMO; // LOGARITMO DW ?
asm
{
// Azzera EAX, EBX, EDX
SUB EAX,EAX
SUB EBX,EBX
SUB EDX,EDX
// Indice per la ricerca
MOV AL,NUMERO
// Corregge l'indice (non esiste il logaritmo di 0)
SUB AL,1
// Moltiplica l'indice per 2 (la tabella è fatta di WORD)
ROL AL,1
// Indirizzo di TABELLA in EBX
LEA EBX,TABELLA
// Converte da BYTE a WORD
CBW
// Somma indirizzo e indice
ADD BX,AX
// Estrae il dato in tabella
MOV DX,[EBX]
// Risultato in LOGARITMO
MOV LOGARITMO,DX
}
Conversione di codici mediante tabelle di lookup
// CONVERSIONE DI CODICI MEDIANTE TABELLE DI LOOKUP
// Il programma esegue la conversione di un numero esadecimale
// nella codifica Gray equivalente.
BYTE GRAY[] = {0x2,0x3,0x7,0x5,0x4,0xC,0xD,0xF,0xE,0xA};
// GRAY DB 0010B, 0110B, 0111B, 0101B, 0100B
//
DB 1100B, 1101B, 1111B, 1110B, 1010B
BYTE VALORE = 8; // VALORE
DB 8H
BYTE RISPOSTA;
// RISPOSTA DB ?
asm
{
// Azzera EAX, EBX
SUB EAX,EAX
SUB EBX,EBX
// Indirizzo della tabella di codifica in EBX
LEA EBX,GRAY
// Numero da convertire (indice tabella) in AL
MOV AL,VALORE
// Codifica in AL
XLAT GRAY
// Risultato in RISPOSTA
MOV RISPOSTA,AL
}
Conversione da numeri ASCII a numeri esadecimali
// CONVERSIONE DA NUMERI ASCII A NUMERI ESADECIMALI
// Il programma converte un numero espresso in formato ASCII
// nel corrispondente numero in formato esadecimale.
// L'input si suppone compreso tra 30H e 46H.
// ASCII
// 30H
// 31H
// 32H
// 33H
// 34H
// 35H
// 36H
// 37H
// 38H
// 39H
// 41H
// 42H
// 43H
// 44H
// 45H
// 46H
HEX
0H
1H
2H
3H
4H
5H
6H
7H
8H
9H
AH
BH
CH
DH
EH
FH
BYTE ASCII = 0x42; // ASCII DB 42H
BYTE HEX;
// HEX
DB ?
asm
{
// Azzera EAX
SUB EAX,EAX
// Numero ASCII da convertire in AL
MOV AL,ASCII
// Conversione
SUB AL,30H
// Il numero è compreso tra 0 e 9?
CMP AL,9
// Salta se il numero è maggiore di 9
JG LETTERA
// Vai alla fine
JMP FINEPRG
// Conversione per numeri maggiori di 9
LETTERA:SUB AL,07H
// Risultato in HEX
FINEPRG:MOV HEX,AL
}