|
[La logica di presentazione degli argomenti
di questo modulo si ispira a Linguaggio Assembly per pc IBM
di Peter Norton]
Istruzioni call - ret
Se volessimo stampare un carattere per
un tot numero di volte potremo scrivere per quel numero di volte
la procedura di stapa o utilzzare una iterazione. Proviamo quest'ultima
soluzione.
|
itera:
stampa:
|
mov
dl, 41
mov cx, 000Ah
call stampa
loop itera
int 20
...
mov
ah,02
int
21
ret |
;
muoviamo il carattere 41h [ascii A] nel registro DL
; muoviamo 10 esadecimale nel registro contatore CX
; richiama la rotine stampa [la parola stampa e' una etichetta]
; torna a itera e scala da 1 da CX
; per CX=0: fine programma
;
; procedura di stampa
; ritorna all'istruzione loop |
Ecco il risultato di questo programma:
AAAAAAAAAA. Abbiamo sostituito i numeri delle righe, indicati
fino a questo momento con il termine etichette, con dei nomi:
itera e stampa. Per provare con debug si puo' continuare a fare
uso dei delle locazioni di memoria rappresentate dalle righe a
sinistra della schermata di debug.
Esaminiamo il programma: call stampa, sposta il controllo del
programma alla routine chiamata stampa. Qui viene stampato il
carattere del registro DL. Poi attraverso la istruzione ret il
controllo del programma torna all'istruzione successiva a quella
di chiamata della routine stampa. L'istruzione successiva
e' loop itera, ovvero torna ad itera e scala 1 dal registro
contatore CX. Itera e' l'istruzione call che rinvia per una seconda
volta alla rotine stampa. Tutto questo si ripete per 10 volte,
ovvero fino a quando CX=0. Quando CX=0 si esce dal loop per realizzare
l'istruzione successiva Int 20, ovvero ritorno a DOS; il programma
e' finito.
Lo stack
L'istruzione ret necessita di un indirizzo
a cui tornare dopo l'esecuzione della chiamata call. L'indirizzo
call viene quindi inserito in una speciale area di memoria chiamata
stack. Lo stack che vedremo essere utilizzato per moltissime funzioni,
memorizza indirizzi di memoria seguendo una procedura cosidetta
LIFO: Last In First Out [l'ultimo ad entrare e' il primo ad uscire].
Lo stack si comporta come il processo di sedimentazione della
terra: ogni strato si sovrappone a quello precedente e se qualcuno
preleva la terra raccoglie prima quella sedimentata per ultima.
Allo stesso modo, lo stack sedimenta indirizzi di memoria [inviati
secondo certe regole con determinate istruzioni] e il primo inirizzo
che si preleva e' quello entrato per ultimo.
La gestione del contenuto dello stack e' controllato da due registri:
SP [Stack Pointer] e SS [Stack Segment].
Nel programma appena scritto, l'indirizzo di call era l'ultimo
ad essere entrato nella pila di indirizzi dello stack. E' l'istruzione
stessa call ad occuparsi dell'invio del proprio indirizzo allo
stack, mentre l'istruzione ret preleva tale indirizzo [lo carica
nel registro IP] e quindi passa il controllo del programma alla
istruzione successiva a call.
Le chiamate call - ret posso essere anche annidate seconde questa
logica:
1 call
2 call
3 call
...
3 ret
2 ret
1 ret
L'ultima call [3] deve essere anche la prima ad essere risolta
e quindi la prima ret si riferira' ad essa. A ben vedere, questa
sequenza segue quella di accumulazione degli indirizzi nello stack.
Le operazioni di invio e recupero degli indirizzi nello stack
che abbiamo visto realizzate da call e da ret, possono essere
compiute dalle istruzioni push e pop. Push e pop estendono l'utilizzo
dello stack ad operazioni non necessariamente legate alle chiamate
delle routine.
Diciamo subito che push e pop sono istruzioni importantissime
inquanto vengo incontro all'esigenza del programmatore di possedere
un numero piu' elevato di registri di lavoro. Push e pop consentono
di utilizzare i registri in maniera locale salvando temporaneamente
il contenuto di questi cosi' da renderli disponibili per altre
operazioni in altre sezioni di un programma.
Push e' l'istruzione di inserimento di parole di dati [2 byte]
nello stack, mentre pop e' l'istruzione di prelievo di questi
dati dallo stack.
Le istruzioni push e pop posso essere anche annidate seconde questa
logica:
1 push
2 push
3 push
...
3 pop
2 pop
1 pop
ad esempio:
|
push cx
push dx
mov cx, 000Ah
call stampa
...
pop
dx
pop
cx
|
|
|
In questo caso il valore contenuto nel
registro CX [cosi' come quello contenuto in DX] vengono temporaneamente
salvati nello stack dall'istruzione push. Successivamente CX viene
caricato con 000ah. Probabilmente esso verra' decrementato in
relazione all'istruzione loop, mentre altre istruzioni interesseranno
il registro DX. Quando il programma incontra le istruzioni pop
i valori originari di CX e di DX [per CX il valore contenuto prima
di mov CX, 000A] verranno ripristinati. Come e' possibile constatare,
se queste operazioni complessificano un po' la gestione del programma,
esse decuplicano il numero dei registri a disposizione.
Procediamo con un nuovo programma; questo dovra' leggere da tastiera
un numero esadecimale a due cifre [ad esmpio 39h] e stamparlo.
Prima di procedere facciamo un piccolo riassunto di quanto finora
realizzato cosi' da avere le idee piu' chiare su cio' che andremo
a fare.
Fino ad ora abbiamo stampato un carattere esadecimele [modulo
II] osservando come int 21 funzione 02 si occupasse di trasformare
automaticamente il numero esadecimale in un carattere ascii.
In seguito [modulo IV] abbiamo preso
un numero esadecimale come F e lo abbiamo trasformato [< 9
0 +30; > 9 = + 37] in modo da stampare il carattere ascii corrispondente
al numero esadecimale [questa procedura vale per i caratteri ascii
0 - 9 e A - F].
Ora vogliamo trasformare un numero esadecimale in un carattere
ascii avendo sulla stessa riga del risultato il numero esadecimale
e la sua codifica in ascii. Si ricordi
che ogni numero digitato viene interpretato da debug come numero
esadecimale, quindi se digitiamo 5 per debug sara' 35h.
|
|
mov
ah, 01 |
|
| |
int
21 |
;
la funzione 01 per INT 21 legge un numero digitato dalla tastiera |
|
mov
dl, al |
;
l'esadecimale preso si inserisce in AL. faccio una copia di
al in dl |
| |
sub
al, 30 |
;
sottrai dal numero esadecimale messo in DL l'esadecimale 30.
ad esempio: |
| |
|
;
il numero 9 corrisponde all'esadecimale 39 a cui viene sottratto
30h. |
| |
cmp
al, 09 |
:
compara il risultato con l'esadecimale 9 |
| |
jle
shift |
;
se uguale o minore salta a shift dove il numero sara' spostato
a sinistra di 4 bit |
| |
|
;
altrimenti... significa che si tratta di una lettera [>
9] e quindi necessita di una |
| |
|
;
ulteriore sottrazione esistendo tra l'ascii 9 e l'ascii A,
7 posizioni |
| |
sub
al, 07 |
;
sottrai 7h dopo aver sottratto 30h... e poi passa alla procedura
di shl |
| shift: |
|
|
| |
mov
cl, 04 |
;
carica il contatore CL con 4h |
| |
shl
dl, cl |
;
slitta DL di 4 bit verso sinistra, quindi il numero binario
si sposta nel semibyte di |
| |
|
;
sinistra lasciando a 0000 quello di destra |
| |
int
21 |
;
leggi il secondo numero digitato dall'utente |
| |
sub
al, 30 |
;
l'istruzione e' come quella sub precedente ma non abbiamo
doppiato AL |
| |
cmp
al, 09 |
; |
| |
jle
fine |
;
il secondo numero esadecimale e' compreso tra 0 e 9 allora
salta alla fine |
| |
|
;
altrimenti... |
| |
sub
al, 07 |
;
il secondo numero e' compreswo tra A e F allora sottrai ancora
7h |
| fine: |
|
|
| |
mov
ah, 02 |
|
| |
mov
dl, al |
;
DL aveva gia il primo numero alla estrema sinistra a cui si
aggiunge il secondo |
| |
|
;
numero alla estrema destra cosi' da avere in un unico byte
le due cifre |
| |
int
21 |
|
| |
int
20 |
|
Se scritto tramite debug, questo programma
una volta lanciato con il comando g resta in attesa che l'utente
scriva un numero a due cifre. Mentre l'utente digita il primo
numero il programma processa l'esadecimale giungendo fino al secondo
int 21. Il primo carattere trattato viene accantonati in DL e
quando anche il secondo numero viene trattato anch'esso viene
inserito in DL e poi stampato.
Giungiamo finalmente all'applicazione delle regole sullo stack
apprese precedentemente per ampliare e perfezionare il programma
precedente.
Il problema che il programma incorpora e'
quello di non discriminare tra numeri esdecimali, infatti a noi
interessa tradurre solo numeri da 30 a 39 e da 41 a 46 [0 a F
ascii] e tralasciare il resto.
Quella che segue e' la procedura di verifica per cifre esadecimeli
comprese tra 30 e 39 e 41 e 46. Inoltre essa si occupa di trasformare
la cifra esadecimale nel segno ascii [sottraendo 30 per 30 - 39
e 37 per 41 - 46]
| inizio: |
|
|
|
|
push
dx |
;
salva il valore di DX proveniente dalla call [che vedremo
fra poco] |
|
mov
ah,08h |
;
nuova funzione per INT 21: legge un
numero digitato dalla tastiera |
| |
|
;
ma non lo visualizza sullo schermo |
| nuovo: |
|
|
| |
int
21 |
;
legato alla istruzione precedente |
| |
cmp
al, 30h |
;
compara il numero letto da tastiera a 0 esadecimale |
| |
jb
nuovo |
;
se il numero e' inferiore a 30 non prenderlo in considerazione
e torna a |
| |
|
;
leggere un nuovo numero, ovvero all'etichetta nuovo...
altrimenti... |
|
cmp
al, 46h |
;
compara il numero digitato con 46h ovvero con il carattere
ascii F |
| |
ja
nuovo |
;
se superiore a 46h non prenderlo in considerazione e vai a
leggere un |
| |
|
;
un nuovo numero... altrimenti... siamo in presenza
di un numero |
| |
|
;
tendenzialmente significativo... |
|
cmp
al, 39h |
;
comparalo con 9h per scoprire se deve essere tradotto in una
lettera o |
|
|
;
in un numero |
|
ja
lettera |
;
se maggiore di 39h [9 ascii] vai a trattarlo come una lettera,
altrimenti... |
|
|
;
e' una cifra da tradurre in numero [> 39h] |
|
mov
ah, 02 |
;
preparati a stamparlo |
|
mov,
dl, al |
; |
|
int
21 |
;
stampalo |
| |
sub
al, 30 |
;
trasforma il numero esadecimale in un carattere ascii [0 -
9] |
| |
pop
dx |
;
stiamo per tornare alla call quindi ripristina il registro
CX originario |
| |
ret |
;
fai ritorno alla call d'origine [che vedremo fra poco] |
| lettera: |
|
|
| |
cmp
al, 41h |
;
compara al con 41h [A ascii] |
| |
jb
nuovo |
;
per minore scarta [da 3Ah a 40h] numero e vai a nuovo... altrimenti... |
| |
mov
ah, 02h |
;
preparati a stamparlo |
| |
mov
dl, al |
|
| |
int
21 |
;
stampalo |
| |
sub
al, 37h |
;
trasforma il numero esadecimale in un carattere ascii [A -
F] |
| |
pop
dx |
;
stiamo per tornare alla call quindi ripristina il registro
CX originario |
| |
ret |
;
fai ritorno alla call d'origine [che vedremo fra poco] |
Questa e' una routine a cui un programma
rimanda immediante l'istruzione call. Le istruzioni precedenti
dovrebbero essere inserite in locazioni di memoria a partire dallo
spostamento 200. Allo spostamento 100 introduciamo la prcedura
di controllo:
|
|
call
inizio |
;
vai immediatamente alla richiesta di un numero all'utente |
|
mov
dl, al |
;
quando torno a questa istruzione grazie ad uno dei ret |
| |
|
;
il numero esadecimale in AL e' stato trasformato; viene messo
in DL |
| |
mov
cl, 04 |
;
viene caricaro il registro contatore CL con 04h |
| |
shl
dl, cl |
;
procede alla rotazione del numero a sinistra |
| |
call
inizio |
;
torno a inizio per il secondo numero |
| |
add
dl, al |
;
addiziono il secondo numero al registro DL dove c'e' gia'
il numero |
| |
|
;
spostao a sinistra. Il secondo numero verra' posizionato a
destra |
| |
mov
ah, 02 |
;
prepara alla stampa |
|
int
21 |
;
|
| |
int
20 |
;
|
Riassumendo: questo programma legge un
numero esadecimale a 2 cifre. Prende la prima la trasforma in
un carattere ascii [infatti ad esempio nel caso di 43 se il programma
non trasformasse la cifra in carattere ascii vedremo stampato
il carattere ascii
per il numero esadecimale 4 invece il nostro programma stampa
4 sottraendo 30 a 34 (4 esadecimale)] e la deposita in un registro,
legge il secondo numero, lo trasforma in un carattere ascii e
la deposita nello stesso registro. Poi il programma stampa le
due cifre digitate e ci fornisce la codifica ascii.
Cio' che distingue questo programma dal precedente e' che esso
controlla che il carattere digitato sia corretto, ovvero compreso
nell'intervallo ritenuto significativo tra 30 e 39 e 41 e 46.
Se e' compreso tra questo intervallo, dopo essere stato trasformato
in carattere ascii viene stampato immediatamente sul video. La
stessa cosa avviene per la seconda cifra. Una volta inseriti due
numeri compresi nei giusti intervalli, il programma stampa di
seguito il corrispondente carattere ascii frutto dell'unione dei
due numeri esadecimali. Abbiamo utilizzato la funzione 8 di int
21 perche' essa legge un numero dalla tastiera senza eco, ovvero
non lo visualizza sullo schermo a differenza della funzione 1
che lo visualizzava.
Data la lunghezza del programma segue la versione da copiare e
incollare nella shell di DOS:
call 0114
mov dl, al
mov cl, 04
shl dl, cl
call 0114
add dl, al
mov ah, 02
int 21
int 20
push dx
mov ah, 08
int 21
cmp al, 30
jb 0117
cmp al, 46
ja 0117
cmp al, 39
ja 012f
mov ah, 02
mov, dl, al
int 21
sub al, 30
pop dx
ret
cmp al, 41
jb 0117
mov ah, 02
mov dl, al
int 21
sub al, 37
pop dx
ret
|