Home --> Assembly 3

Lezione 3 - numerazioni / operazioni matematiche



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
0*2^0 + 1*2^1 + 0*2^2 + 1*2^3 = 0 + 2 + 0 + 8 = 10
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:
6BC8 = 6*16^3 + 11*16^2 + 12*16^1 + 8*16^0 = 27592
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.
1000 / 256 = 3		; resto = 232
232 / 16 = 14 = E(hex)	; resto = 8
8 / 1 = 8		; resto = 0
Risultato: 1000=3E8(hex).
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:
C:\PROGRA~2\A86>debug lezione3.com
-u
0CC3:0100 B00A          MOV     AL,0A
0CC3:0102 2C05          SUB     AL,05
0CC3:0104 B105          MOV     CL,05
0CC3:0106 F6E1          MUL     CL
0CC3:0108 02C1          ADD     AL,CL
0CC3:010A F6361601      DIV     BYTE PTR [0116]
0CC3:010E E80600        CALL    0117
0CC3:0111 B8004C        MOV     AX,4C00
0CC3:0114 CD21          INT     21
0CC3:0116 06            PUSH    ES
0CC3:0117 8AD4          MOV     DL,AH
0CC3:0119 C0            DB      C0
0CC3:011A EA045080C2    JMP     C280:5004
0CC3:011F 3080FA39      XOR     [BX+SI+39FA],AL
-
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 :)
<< Pagina Precedente