Salve a tutti! Come promesso, questa "puntata"
sarà meno teorica (e pallosa) di quella precedente…almeno spero! La teoria,
però, è indispensabile (quasi quanto la pratica), quindi anche stavolta non può
mancare; in particolare, in questa lezione vedremo la numerazione binaria ed
esadecimale. Ma non preoccupatevi: facciamo in fretta, e poi attacchiamo subito
con un po' di codice!
NUMERAZIONE BINARIA Nella vita reale
siamo abituati ad usare la numerazione in base 10: usiamo cioè dieci cifre per
comporre i numeri. Il computer invece, nelle sue parti "più interne", conosce
solo i due valori 0 e 1, ed esegue quindi i suoi calcoli ragionando in base 2.
Quello che serve a noi, in pratica, è un modo per tradurre i numeri da base 10 a
base 2, e viceversa. Passare dalla numerazione binaria a quella decimale è
piuttosto semplice. Come al solito, partiamo con un esempio. Supponiamo di avere
il numero binario 1010. Per ottenere il numero decimale facciamo semplicemente
Voilà: 1010 (binario) vale 10 (decimale). L'operazione che abbiamo fatto è
semplicissima: è bastato prendere ogni cifra del numero binario, e moltiplicarla
per 2^n, dove n è la posizione della cifra all'interno del numero
stesso. Poi abbiamo sommato il tutto, e ci è saltato fuori 10. Potete fare la
controprova con una qualsiasi calcolatrice scientifica che supporti la
numerazione binaria (anche la calcolatrice di Windows va bene). Nel compiere
questo tipo di operazione, ricordatevi di partire dalla cifra "più a destra" del
numero (la cifra meno significativa), e di assegnarle la posizione
n = 0.
Anche il passaggio da numero decimale a numero binario non
è difficile. Supponiamo di avere il numero 50 (decimale) e di volerlo
trasformare in base 2:
50 / 2 = 25 resto ---> 0
25 / 2 = 12 resto ---> 1
12 / 2 = 6 resto ---> 0
6 / 2 = 3 resto ---> 0
3 / 2 = 1 resto ---> 1
1 / 2 = 0 resto ---> 1 ---> 110010
Ad ogni passaggio, abbiamo diviso il numero per 2, considerando il
risultato per difetto (cioè, ad esempio, dividendo 25 / 2, assumiamo come
risultato 12 e non 13). Quello che importa a noi è il resto di ogni
divisione: ogni resto costituisce una cifra del nostro numero binario.
Attenzione: bisogna considerare come cifra meno significativa il primo
resto, e come cifra più significativa l'ultimo resto.
Una nota a
margine: in qualunque assemblatore è possibile indicare i numeri in formato
binario, aggiungendo una "b" o "xB" alla fine del numero:
mov dl,00110010b ;muove 50 in DL
NUMERAZIONE ESADECIMALE La rappresentazione dei dati in base
16 è molto utile: il computer non "ragiona" in base 16, ma nel mondo assembly,
spesso, si usa indicare i numeri in esadecimale invece che in binaro. E' comodo,
infatti, usare una base di numerazione che sia una potenza di 2. Assumere
come base 16 significa usare 16 cifre: noi però ne conosciamo solo 10, di cifre.
Ce ne mancano altre 6! In questo caso, allora, si usano le prime lettere
dell'alfabeto: A,B,C,D,E,F. Naturalmente, avremo che 10 (decimale) = A (hex), 11
(decimale) = B (hex), 12 (decimale) = C (hex), 13 (decimale) = D (hex), 14
(decimale) = E (hex), 15 (decimale) = F (hex)...e 16 decimale? Beh, ora abbiamo
finito le cifre a disposizione, quindi dobbiamo riniziare: 16 decimale sarà
rappresentato da 10 hex, 17 (decimale) = 11 (hex), ecc. Anche stavolta,
quindi, cerchiamo dei procedimenti per convertire i numeri da base 16 a base 10,
e viceversa. La prima operazione è abbastanza semplice: è molto simile a
quello che facevamo con i numeri binari. Infatti, supponiamo di avere, ad
esempio, il numero esadecimale 6BC8. Ricordandoci che B=11(decimale) e
C=12(decimale), facciamo:
In pratica, invece di cifra*2^n facciamo cifra*16^n.
Ricordatevi sempre di assegnare la posizione n=0 alla cifra meno
significativa.
Vediamo ora come fare il procedimento inverso: il metodo
che vi propongo non è proprio immediato, ma secondo me è il più veloce, a patto
che si conoscano le prime potenze di 16 (16^2=256, 16^3=4096,
16^4=65536). L'operazione consiste nel prendere il numero che dovete
convertire in esadecimale, e dividerlo per la prima potenza di 16 a lui
inferiore. Il risultato della divisione costituisce una cifra del nostro numero
in base 16. Poi si prende il resto, e si continua con lo stesso
procedimento. Meglio proseguire con un esempio: supponiamo di avere
94(decimale) e di volerlo convertire in hex. La prima potenza di 16 che sta in
94 è 16^1=16. Allora, 94/16=5, con resto 14. La prima cifra del nostro numero
esadecimale sarà 5. Prendiamo il resto della divisione: 14. La prima potenza di
16 che sta in 14 è 16^0=1, e abbiamo 14/1=14. La seconda cifra del nostro numero
esadecimale sarà quindi 14(decimale)=E (bisogna scrivere le cifre in
esadecimale). Ora il processo è finito, quindi concludiamo che
94(decimale)=5E(hex).
Se non avete capito, ecco qui un altro esempio:
convertiamo 1000 in esadecimale.
Ok, concludiamo questa parte sulle numerazioni in base 2 e base 16. Il mio
consiglio è: cercate di imparare a fare queste operazioni, perchè è sempre bene
saperle, soprattutto per i numeri piccoli. In generale, comunque, usate una
calcolatrice scientifica (come quella di Windows, per esempio), che vi
semplifica la vita! ;)
QUALCHE ISTRUZIONE MATEMATICA Ok,
eccoci al codice! In questa parte impareremo qualche nuova istruzione, in
particolare: ADD, SUB, MUL, DIV. Come potete immaginare dai nomi, ADD serve per
addizionare qualcosa, SUB per sottrarre, MUL per moltiplicare e DIV per
dividere. Iniziamo dalle prime due: ADD e SUB. La sintassi di queste
istruzioni è:
add (sub) operando1, operando2
Ad esempio:
add al,bl ;(1)
sub cx,ax ;(2)
add si,6 ;(3)
sub [di],dl ;(4)
add 6,ax ;(5) ----> SBAGLIATO!!
ADD (e anche SUB) prende l'operando1, lo somma (sottrae nel caso di
SUB) all'operando2, e memorizza il risultato in operando1. E'
ovvio, quindi, che l'operando1 *deve* essere un registro (caso 1, 2) o
una locazione di memoria (caso 4), mentre l'operando2 può essere anche un
numero immediato (caso 3). Il caso 5 è sbagliato, per la ragione che vi ho detto
prima (come possiamo memorizzare il risultato di un'addizione all'interno di
un numero??? ) In generale, ricordatevi che, in assembly, il primo
operando (quello a sinistra) è quello che cambia, mentre il secondo operando (a
destra) rimane invariato.
MUL e DIV si comportano in modo diverso:
innanzitutto, prevedono un solo operando. Voi vi chiederete: come si può fare
una moltiplicazione o una divisione con un solo operando?? La risposta è:
l'altro "operando" è AX o AL. Inoltre, l'operando di MUL o DIV non può
essere un numero immediato: deve essere un registro o una locazione di
memoria. La sintassi è:
mul cl ;(1)
div byte ptr [si] ;(2) ("byte ptr" significa che vogliamo il byte che sta in [si])
mul bx ;(3)
div cx ;(4)
mul 6 ;(5) ---> sbagliato, perchè l'operando non può essere un numero immediato
Iniziamo a parlare di MUL: se il suo operando è a 8 bit (caso 1), allora
MUL moltiplica l'operando per AL e memorizza il risultato in AX. Se l'operando
invece è a 16 bit (caso 3), allora MUL moltiplica l'operando per AX e memorizza
il risultato (attenzione...) in DX:AX. Questo vuol dire che, nel secondo caso,
il risultato è a 32 bit (16 bit di DX + 16 bit di AX); scrivere DX:AX
corrisponde a dire che in AX stanno i 16 bit meno significativi del
risultato, mentre in DX stanno i 16 bit più significativi. Passiamo a
DIV: se il suo operando è a 8 bit (caso 2), allora DIV prende AX, lo divide per
l'operando a 8 bit, memorizza il risultato in AL e il resto in AH. Se l'operando
invece è a 16 bit (caso 4), allora DIV prende DX:AX (32 bit), divide il tutto
per AX, memorizza il risultato in AX e il resto in DX. Attenzione: un errore
frequente, con MUL o DIV, è dimenticarsi che, nel caso di operandi a 16 bit,
anche il registro DX viene cambiato. Se il contenuto del registro DX vi
interessa, allora dovreste salvarlo prima da qualche parte.
METTIAMO
IN PRATICA IL TUTTO Ora non dobbiamo fare altro che "provare" queste
quattro istruzioni con un programmino: allora, create un file...io l'ho nominato
"lezione3.asm". Aprite il file, e scrivete questo codice:
mov al,10 ;setta AL=10
sub al,5 ;sottrae 5 ad AL
mov cl,5 ;setta CL=5
mul cl ;moltiplica AL*CL, memorizza il contenuto in AX
add al,cl ;somma AL con CL
div byte ptr [var1] ;divide AX per il byte puntato da var1
mov ax,4c00h ;termina il programma
int 21h
var1 db 6 ;mette un 6 nella locazione di memoria
;contrassegnata da "var1".
Assemblate con "a86 lezione3.asm" e lanciate "lezione3.com"...non succede
niente! Ma, in effetti, noi abbiamo detto alla CPU di esegure certe operazioni,
non di mostrare sullo schermo il risultato. Quest'ultima operazione non è
qualcosa di "elementare": non esiste un'istruzione, e neppure un interrupt, che
mostri sullo schermo un numero. Dobbiamo scrivere noi la nostra routine!
VISUALIZZARE I NUMERI Ecco quindi una piccola procedura per
visualizzare un numero in formato esadecimale:
ShowNumber: ;label che identifica la routine "ShowNumber"
;input AX = numero da mostrare
mov dl,ah ;setta DL=AH
shr dl,4 ;esegue uno "shift" di 4 bit a destra del registro DL
push ax ;salva AX sullo stack
add dl,30h ;aggiunge 30h a DL
cmp dl,39h ;confronta DL con 39h
jb mostraCifra ;se DL<39h, salta a mostraCifra
add dl,07 ;aggiunge 7 a DL
mostraCifra:
mov ah,02h ;funzione 02h dell'int 21h
int 21h ;visualizza un carattere
pop ax ;riprende AX che aveva salvato sullo stack
shl ax,4 ;esegue uno "shift" di 4 bit a sinistra di AX
jnz ShowNumber ;se AX non è zero, salta a ShowNumber
showNumberEnd:
ret ;ritorna al codice chiamante
Questa routine ve la dò per buona, senza spiegarla...semmai più avanti
cercheremo di capirla insieme. Prima di tutto, però, vediamo brevemente cos'è
una routine e come si usa. Una routine (o procedura), in assembly, non è
altro che un pezzo di codice che sta da qualche parte, e che noi, nel
nostro programma, possiamo richiamare quando vogliamo. La prima istruzione della
routine viene identificata da una "label", una etichetta (nel nostro caso
"ShowNumber:"), che deve essere seguita dai due punti o dall'indicazione "proc".
Per chiamare una routine, basta utilizzare l'istruzione "call (label)"; al
termine di una routine, poi, bisogna ricordarsi di specificare l'istruzione
"ret" per ritornare al codice chiamante. Questa è una descrizione abbastanza
sommaria e sbrigativa, che non si addentra nel significato vero e proprio delle
istruzioni "call" e "ret"; per ora, comunque, mi interessa che teniate a mente
che una routine non è altro che un pezzo di codice che può essere
richiamato da un altro punto del programma.
Ok, ora aprite "lezione3.asm"
ed aggiungete la routine ShowNumber dopo "var1 db 6". Inoltre, prima di "mov
ax,4c00h" inserite "call ShowNumber". Se avete fatto tutto nel modo giusto,
il contenuto di "lezione3.asm" dovrebbe essere:
mov al,10 ;setta AL=10
sub al,5 ;sottrae 5 ad AL
mov cl,5 ;setta CL=5
mul cl ;moltiplica AL*CL, memorizza il contenuto in AX
add al,cl ;somma AL con CL
div byte ptr [var1] ;divide AX per il byte puntato da var1
call ShowNumber ;chiama la routine "ShowNumber"
mov ax,4c00h ;termina il programma
int 21h
var1 db 6 ;mette un 6 nella locazione di memoria
;contrassegnata da "var1".
ShowNumber: ;label che identifica la routine "ShowNumber"
;input AX = numero da mostrare
mov dl,ah ;setta DL=AH
shr dl,4 ;esegue uno "shift" di 4 bit a destra del registro DL
push ax ;salva AX sullo stack
add dl,30h ;aggiunge 30h a DL
cmp dl,39h ;confronta DL con 39h
jb mostraCifra ;se DL<39h, salta a mostraCifra
add dl,07 ;aggiunge 7 a DL
mostraCifra:
mov ah,02h ;funzione 02h dell'int 21h
int 21h ;visualizza un carattere
pop ax ;riprende AX che aveva salvato sullo stack
shl ax,4 ;esegue uno "shift" di 4 bit a sinistra di AX
jnz ShowNumber ;se AX non è zero, salta a ShowNumber
showNumberEnd:
ret ;ritorna al codice chiamante
La nostra "ShowNumber" non fa altro che prendere il numero contenuto in AX
e stamparlo sullo schermo. Prima di lanciare il programma, provate a seguire
un attimo le istruzioni: riuscite ad indovinare il numero che verrà scritto
sullo schermo? Ora lanciate "lezione3.com": avevate indovinato? (non era
difficile!)
Come avevamo fatto nella scorsa lezione, anche stavolta
proviamo ad aprire il nostro programma assemblato con il debug: digitate "debug
lezione3.com". Vedrete una schermata di questo tipo:
Riconoscete il nostro programma? Notate che "mov al,10" è tradotto dal
debug con "MOV AL,0A", perchè il debug ragiona in esadecimale. "div byte ptr
[var1]" è tradotto con "DIV BYTE PTR [0116]" (e infatti, se guardate
all'indirizzo 0116, vedete che ci sta proprio il valore 06 che noi avevamo
specificato. Il debug cerca di tradurre questo valore con un'istruzione, "PUSH
ES".... ;) ). "call ShowNumber" è tradotto con "CALL 0117" (e all'indirizzo
0117 inizia proprio la nostra routine "ShowNumber"). Quello che voglio farvi
capire, è che ogni "label" (sia che rappresenti una variabile, come var1, o che
rappresenti una routine, come ShowNumber) in realtà non è altro che un
numero, più precisamente un offset, che indica la posizione, nella memoria,
in cui si trova la variabile o la routine.
Ok, anche per questa volta
siamo giunti alla fine. Spero che abbiate capito tutto, se c'è qualcosa che non
va o che non avete capito: -albe-@libero.it. Aspettando la prossima
lezione, vi consiglio di esercitarvi un po' con le numerazioni; inoltre potreste
provare a modificare il programmino di questa lezione, facendo altre operazioni.
Ad esempio, provate a scrivere un programmino che calcoli il mod di un
numero. Alla prossima! - Albe
PS: mi sono sempre dimenticato di
dirvi che i commenti, all'interno del codice, si mettono con il punto e virgola!
Ma l'avrete sicuramente capito da soli :)