Assembly Base con NASM
Capitolo 22: Istruzioni per la manipolazione delle stringhe
In questo capitolo vengono esaminate una serie di potentissime istruzioni che la
Intel definisce: istruzioni per la manipolazione delle stringhe (o,
più brevemente, istruzioni per le stringhe); si tenga presente che, in
relazione a tali istruzioni, il termine stringa deve essere inteso nel
senso più generale possibile. Nella terminologia usata dalla Intel, infatti,
la stringa rappresenta una sequenza (vettore) di generici elementi (cioè, di
generici numeri binari), tutti della stessa ampiezza in bit; lo scopo fondamentale
delle istruzioni per le stringhe, è quello di permetterci di gestire questi vettori
nel modo più comodo ed efficiente possibile.
Per chiarire bene questo aspetto, consideriamo un blocco di generiche informazioni
(cioè, di generici valori binari) che occupa 1024 byte di memoria; a seconda
delle nostre esigenze, legate al tipo di operazione che vogliamo svolgere, può essere
conveniente immaginare tale blocco come un vettore di 1024 elementi da
1 byte ciascuno, oppure come un vettore di 512 elementi da 1
word ciascuno o anche come un vettore di 256 elementi da 1 dword
ciascuno e così via.
In base a questa suddivisione ideale, possiamo parlare di stringa di BYTE,
stringa di WORD, stringa di DWORD e così via; ciascun BYTE,
WORD o DWORD, contiene un generico numero binario di cui,
eventualmente, possiamo anche ignorare il significato.
Il fatto che il blocco di informazioni venga visto come un vettore (o stringa),
impone che i vari elementi si trovino disposti in memoria in modo consecutivo e
contiguo; consideriamo, ad esempio, la seguente definizione presente all'offset
0000h di un blocco dati chiamato DATASEGM:
In presenza di questa definizione, l'assembler crea una locazione di memoria
da 4*4=16 byte, che inizia dall'offset 0000h del segmento
DATASEGM; la Figura 22.1 mostra la disposizione che assumerà strDword,
una volta che il programma verrà caricato in memoria.
In questo caso abbiamo chiaramente a che fare con un vettore di 4 elementi
da 1 dword ciascuno; nessuno però ci impedisce di trattare il blocco di
memoria come vettore di 8 elementi da 1 word ciascuno, in modo da
ottenere la situazione di Figura 22.2.
Analogamente, nessuno ci impedisce di trattare il blocco di memoria come vettore di
16 elementi da 1 byte ciascuno, in modo da ottenere la situazione di
Figura 22.3.
In sostanza, possiamo organizzare il vettore strDword nel modo che più si
adatta alle nostre esigenze; il programma di Figura 22.4 illustra questo importante
aspetto, visualizzando strDword come vettore di BYTE, di WORD
e di DWORD.
Osserviamo che all'interno di strbyte_loop, al registro BX
viene assegnato l'offset 0000h, che viene poi incrementato di 1
ad ogni ciclo; di conseguenza, il contenuto di BX copiato in AX,
coincide con l'indice del vettore di BYTE.
All'interno di strword_loop, al registro BX viene assegnato
l'offset 0000h, che viene poi incrementato di 2 ad ogni ciclo;
di conseguenza, il contenuto di BX copiato in AX, coincide con
l'indice, moltiplicato per 2, del vettore di WORD.
All'interno di strdword_loop, al registro BX viene assegnato
l'offset 0000h, che viene poi incrementato di 4 ad ogni ciclo;
di conseguenza, il contenuto di BX copiato in AX, coincide con
l'indice, moltiplicato per 4, del vettore di DWORD.
Se la definizione di strDword si trova ad un offset maggiore di 0000h,
da cui vogliamo ricavare un indice che parte da 0000h, allora dopo l'istruzione:
mov ax, bx ; ax = Offset(strDword+bx)
dobbiamo scrivere:
sub ax, strDword; ax = ax - Offset(strDword)
In alternativa, possiamo anche seguire una strada più semplice utilizzando un
apposito registro (ad esempio, SI) per gestire l'indice del vettore; questo
metodo è preferibile in quanto, come sappiamo, è pericoloso tentare di calcolare
in modo empirico l'offset di un dato (a tale proposito, si osservi che se
DATASEGM non ha un allineamento di tipo PARA, può capitare che
strDword parta da un offset diverso da 0000h).
Tornando alle Figure 22.1, 22.2 e 22.3, possiamo notare che a ciascun elemento di
un vettore (o stringa) è associato un indice, un offset e un
contenuto; analizziamo il significato, abbastanza ovvio, di questi parametri.
L'indice di un elemento rappresenta la posizione dell'elemento stesso
all'interno del vettore; come al solito, nel rispetto della convenzione adottata
dall'Assembly, conviene far partire gli indici da zero (e quindi, l'elemento
di indice 0 è il primo elemento del vettore, l'elemento di indice 1 è
il secondo elemento del vettore, l'elemento di indice 2 è il terzo elemento
del vettore e così via).
La memoria complessiva occupata da un vettore può essere ottenuta moltiplicando
il numero degli elementi del vettore per la dimensione in byte di ogni elemento;
nel caso di Figura 22.1, 22.2 e 22.3, abbiamo un vettore che occupa una porzione
di memoria da:
4 * 4 = 8 * 2 = 16 * 1 = 16 byte
L'offset di un elemento è, come già sappiamo, la sua distanza in byte
dall'inizio del segmento di programma nel quale il vettore è stato definito;
in Figura 22.1 (vettore di DWORD) vediamo che l'elemento di indice 0
si trova all'offset 0000h, l'elemento di indice 1 si trova
all'offset 0004h, l'elemento di indice 2 si trova all'offset
0008h, etc.
Per il vettore di WORD di Figura 22.2 vediamo che l'elemento di indice
0 si trova all'offset 0000h, l'elemento di indice 1 si
trova all'offset 0002h, l'elemento di indice 2 si trova all'offset
0004h, etc; infine, per il vettore di BYTE di Figura 22.3 vediamo
che l'elemento di indice 0 si trova all'offset 0000h, l'elemento di
indice 1 si trova all'offset 0001h, l'elemento di indice 2
si trova all'offset 0002h, etc.
Il contenuto di un elemento è il valore binario che viene memorizzato
nella locazione di memoria riservata all'elemento stesso; le Figure 22.1, 22.2 e
22.3 mostrano il contenuto dei vari elementi in notazione esadecimale.
Il nome strDword, assegnato alla stringa del nostro esempio, rappresenta
come sappiamo un Disp16 dietro il quale si nasconde l'offset iniziale
(0000h) della stringa stessa, calcolato rispetto a DATASEGM;
l'indirizzo logico da cui inizia la stringa è quindi DATASEGM:0000h.
Per accedere agli elementi della stringa possiamo utilizzare tutti i metodi che
abbiamo studiato nei precedenti capitoli; servendoci del nome strDword
possiamo accedere alla stringa in modo diretto (accesso per nome), mentre servendoci
dei registri puntatori, possiamo accedere alla stringa in modo indiretto (accesso per
indirizzo).
Nel caso di Figura 22.1, abbiamo a che fare con un vettore di DWORD; per accedere
al vettore in modo diretto, possiamo servirci della sintassi:
dword [strDword+0], dword [strDword+4], dword [strDword+8], ...
Nel caso di accesso diretto al vettore di Figura 22.2, possiamo servirci della sintassi:
word [strDword+0], word [strDword+2], word [strDword+4], ...
Analogamente, per il vettore di Figura 22.3 dobbiamo scrivere:
byte [strDword+0], byte [strDword+1], byte [strDword+2], ...
Nel caso di accesso indiretto (tramite i registri puntatori), la situazione è
ancora più semplice; caricando, ad esempio, in BX l'offset (0000h) di
strDword, possiamo accedere alle varie DWORD del vettore di Figura 22.1
con la sintassi:
dword [BX+0], dword [BX+4], dword [BX+8], ...
Per il vettore di WORD di Figura 22.2, la sequenza è:
word [BX+0], word [BX+2], word [BX+4], ...
Per il vettore di BYTE di Figura 22.3, la sequenza è:
byte [BX+0], byte [BX+1], byte [BX+2], ...
Naturalmente, sappiamo che in casi di questo genere, possiamo fare in modo
che l'ampiezza in byte degli operandi venga determinata implicitamente
dall'assembler; ad esempio, nel caso dell'istruzione:
mov ax, [bx+4]
la presenza del registro AX dice all'assembler che gli operandi sono
a 16 bit (e quindi, [BX+4] rappresenta una WORD che si trova
all'indirizzo logico DATASEGM:0004h).
È importante ribadire che, come è stato già spiegato in un precedente capitolo,
esiste una grossa differenza tra Assembly e linguaggi di alto livello, nel modo
di gestire i vettori; in Assembly, il programmatore ha il compito di calcolare
gli spiazzamenti relativi ad ogni elemento di un vettore. Gli esempi che abbiamo appena
analizzato rendono evidente questo aspetto; nel caso, ad esempio, del vettore di
DWORD di Figura 22.1, abbiamo la seguente situazione:
- dword [strDword+0] rappresenta la DWORD che si trova all'offset
0000h + 0 = 0000h di DATASEGM
- dword [strDword+4] rappresenta la DWORD che si trova all'offset
0000h + 4 = 0004h di DATASEGM
- dword [strDword+8] rappresenta la DWORD che si trova all'offset
0000h + 8 = 0008h di DATASEGM
e così via.
Nel caso dei linguaggi di alto livello, invece, tutti questi calcoli vengono
effettuati dal compilatore o dall'interprete; considerando sempre il vettore di
DWORD di Figura 22.1, possiamo affermare che se stiamo utilizzando un
qualunque linguaggio di alto livello, abbiamo la seguente situazione:
- strDword[0] rappresenta la DWORD che si trova all'offset
0000h + (0 * 4) = 0000h di DATASEGM
- strDword[1] rappresenta la DWORD che si trova all'offset
0000h + (1 * 4) = 0004h di DATASEGM
- strDword[2] rappresenta la DWORD che si trova all'offset
0000h + (2 * 4) = 0008h di DATASEGM
e così via.
In sostanza, il compilatore (o l'interprete) sa che ogni elemento di strDword
occupa 4 byte; di conseguenza, l'indice specificato dal programmatore viene
moltiplicato per 4 in modo da ottenere il corretto spiazzamento.
22.1 Stringhe ASCII generiche, stringhe C e stringhe Pascal
Abbiamo visto quindi che le stringhe generiche rappresentano sequenze di elementi,
tutti della stessa natura (stessa ampiezza in bit), disposti in memoria in modo
consecutivo e contiguo; dal punto di vista del computer, il contenuto di ciascun
elemento della stringa non è altro che un normalissimo numero binario. È compito
del programmatore associare a questo numero binario un significato ben preciso;
nel caso, ad esempio, del vettore strDword di Figura 22.1, i valori a
32 bit contenuti in ciascun elemento potrebbero rappresentare numeri reali
in formato IEEE a 32 bit (short real).
Un caso particolarmente importante per le stringhe, si presenta quando abbiamo a
che fare con un vettore di BYTE, dove ogni elemento contiene un codice
ASCII; come già sappiamo,
queste particolari stringhe vengono definite stringhe alfanumeriche o
stringhe ASCII!
Le stringhe ASCII sono talmente importanti, che tutti i linguaggi di
programmazione, compreso l'Assembly, le trattano come un vero e proprio tipo
di dato predefinito; in un precedente capitolo abbiamo visto che in Assembly
possiamo scrivere, ad esempio:
strByte db 'Stringa ASCII'
Incontrando questa definizione, l'assembler riserva a strByte 13
locazioni di memoria da 1 byte ciascuna, per un totale di 13 byte;
questi 13 byte vengono riempiti dall'assembler con i codici
ASCII dei 13
simboli che formano la stringa. Possiamo affermare quindi che la precedente
definizione è perfettamente equivalente a:
strByte db 53h, 74h, 72h, 69h, 6Eh, 67h, 61h, 20h, 41h, 53h, 43h, 49h, 49h
Supponendo che strByte sia stata definita a partire dall'offset 0018h
di un blocco DATASEGM, quando il programma verrà caricato in memoria la
stringa assumerà la disposizione mostrata in Figura 22.5.
Se ora vogliamo scrivere un programma che, attraverso un loop, visualizza tutti
dettagli relativi alla stringa di Figura 22.5, ci accorgiamo subito che si presenta
un piccolo problema; all'interno del loop che visualizza le varie informazioni,
come facciamo a sapere se la stringa è terminata?
In sostanza, la Figura 22.5 rappresenta una generica stringa ASCII che non
contiene alcuna informazione sulla propria lunghezza; siccome le stringhe
ASCII vengono utilizzate in modo massiccio nei programmi, è necessario trovare
una adeguata soluzione a questo problema.
Una tecnica molto utilizzata dai programmatori Assembly, consiste nel
servirsi del location counter ($); come sappiamo, in fase di
assemblaggio di un programma, il location counter rappresenta l'offset
corrente all'interno del segmento di programma che l'assembler sta
esaminando!
Per determinare in fase di assemblaggio la lunghezza di strByte, possiamo
utilizzare allora il metodo seguente:
(come vedremo nel Capitolo 24, la direttiva EQU permette di rappresentare
con un nome simbolico, sia valori numerici, sia stringhe alfanumeriche, dalle più
semplici alle più complesse).
Quando l'assembler incontra la dichiarazione di STRBYTE_LEN, il location
counter segna l'offset 0025h (relativo a DATASEGM); infatti,
sommando le dimensioni del blocco fillChar (0018h byte) con le
dimensioni del blocco strByte (000Dh byte), otteniamo:
0018h + 000Dh = 0025h
Di conseguenza, tenendo presente che strByte si trova all'offset 0018h
di DATASEGM, si ha:
STRBYTE_LEN = $ - offset strByte = 0025h - 0018h = 000Dh
Ma 000Dh=13 è proprio la lunghezza in byte di strByte!
A questo punto possiamo procedere con un esempio pratico; il programma di Figura
22.6 visualizza l'indice, l'offset, il codice
ASCII e il simbolo
ASCII di ogni elemento di
strByte.
La tecnica mostrata in precedenza per la determinazione della lunghezza di
strByte, funziona solo in fase di assemblaggio, per le stringhe definite
staticamente; per determinare la lunghezza di eventuali stringhe create
dinamicamente (in fase di esecuzione), dobbiamo necessariamente ricorrere ad
altri espedienti.
Per risolvere questo problema in modo efficiente, in campo informatico sono stati
adottati diversi accorgimenti; nei precedenti capitoli ne abbiamo già incontrati
alcuni. Abbiamo visto, ad esempio, che il servizio n. 09h (display
string) della INT 21h, permette di mostrare una stringa ASCII
sullo schermo, a partire dalla posizione corrente del cursore; tale servizio si
aspetta che la stringa termini con il simbolo '$'. Un altro esempio
emblematico è rappresentato dalla procedura writeString presente nelle
librerie EXELIB e COMLIB; tale procedura permette di mostrare sullo
schermo una stringa ASCII che deve terminare con un BYTE di valore
0.
Tutti questi accorgimenti, finalizzati a rendere più semplice e veloce la
manipolazione delle stringhe ASCII, hanno portato alla nascita di diverse
convenzioni; analizziamo, in particolare, le due convenzioni più importanti.
22.1.1 Convenzione C per le stringhe ASCII
Secondo la convenzione del linguaggio C, una stringa ASCII viene
definita come un vettore di BYTE il cui ultimo elemento vale zero; in
questo caso si parla di stringa C o zero terminated string.
In linguaggio C, la stringa strByte del nostro esempio può essere
definita in questo modo:
char strByte[] = "Stringa ASCII";
Quando il compilatore C incontra questa definizione, crea 14
locazioni di memoria consecutive e contigue, ciascuna delle quali occupa un byte;
nelle prime 13 locazioni vengono caricati i codici
ASCII dei 13
simboli che formano la stringa, mentre nella quattordicesima locazione viene
caricato il valore 00h. Una stringa C, quindi, occupa in memoria
1 byte in più rispetto al numero di simboli che formano la stringa stessa.
Nel linguaggio C gli indici dei vettori partono da zero, per cui i vari
elementi di strByte sono rappresentati dalle notazioni:
strByte[0], strByte[1], strByte[2], ...
Il programma in linguaggio C di Figura 22.7, visualizza l'indice, l'offset,
il codice ASCII
e il simbolo ASCII di ogni
elemento di strByte.
In questo esempio la stringa viene gestita in notazione vettoriale solo per scopo
didattico; è chiaro che un programmatore C esperto utilizzerebbe, invece,
i puntatori.
La funzione strlen del C restituisce la lunghezza "apparente" della
stringa (cioè, 13); la funzione sizeof restituisce, invece, la
dimensione effettiva in byte dell'oggetto strByte (cioè, 14).
L'operatore & del C restituisce l'offset del dato a cui viene
applicato; il simbolo &strByte[i] rappresenta quindi l'offset
dell'elemento di indice i di strByte.
Il programma stampa anche il contenuto dell'ultimo elemento (strByte[13])
della stringa; in questo modo si può constatare che strByte[13] contiene
proprio il valore 0 aggiunto automaticamente dal compilatore C alla
fine della stringa!
La convenzione C presenta il vantaggio di permettere la definizione di
stringhe ASCII di lunghezza teoricamente infinita; infatti, una stringa
C continua finché non viene incontrato lo zero finale!
Lo svantaggio evidente della convenzione C è dato dal fatto che la
gestione delle stringhe ASCII non è molto efficiente a causa della
necessità di testare continuamente la condizione di fine stringa; più avanti
vedremo un esempio in Assembly che chiarirà questo aspetto.
22.1.2 Convenzione Pascal per le stringhe ASCII
Secondo la convenzione del linguaggio Pascal, una stringa ASCII
viene definita come un vettore di BYTE il cui primo elemento contiene la
lunghezza della stringa stessa; in questo caso si parla di stringa Pascal.
In linguaggio Pascal, la stringa strByte del nostro esempio può
essere definita in questo modo:
const strByte: string[13] = 'Stringa ASCII';
Quando il compilatore Pascal incontra questa definizione, crea 14
locazioni di memoria consecutive e contigue, ciascuna delle quali occupa un byte;
nella prima locazione viene inserito il valore 13 (lunghezza della stringa),
mentre nelle successive 13 locazioni vengono caricati i codici
ASCII dei 13
simboli che formano la stringa. Una stringa Pascal, quindi, occupa in
memoria 1 byte in più rispetto al numero di simboli che formano la stringa
stessa.
Nel linguaggio Pascal gli indici dei vettori partono da uno, per cui i vari
elementi di strByte sono rappresentati dalle notazioni:
strByte[1], strByte[2], strByte[3], ...
In realtà, esiste anche l'elemento strByte[0] che, come abbiamo appena
visto, contiene la lunghezza in byte della stringa!
Il programma in linguaggio Pascal di Figura 22.8, visualizza l'indice,
l'offset, il codice ASCII
e il simbolo ASCII di ogni
elemento di strByte.
La funzione Length del Pascal restituisce la lunghezza "apparente" della
stringa (cioè, 13); la funzione SizeOf restituisce, invece, la dimensione
effettiva in byte dell'oggetto strByte (cioè, 14).
La funzione Ofs del Pascal restituisce l'offset del dato a cui viene
applicata; il simbolo Ofs(strByte[i]) rappresenta quindi l'offset dell'elemento
di indice i di strByte.
La funzione Ord restituisce il contenuto numerico di strByte[0] (cioè,
13); per velocizzare il ciclo FOR possiamo servirci proprio del valore
Ord(strByte[0]) che rappresenta la condizione di uscita dal ciclo stesso
(13 loop a partire dall'indice 1).
La convenzione Pascal presenta il vantaggio di permettere una gestione
molto efficiente delle stringhe ASCII; ciò è dovuto chiaramente al fatto
che conosciamo in anticipo la lunghezza della stringa.
Lo svantaggio evidente della convenzione Pascal è dato dal fatto che la
lunghezza di una stringa ASCII è limitata a 255 elementi; infatti,
la lunghezza di una stringa Pascal viene memorizzata in una locazione da
1 byte, per cui deve essere un valore compreso tra 0 e 255!
22.1.3 Gestione delle stringhe C e Pascal in Assembly
Per analizzare meglio pregi e difetti delle convenzioni C e Pascal,
scriviamo un apposito programma Assembly; ovviamente, in Assembly
tutta la gestione dei dettagli relativi alle stringhe C e Pascal
ricade sul programmatore!
La definizione Assembly di una stringa C deve comprendere anche
l'aggiunta dello zero finale; nel caso di strByte, possiamo scrivere:
strByte db 'Stringa ASCII', 0
La definizione Assembly di una stringa Pascal deve comprendere anche
la lunghezza della stringa stessa (primo elemento); nel caso di strByte,
possiamo scrivere:
strByte db 13, 'Stringa ASCII'
Il programma di Figura 22.9 visualizza l'indice, l'offset, il codice
ASCII e il simbolo
ASCII di ogni elemento
della stringa strByte definita, sia in versione C, sia in versione
Pascal.
Analizzando il loop delimitato dall'etichetta strByteC_loop, possiamo
notare che, non conoscendo in anticipo la lunghezza di strByteC, siamo
costretti ad effettuare un test di fine stringa ad ogni iterazione; come si
può facilmente immaginare, tutto ciò si ripercuote negativamente sulla velocità
di esecuzione del loop stesso!
Per gestire anche il caso di eventuali stringhe di lunghezza nulla, conviene
effettuare il test di fine stringa all'inizio del loop; in alternativa, se
siamo sicuri di dover gestire stringhe di lunghezza maggiore di zero, possiamo
anche modificare il loop in questo modo:
La sostanza però non cambia in quanto, anche in questo caso, dobbiamo effettuare
un test di fine stringa ad ogni iterazione; l'unico modo per evitare questo
problema, consiste nel calcolare in anticipo la lunghezza di strByteC!
Per quanto riguarda il loop delimitato dall'etichetta strBytePas_loop,
possiamo notare che la gestione delle stringhe Pascal si svolge in
modo molto semplice ed efficiente; naturalmente, ciò accade in quanto, ogni
stringa Pascal, specifica la propria lunghezza attraverso il suo elemento
di indice 0.
Osserviamo anche l'istruzione:
mov bx, strBytePas + 1
Questa istruzione fa puntare DS:BX all'elemento di indice 1 di
strBytePas; in questo modo, viene scavalcato l'elemento di indice 0
che contiene la lunghezza della stringa!
Dopo aver esaminato tutti gli aspetti teorici relativi alle stringhe, possiamo
passare allo studio delle istruzioni che le CPU della famiglia 80x86
mettono a disposizione per la gestione di questi particolari aggregati di dati;
attraverso tali istruzioni, è possibile manipolare stringhe di grosse dimensioni,
in modo estremamente semplice ed efficiente.
Tutte le istruzioni per le stringhe, utilizzano SI per indirizzare un
eventuale operando sorgente e DI per indirizzare un eventuale operando
destinazione; da ciò derivano i nomi di source index (indice sorgente)
per SI e destination index (indice destinazione) per DI.
22.2 Le istruzioni
STOS,
STOSB,
STOSW,
STOSD
Con il mnemonico STOS si indica l'istruzione Store String
(scrittura di un BYTE/WORD/DWORD in una stringa); le uniche forme
lecite per questa istruzione, sono le seguenti:
Nella prima forma, l'istruzione STOS richiede esplicitamente un operando
DEST che rappresenta una locazione di memoria; l'operando SRC è
implicitamente il registro accumulatore.
L'operando DEST deve essere indirizzato obbligatoriamente con ES:DI;
a seconda dell'ampiezza in bit (8, 16 o 32) di DEST,
la CPU decide se utilizzare AL, AX o EAX come operando
SRC.
Siccome la coppia ES:DI non specifica la dimensione in bit della locazione
di memoria a cui punta, dobbiamo servirci necessariamente degli address size
operators; si possono presentare allora i seguenti tre casi:
- Scrittura di un BYTE in [ES:DI]
stos byte [es:di] ; codice macchina AAh
Quando la CPU incontra il codice macchina AAh, copia il contenuto
a 8 bit di AL, nella locazione di memoria puntata da ES:DI;
subito dopo, la CPU aggiorna il puntatore DI in base allo stato
del flag DF. Se DF=0, il contenuto di DI viene incrementato
di 1; se DF=1, il contenuto di DI viene, invece, decrementato
di 1.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
mov [es:di], al
- Scrittura di una WORD in [ES:DI]
stos word [es:di] ; codice macchina ABh
Quando la CPU incontra il codice macchina ABh, copia il contenuto
a 16 bit di AX, nella locazione di memoria puntata da ES:DI;
subito dopo, la CPU aggiorna il puntatore DI in base allo stato
del flag DF. Se DF=0, il contenuto di DI viene incrementato
di 2; se DF=1, il contenuto di DI viene, invece, decrementato
di 2.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
mov [es:di], ax
- Scrittura di una DWORD in [ES:DI]
stos dword [es:di] ; codice macchina 66h ABh
Quando la CPU incontra il codice macchina 66h ABh, copia il contenuto
a 32 bit di EAX, nella locazione di memoria puntata da ES:DI;
subito dopo, la CPU aggiorna il puntatore DI in base allo stato
del flag DF. Se DF=0, il contenuto di DI viene incrementato
di 4; se DF=1, il contenuto di DI viene, invece, decrementato
di 4.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
mov [es:di], eax
L'istruzione STOS determina una associazione predefinita tra DI e
ES; di conseguenza, se scriviamo:
stos word [di]
viene automaticamente utilizzato ES come registro di segmento!
Il segment override è proibito; se proviamo quindi a scrivere istruzioni del
tipo:
stos byte [ds:di]
otteniamo un messaggio di errore da parte dell'assembler!
In presenza, invece, di istruzioni del tipo:
stos word [bx]
o
stos word [es:dx]
viene ugualmente utilizzata la coppia predefinita ES:DI; in casi del genere,
molti assembler non mostrano alcun messaggio di errore!
Ciò è una diretta conseguenza del fatto che, il codice macchina di STOS,
ha il solo scopo di specificare l'ampiezza in bit degli operandi; tutte le altre
informazioni non sono necessarie e, di fatto, vengono ignorate dall'assembler!
Usualmente, l'istruzione STOS viene utilizzata nella elaborazione di
dati da memorizzare poi in una stringa.
Come esempio pratico, supponiamo di voler inizializzare con il valore 8Fh,
tutti i 256 elementi del seguente vettore di BYTE, definito in un
segmento DATASEGM referenziato da DS:
vectByte resb 256
Con l'ausilio di STOS possiamo scrivere il seguente codice:
L'esempio appena illustrato non evidenzia le potenzialità dell'istruzione
STOS; si può dire anzi che in casi del genere è preferibile utilizzare
MOV che si rivela molto più veloce ed efficiente. La situazione cambia
radicalmente nel momento in cui si ha la necessità di operare su blocchi di
memoria di grosse dimensioni; in tal caso, come vedremo nel seguito del
capitolo, STOS diventa molto più efficiente di MOV!
22.2.1 Forme implicite dell'istruzione STOS
In virtù del fatto che gli operandi obbligatori dell'istruzione STOS sono
ES:DI e AL/AX/EAX, vengono resi disponibili i mnemonici STOSB,
STOSW e STOSD, che evitano al programmatore di dover scrivere ogni
volta l'operando DEST; come si può facilmente intuire:
stosb ; Store String Byte
equivale a:
stos byte [es:di]
stosw ; Store String Word
equivale a:
stos word [es:di]
stosd ; Store String Dword
equivale a:
stos dword [es:di]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi
previsti per STOS.
22.2.2 Effetti provocati da STOS, STOSB, STOSW e STOSD,
sugli operandi e sui flags
L'esecuzione delle istruzioni STOS, STOSB, STOSW e
STOSD, modifica il contenuto della locazione di memoria puntata da
ES:DI; inoltre, il contenuto del registro puntatore DI viene
aggiornato in base allo stato del flag DF. Il contenuto dell'operando
SRC rimane inalterato.
L'esecuzione delle istruzioni STOS, STOSB, STOSW e
STOSD, non modifica alcun campo del Flags Register.
22.3 Le istruzioni
LODS,
LODSB,
LODSW,
LODSD
Con il mnemonico LODS si indica l'istruzione Load String
(lettura di un BYTE/WORD/DWORD da una stringa); le uniche forme
lecite per questa istruzione, sono le seguenti:
Nella prima forma, l'istruzione LODS richiede esplicitamente un operando
SRC che rappresenta una locazione di memoria; l'operando DEST è
implicitamente il registro accumulatore.
Per l'indirizzamento dell'operando SRC è obbligatorio l'uso del registro
puntatore SI; in assenza di diverse indicazioni da parte del programmatore,
SI viene automaticamente associato a DS.
A seconda dell'ampiezza in bit (8, 16 o 32) di SRC,
la CPU decide se utilizzare AL, AX o EAX come operando
DEST; siccome la coppia DS:SI non specifica la dimensione in bit
della locazione di memoria a cui punta, dobbiamo servirci necessariamente degli
address size operators. Si possono presentare allora i seguenti tre casi:
- Lettura di un BYTE da [DS:SI]
lods byte [ds:si] ; codice macchina ACh
Quando la CPU incontra il codice macchina ACh, copia nel registro
AL il contenuto a 8 bit della locazione di memoria puntata da
DS:SI; subito dopo, la CPU aggiorna il puntatore SI in base
allo stato del flag DF. Se DF=0, il contenuto di SI viene
incrementato di 1; se DF=1, il contenuto di SI viene, invece,
decrementato di 1.
Trascurando l'aggiornamento di SI, la precedente istruzione equivale a:
mov al, [ds:si]
- Lettura di una WORD da [DS:SI]
lods word [ds:si] ; codice macchina ADh
Quando la CPU incontra il codice macchina ADh, copia nel registro
AX il contenuto a 16 bit della locazione di memoria puntata da
DS:SI; subito dopo, la CPU aggiorna il puntatore SI in base
allo stato del flag DF. Se DF=0, il contenuto di SI viene
incrementato di 2; se DF=1, il contenuto di SI viene, invece,
decrementato di 2.
Trascurando l'aggiornamento di SI, la precedente istruzione equivale a:
mov ax, [ds:si]
- Lettura di una DWORD da [DS:SI]
lods dword [ds:si] ; codice macchina 66h ADh
Quando la CPU incontra il codice macchina 66h ADh, copia nel registro
EAX il contenuto a 32 bit della locazione di memoria puntata da
DS:SI; subito dopo, la CPU aggiorna il puntatore SI in base allo
stato del flag DF. Se DF=0, il contenuto di SI viene incrementato
di 4; se DF=1, il contenuto di SI viene, invece, decrementato
di 4.
Trascurando l'aggiornamento di SI, la precedente istruzione equivale a:
mov eax, [ds:si]
L'istruzione LODS determina una associazione predefinita tra SI e
DS; di conseguenza, se scriviamo:
lods byte [si]
viene automaticamente utilizzato DS come registro di segmento!
L'uso dell'istruzione LODS presenta un potenziale problema legato al
fatto che DS viene generalmente utilizzato per referenziare il segmento
dati principale di un programma; può capitare allora che l'impiego di LODS
renda necessaria la modifica del contenuto dello stesso DS!
Naturalmente, in un caso del genere bisogna prendere tutte le opportune
precauzioni (che consistono nel preservare il contenuto originale di DS);
proprio per venire incontro alle esigenze del programmatore, l'istruzione
LODS permette il segment override.
In pratica, al posto di DS possiamo specificare esplicitamente un diverso
registro di segmento (CS, ES, FS, GS o SS); in
questo modo, se la nostra intenzione è quella di non modificare DS, possiamo
scrivere istruzioni del tipo:
lods dword [fs:si]
In presenza di istruzioni del tipo:
lods word [bx]
o
lods byte [gs:ax]
viene ugualmente utilizzato il puntatore predefinito SI; in casi del genere,
molti assembler non mostrano alcun messaggio di errore!
Ciò è una diretta conseguenza del fatto che, il codice macchina di LODS,
ha il solo scopo di specificare l'ampiezza in bit degli operandi e la presenza
di un eventuale segment override; tutte le altre informazioni non sono necessarie
e, di fatto, vengono ignorate dall'assembler!
Usualmente, l'istruzione LODS viene utilizzata nella elaborazione di dati
precedentemente letti da una stringa.
Come esempio pratico, supponiamo di avere un segmento dati DATASEGM
referenziato da DS; all'interno di DATASEGM sono presenti le
seguenti definizioni:
Vogliamo leggere la stringa C chiamata strSource, convertirla in
maiuscolo e scriverla in strDest; con l'ausilio delle istruzioni
LODS e STOS possiamo scrivere il seguente codice:
Osserviamo che il test di fine stringa deve essere sistemato in fondo al loop;
infatti, la copia tra stringhe C deve comprendere anche lo zero finale!
Nell'esempio appena presentato, sia la stringa sorgente, sia la stringa destinazione,
si trovano nel segmento DATASEGM referenziato da DS; in un caso del
genere, ci basta porre ES=DS senza la necessità di modificare il contenuto
di DS (eventualmente, se anche ES era già in uso, dobbiamo preservarne
il contenuto originale).
Il caso più semplice si verifica quando la stringa sorgente si trova in un segmento
referenziato da DS, mentre la stringa destinazione si trova in un segmento
referenziato da ES; in una situazione del genere, non dobbiamo apportare
alcuna modifica a DS e ES.
Il caso più contorto si verifica, invece, quando la stringa sorgente si trova in un
segmento referenziato da ES, mentre la stringa destinazione si trova in un
segmento referenziato da DS; in una situazione del genere, siamo costretti a
scambiare il contenuto di DS con quello di ES (preservando i valori
originali).
Per affrontare questo caso, prima del loop dobbiamo scrivere:
Subito dopo il loop dobbiamo scrivere:
In analogia a quanto è stato detto per STOS, anche l'esempio appena illustrato
non evidenzia le potenzialità dell'istruzione LODS; in generale, tutte le
istruzioni presentate in questo capitolo, diventano convenienti nel momento in cui
si deve operare su stringhe di grosse dimensioni.
22.3.1 Forme implicite dell'istruzione LODS
In virtù del fatto che l'istruzione LODS utilizza AL/AX/EAX come
operando DEST e la coppia predefinita DS:SI per indirizzare
l'operando SRC, vengono resi disponibili i mnemonici LODSB,
LODSW e LODSD, che evitano al programmatore di dover scrivere ogni
volta l'operando SRC; come si può facilmente intuire:
lodsb ; Load String Byte
equivale a:
lods byte [ds:si]
lodsw ; Load String Word
equivale a:
lods word [ds:si]
lodsd ; Load String Dword
equivale a:
lods dword [ds:si]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi
previsti per LODS.
Se vogliamo applicare il segment override all'operando SRC di LODSB,
LODSW e LODSD, possiamo utilizzare la seguente sintassi supportata
da NASM:
fs lodsw
Tale istruzione equivale a:
lods word [fs:si]
Come è stato già spiegato, il programmatore può solo indicare un registro di segmento
diverso da quello predefinito DS; l'uso del registro puntatore SI è,
invece, obbligatorio.
22.3.2 Effetti provocati da LODS, LODSB, LODSW e LODSD,
sugli operandi e sui flags
L'esecuzione delle istruzioni LODS, LODSB, LODSW e
LODSD, modifica il contenuto dell'accumulatore; inoltre, il contenuto del
registro puntatore SI viene aggiornato in base allo stato del flag DF.
Il contenuto dell'operando SRC rimane inalterato.
L'esecuzione delle istruzioni LODS, LODSB, LODSW e
LODSD, non modifica alcun campo del Flags Register.
22.4 Le istruzioni
MOVS,
MOVSB,
MOVSW,
MOVSD
Con il mnemonico MOVS si indica l'istruzione Move Data from String
to String (trasferimento dati da stringa a stringa); le uniche forme lecite
per questa istruzione, sono le seguenti:
Nella prima forma, l'istruzione MOVS richiede esplicitamente entrambi gli
operandi SRC e DEST, che rappresentano delle locazioni di memoria;
l'operando DEST deve essere indirizzato obbligatoriamente con la coppia
ES:DI. Per l'indirizzamento dell'operando SRC è obbligatorio l'uso
del registro puntatore SI; in assenza di diverse indicazioni da parte del
programmatore, SI viene automaticamente associato a DS.
Siccome le coppie ES:DI e DS:SI non specificano la dimensione in bit
delle locazioni di memoria a cui puntano, dobbiamo servirci necessariamente degli
address size operators (da applicare ad uno solo dei due operandi); si
possono presentare allora i seguenti tre casi:
- Copia di un BYTE da [DS:SI] a [ES:DI]
movs byte [es:di], [ds:si] ; codice macchina A4h
Quando la CPU incontra il codice macchina A4h, legge il contenuto a
8 bit della locazione di memoria puntata da DS:SI e lo copia nella
locazione di memoria puntata da ES:DI; subito dopo, la CPU aggiorna i
puntatori DI e SI in base allo stato del flag DF. Se DF=0,
il contenuto di DI e SI viene incrementato di 1; se DF=1,
il contenuto di DI e SI viene, invece, decrementato di 1.
Trascurando l'aggiornamento di DI e SI, la precedente istruzione
equivale a:
- Copia di una WORD da [DS:SI] a [ES:DI]
movs word [es:di], [ds:si] ; codice macchina A5h
Quando la CPU incontra il codice macchina A5h, legge il contenuto a
16 bit della locazione di memoria puntata da DS:SI e lo copia nella
locazione di memoria puntata da ES:DI; subito dopo, la CPU aggiorna i
puntatori DI e SI in base allo stato del flag DF. Se DF=0,
il contenuto di DI e SI viene incrementato di 2; se DF=1,
il contenuto di DI e SI viene, invece, decrementato di 2.
Trascurando l'aggiornamento di DI e SI, la precedente istruzione
equivale a:
- Copia di una DWORD da [DS:SI] a [ES:DI]
movs dword [es:di], [ds:si] ; codice macchina 66h A5h
Quando la CPU incontra il codice macchina 66h A5h, legge il contenuto
a 32 bit della locazione di memoria puntata da DS:SI e lo copia nella
locazione di memoria puntata da ES:DI; subito dopo, la CPU aggiorna i
puntatori DI e SI in base allo stato del flag DF. Se DF=0,
il contenuto di DI e SI viene incrementato di 4; se DF=1,
il contenuto di DI e SI viene, invece, decrementato di 4.
Trascurando l'aggiornamento di DI e SI, la precedente istruzione
equivale a:
Apparentemente, MOVS sembra un'istruzione per il trasferimento dati da memoria
a memoria; in realtà, i dati vengono trasferiti con un passaggio intermedio che
coinvolge i registri temporanei della CPU!
L'istruzione MOVS determina una associazione predefinita tra SI e
DS e tra DI e ES; di conseguenza, se scriviamo:
movs byte [di], [si]
il registro puntatore SI viene automaticamente associato a DS, mentre
DI viene automaticamente associato a ES!
L'uso dell'istruzione MOVS presenta un potenziale problema legato al
fatto che DS viene generalmente utilizzato per referenziare il segmento
dati principale di un programma; può capitare allora che l'impiego di MOVS
renda necessaria la modifica del contenuto dello stesso DS!
Naturalmente, in un caso del genere bisogna prendere tutte le opportune
precauzioni (che consistono nel preservare il contenuto originale di DS);
proprio per venire incontro alle esigenze del programmatore, l'istruzione
MOVS permette il segment override, esclusivamente per l'operando
SRC.
In pratica, al posto di DS possiamo specificare esplicitamente un diverso
registro di segmento (CS, ES, FS, GS o SS); in
questo modo, se la nostra intenzione è quella di non modificare DS, possiamo
scrivere istruzioni del tipo:
movs dword [es:di], [fs:si]
Il segment override relativo all'operando DEST è proibito; se proviamo
quindi a scrivere istruzioni del tipo:
movs byte [fs:di], [ds:si]
otteniamo un messaggio di errore da parte dell'assembler!
In presenza, invece, di istruzioni del tipo:
movs word [cx], [dx]
o
movs word [ax], [gs:bx]
vengono ugualmente utilizzati i puntatori predefiniti DI e SI; in
casi del genere, molti assembler non mostrano alcun messaggio di errore!
Ciò è una diretta conseguenza del fatto che, il codice macchina di MOVS,
ha il solo scopo di specificare l'ampiezza in bit degli operandi e la presenza
di un eventuale segment override per SRC; tutte le altre informazioni non
sono necessarie e, di fatto, vengono ignorate dall'assembler!
Usualmente, l'istruzione MOVS viene utilizzata per copiare grosse quantità
di dati, da una parte all'altra della memoria.
Come esempio pratico, supponiamo di avere un segmento dati DATASEGM
referenziato da DS; all'interno di DATASEGM sono presenti le
seguenti definizioni:
Vogliamo copiare strSource in StrDest; con l'ausilio dell'istruzione
MOVS possiamo scrivere il seguente codice:
Analizzando la tabella
delle istruzioni della CPU, si può notare che il numero di cicli di
clock necessari alla CPU per eseguire MOVS è sempre lo stesso,
indipendentemente dall'ampiezza in bit (8, 16 o 32) del
dato da trasferire; è chiaro quindi che, nei limiti del possibile, conviene sempre
utilizzare MOVS con operandi a 32 bit e ciò vale per tutte le
istruzioni illustrate in questo capitolo!
Nel caso del nostro esempio, possiamo notare che un vettore di 40
elementi da 1 byte ciascuno, può essere visto anche come vettore di
20 elementi da 1 word ciascuno; il precedente codice può essere
allora riscritto in questo modo:
Un vettore di 20 elementi da 1 word ciascuno, può essere visto anche
come vettore di 10 elementi da 1 dword ciascuno; il precedente codice
può essere allora riscritto in questo modo:
In questa terza versione dell'esempio, nello stesso intervallo di tempo riusciamo
a copiare una quantità di dati 4 volte superiore rispetto al caso della
copia byte per byte!
Nell'esempio appena presentato, sia la stringa sorgente, sia la stringa destinazione,
si trovano nel segmento DATASEGM referenziato da DS; in un caso del
genere, ci basta porre ES=DS senza la necessità di modificare il contenuto
di DS (eventualmente, se anche ES era già in uso, dobbiamo preservarne
il contenuto originale).
Se la situazione relativa ai registri di segmento è più complicata, si possono
applicare gli stessi concetti esposti in precedenza per l'istruzione LODS.
22.4.1 Forme implicite dell'istruzione MOVS
In virtù del fatto che l'istruzione MOVS utilizza la coppia obbligata
ES:DI per indirizzare l'operando DEST e la coppia predefinita
DS:SI per indirizzare l'operando SRC, vengono resi disponibili
i mnemonici MOVSB, MOVSW e MOVSD, che evitano al programmatore
di dover scrivere ogni volta gli operandi SRC e DEST; come si può
facilmente intuire:
movsb ; Move String Byte
equivale a:
movs byte [es:di], [ds:si]
movsw ; Move String Word
equivale a:
movs word [es:di], [ds:si]
movsd ; Move String Dword
equivale a:
movs dword [es:di], [ds:si]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi
previsti per MOVS.
Se vogliamo applicare il segment override all'operando SRC di MOVSB,
MOVSW e MOVSD, possiamo utilizzare la seguente sintassi supportata
da NASM:
gs movsd
Tale istruzione equivale a:
movs dword [es:di], [gs:si]
Come è stato già spiegato, il programmatore può solo indicare un registro di segmento
diverso da quello predefinito DS; l'uso del registro puntatore SI è,
invece, obbligatorio.
22.4.2 Effetti provocati da MOVS, MOVSB, MOVSW e MOVSD,
sugli operandi e sui flags
L'esecuzione delle istruzioni MOVS, MOVSB, MOVSW e
MOVSD, modifica il contenuto della locazione di memoria puntata da
ES:DI; inoltre, il contenuto dei registri puntatori DI e SI
viene aggiornato in base allo stato del flag DF. Il contenuto
dell'operando SRC rimane inalterato.
L'esecuzione delle istruzioni MOVS, MOVSB, MOVSW e
MOVSD, non modifica alcun campo del Flags Register.
22.5 Le istruzioni
SCAS,
SCASB,
SCASW,
SCASD
Con il mnemonico SCAS si indica l'istruzione Scan String (scansione
di una stringa); le uniche forme lecite per questa istruzione, sono le seguenti:
Nella prima forma, l'istruzione SCAS richiede esplicitamente un operando
DEST che rappresenta una locazione di memoria; l'operando SRC è
implicitamente il registro accumulatore.
L'operando DEST deve essere indirizzato obbligatoriamente con ES:DI;
a seconda dell'ampiezza in bit (8, 16 o 32) di DEST,
la CPU decide se utilizzare AL, AX o EAX come operando
SRC.
Siccome la coppia ES:DI non specifica la dimensione in bit della locazione
di memoria a cui punta, dobbiamo servirci necessariamente degli address size
operators; si possono presentare allora i seguenti tre casi:
- Scansione di un BYTE puntato da [ES:DI]
scas byte [es:di] ; codice macchina AEh
Quando la CPU incontra il codice macchina AEh, calcola:
Temp8 = AL - [ES:DI]
Questa comparazione aritmetica provoca la modifica dei flags OF, SF,
ZF, AF, PF e CF; consultando tali flags si può
conoscere il risultato della comparazione stessa.
Successivamente, la CPU aggiorna il puntatore DI in base allo stato
del flag DF. Se DF=0, il contenuto di DI viene incrementato
di 1; se DF=1, il contenuto di DI viene, invece, decrementato
di 1.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
cmp al, [es:di]
- Scansione di una WORD puntata da [ES:DI]
scas word [es:di] ; codice macchina AFh
Quando la CPU incontra il codice macchina AFh, calcola:
Temp16 = AX - [ES:DI]
Questa comparazione aritmetica provoca la modifica dei flags OF, SF,
ZF, AF, PF e CF; consultando tali flags si può
conoscere il risultato della comparazione stessa.
Successivamente, la CPU aggiorna il puntatore DI in base allo stato
del flag DF. Se DF=0, il contenuto di DI viene incrementato
di 2; se DF=1, il contenuto di DI viene, invece, decrementato
di 2.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
cmp ax, [es:di]
- Scansione di una DWORD puntata da [ES:DI]
scas dword [es:di] ; codice macchina 66h AFh
Quando la CPU incontra il codice macchina 66h AFh, calcola:
Temp32 = EAX - [ES:DI]
Questa comparazione aritmetica provoca la modifica dei flags OF, SF,
ZF, AF, PF e CF; consultando tali flags si può
conoscere il risultato della comparazione stessa.
Successivamente, la CPU aggiorna il puntatore DI in base allo stato
del flag DF. Se DF=0, il contenuto di DI viene incrementato
di 4; se DF=1, il contenuto di DI viene, invece, decrementato
di 4.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
cmp eax, [es:di]
L'istruzione SCAS determina una associazione predefinita tra DI e
ES; di conseguenza, se scriviamo:
scas word [di]
viene automaticamente utilizzato ES come registro di segmento!
Il segment override è proibito; se proviamo quindi a scrivere istruzioni del
tipo:
scas byte [ds:di]
otteniamo un messaggio di errore da parte dell'assembler!
In presenza, invece, di istruzioni del tipo:
scas dword [cx]
o
scas byte [es:ax]
viene ugualmente utilizzata la coppia predefinita ES:DI; in casi del genere,
molti assembler non mostrano alcun messaggio di errore!
Ciò è una diretta conseguenza del fatto che, il codice macchina di SCAS,
ha il solo scopo di specificare l'ampiezza in bit degli operandi; tutte le altre
informazioni non sono necessarie e, di fatto, vengono ignorate dall'assembler!
Usualmente, l'istruzione SCAS viene utilizzata per la ricerca di un
"pattern" (modello o schema) all'interno di una stringa; in sostanza,
attraverso SCAS possiamo sapere se un determinato valore binario è
presente o no in una stringa.
Come esempio pratico, supponiamo di voler calcolare la lunghezza della seguente
stringa C definita in un segmento DATASEGM referenziato da
DS:
strByte db 'Stringa C da esaminare', 0
In un caso del genere, il pattern da cercare è ovviamente lo zero finale; possiamo
scrivere allora il seguente codice:
In questo esempio, l'istruzione SCAS scandisce l'intera stringa strByte
alla ricerca di un elemento di valore 0 (che termina la stringa stessa); se
la comparazione produce un risultato diverso da zero (ZF=0), il loop viene
ripetuto (JNZ).
Quando viene trovato il valore 0, la comparazione produce ZF=1 e il
loop termina; a questo punto, DI punta alla fine della stringa più 1
(per l'ultimo incremento effettuato da SCAS). Sottraendo da DI l'offset
di strByte più 1, otteniamo quindi la lunghezza (escluso lo zero finale)
della stessa stringa strByte!
Osserviamo che per una stringa di lunghezza nulla, viene effettuata una sola
iterazione, con DI che subisce quindi un unico incremento; alla fine si
ottiene correttamente:
DI - (offset strByte + 1) = 0
22.5.1 Forme implicite dell'istruzione SCAS
In virtù del fatto che gli operandi obbligatori dell'istruzione SCAS sono
ES:DI e AL/AX/EAX, vengono resi disponibili i mnemonici SCASB,
SCASW e SCASD, che evitano al programmatore di dover scrivere ogni
volta l'operando DEST; come si può facilmente intuire:
scasb ; Scan String Byte
equivale a:
scas byte [es:di]
scasw ; Scan String Word
equivale a:
scas word [es:di]
scasd ; Scan String Dword
equivale a:
scas dword [es:di]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi
previsti per SCAS.
22.5.2 Effetti provocati da SCAS, SCASB, SCASW e SCASD,
sugli operandi e sui flags
L'esecuzione delle istruzioni SCAS, SCASB, SCASW e SCASD,
non modifica alcun operando in quanto il risultato della comparazione viene scartato;
il contenuto del registro puntatore DI viene aggiornato in base allo stato del
flag DF
L'esecuzione delle istruzioni SCAS, SCASB, SCASW e SCASD,
modifica i campi OF, SF, ZF, AF, PF e CF del
Flags Register.
22.6 Le istruzioni
CMPS,
CMPSB,
CMPSW,
CMPSD
Con il mnemonico CMPS si indica l'istruzione Compare String Operands
(comparazione tra due stringhe); le uniche forme lecite per questa istruzione,
sono le seguenti:
Nella prima forma, l'istruzione CMPS richiede esplicitamente entrambi gli
operandi SRC e DEST, che rappresentano delle locazioni di memoria;
l'operando DEST deve essere indirizzato obbligatoriamente con la coppia
ES:DI. Per l'indirizzamento dell'operando SRC è obbligatorio l'uso
del registro puntatore SI; in assenza di diverse indicazioni da parte del
programmatore, SI viene automaticamente associato a DS.
Siccome le coppie ES:DI e DS:SI non specificano la dimensione in bit
delle locazioni di memoria a cui puntano, dobbiamo servirci necessariamente degli
address size operators (da applicare ad uno solo dei due operandi); si
possono presentare allora i seguenti tre casi:
- Comparazione tra i BYTE puntati da [DS:SI] e [ES:DI]
cmps byte [ds:si], [es:di] ; codice macchina A6h
Quando la CPU incontra il codice macchina A6h, calcola:
Temp8 = BYTE [DS:SI] - [ES:DI]
Questa comparazione aritmetica provoca la modifica dei flags OF, SF,
ZF, AF, PF e CF; consultando tali flags si può
conoscere il risultato della comparazione stessa.
Successivamente, la CPU aggiorna i puntatori DI e SI in base
allo stato del flag DF. Se DF=0, il contenuto di DI e SI
viene incrementato di 1; se DF=1, il contenuto di DI e
SI viene, invece, decrementato di 1.
Trascurando l'aggiornamento di DI e SI, la precedente istruzione
equivale a:
- Comparazione tra le WORD puntate da [DS:SI] e [ES:DI]
cmps word [ds:si], [es:di] ; codice macchina A7h
Quando la CPU incontra il codice macchina A7h, calcola:
Temp16 = WORD [DS:SI] - [ES:DI]
Questa comparazione aritmetica provoca la modifica dei flags OF, SF,
ZF, AF, PF e CF; consultando tali flags si può
conoscere il risultato della comparazione stessa.
Successivamente, la CPU aggiorna i puntatori DI e SI in base
allo stato del flag DF. Se DF=0, il contenuto di DI e SI
viene incrementato di 2; se DF=1, il contenuto di DI e
SI viene, invece, decrementato di 2.
Trascurando l'aggiornamento di DI e SI, la precedente istruzione
equivale a:
- Comparazione tra le DWORD puntate da [DS:SI] e [ES:DI]
cmps dword [ds:si], [es:di] ; codice macchina 66h A7h
Quando la CPU incontra il codice macchina 66h A7h, calcola:
Temp32 = DWORD [DS:SI] - [ES:DI]
Questa comparazione aritmetica provoca la modifica dei flags OF, SF,
ZF, AF, PF e CF; consultando tali flags si può
conoscere il risultato della comparazione stessa.
Successivamente, la CPU aggiorna i puntatori DI e SI in base
allo stato del flag DF. Se DF=0, il contenuto di DI e SI
viene incrementato di 4; se DF=1, il contenuto di DI e
SI viene, invece, decrementato di 4.
Trascurando l'aggiornamento di DI e SI, la precedente istruzione
equivale a:
Apparentemente, CMPS sembra un'istruzione per la comparazione tra due
operandi di tipo Mem; in realtà, la comparazione viene effettuata con
un passaggio intermedio che coinvolge i registri temporanei della CPU!
L'istruzione CMPS determina una associazione predefinita tra SI e
DS e tra DI e ES; di conseguenza, se scriviamo:
cmps byte [si], [di]
il registro puntatore SI viene automaticamente associato a DS, mentre
DI viene automaticamente associato a ES!
L'uso dell'istruzione CMPS presenta un potenziale problema legato al
fatto che DS viene generalmente utilizzato per referenziare il segmento
dati principale di un programma; può capitare allora che l'impiego di CMPS
renda necessaria la modifica del contenuto dello stesso DS!
Naturalmente, in un caso del genere bisogna prendere tutte le opportune
precauzioni (che consistono nel preservare il contenuto originale di DS);
proprio per venire incontro alle esigenze del programmatore, l'istruzione
CMPS permette il segment override, esclusivamente per l'operando
SRC.
In pratica, al posto di DS possiamo specificare esplicitamente un diverso
registro di segmento (CS, ES, FS, GS o SS); in
questo modo, se la nostra intenzione è quella di non modificare DS, possiamo
scrivere istruzioni del tipo:
cmps word [gs:si], [es:di]
Il segment override relativo all'operando DEST è proibito; se proviamo
quindi a scrivere istruzioni del tipo:
cmps word [ds:si], [fs:di]
otteniamo un messaggio di errore da parte dell'assembler!
In presenza, invece, di istruzioni del tipo:
cmps dword [cx], [ax]
o
cmps byte [gs:dx], [bx]
vengono ugualmente utilizzati i puntatori predefiniti DI e SI; in
casi del genere, molti assembler non mostrano alcun messaggio di errore!
Ciò è una diretta conseguenza del fatto che, il codice macchina di CMPS,
ha il solo scopo di specificare l'ampiezza in bit degli operandi e la presenza
di un eventuale segment override per SRC; tutte le altre informazioni non
sono necessarie e, di fatto, vengono ignorate dall'assembler!
Usualmente, l'istruzione CMPS viene utilizzata per confrontare due blocchi
di memoria; possiamo servirci quindi di CMPS per sapere, ad esempio, se
due file su disco coincidono, se due stringhe sono uguali, se una stringa contiene
al suo interno una determinata sottostringa e così via.
Come esempio pratico, supponiamo di avere un segmento dati DATASEGM
referenziato da DS; all'interno di DATASEGM sono presenti le
seguenti definizioni:
Vogliamo confrontare strSource con StrDest per sapere se le due
stringhe Pascal sono uguali; con l'ausilio dell'istruzione CMPS
possiamo scrivere il seguente codice:
La prima osservazione da fare riguarda il fatto che nel registro CX
viene caricato il BYTE di indice 0 di strSource (possiamo
anche servirci del BYTE di indice 0 di strDest); come
sappiamo, l'elemento di indice 0 di una stringa Pascal contiene
la lunghezza della stringa stessa.
All'interno del loop, le due stringhe vengono scandite elemento per elemento;
se CMPS trova due elementi che differiscono tra loro (ZF=0), le
due stringhe sono ovviamente diverse e, in tal caso, l'esecuzione salta
(JNZ) all'etichetta stringhe_diverse.
Osserviamo che alla prima iterazione, CMPS confronta le lunghezze delle
due stringhe (elementi di indice 0); appare ovvio che se le due lunghezze
non coincidono, le due stringhe sono diverse.
Se il loop termina regolarmente (CX=0), le due stringhe sono uguali!
Subito dopo l'etichetta stringhe_diverse, possiamo anche inserire il
codice necessario per conoscere l'indice degli elementi che, comparati tra
loro, hanno prodotto ZF=0 con conseguente uscita anticipata dal loop
(JNZ); per determinare tale informazione, ci basta utilizzare la solita
istruzione:
sub si, strSource + 1
Naturalmente, possiamo anche scrivere:
sub di, strDest + 1
In relazione ai registri di segmento coinvolti da CMPS, possiamo notare
che ci troviamo nella stessa situazione di MOVS; se abbiamo la necessità
di preservare il contenuto originale di tali registri, possiamo quindi seguire
lo stesso procedimento già illustrato per le istruzioni LODS e MOVS.
22.6.1 Forme implicite dell'istruzione CMPS
In virtù del fatto che l'istruzione CMPS utilizza la coppia obbligata
ES:DI per indirizzare l'operando DEST e la coppia predefinita
DS:SI per indirizzare l'operando SRC, vengono resi disponibili
i mnemonici CMPSB, CMPSW e CMPSD, che evitano al programmatore
di dover scrivere ogni volta gli operandi SRC e DEST; come si può
facilmente intuire:
cmpsb ; Compare String Byte
equivale a:
cmps byte [ds:si], [es:di]
cmpsw ; Compare String Word
equivale a:
cmps word [ds:si], [es:di]
cmpsd ; Compare String Dword
equivale a:
cmps dword [ds:si], [es:di]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi
previsti per CMPS.
Se vogliamo applicare il segment override all'operando SRC di CMPSB,
CMPSW e CMPSD, possiamo utilizzare la seguente sintassi supportata
da NASM:
fs cmpsb
Tale istruzione equivale a:
cmps byte [fs:si], [es:di]
Come è stato già spiegato, il programmatore può solo indicare un registro di segmento
diverso da quello predefinito DS; l'uso del registro puntatore SI è,
invece, obbligatorio.
22.6.2 Effetti provocati da CMPS, CMPSB, CMPSW e CMPSD,
sugli operandi e sui flags
L'esecuzione delle istruzioni CMPS, CMPSB, CMPSW e CMPSD,
non modifica alcun operando in quanto il risultato della comparazione viene scartato;
il contenuto dei registri puntatori DI e SI viene aggiornato in base
allo stato del flag DF
L'esecuzione delle istruzioni CMPS, CMPSB, CMPSW e CMPSD,
modifica i campi OF, SF, ZF, AF, PF e CF del
Flags Register.
22.7 Le istruzioni
INS,
INSB,
INSW,
INSD
Con il mnemonico INS si indica l'istruzione Input from Port to String
(trasferimento dati da una porta hardware ad una stringa); le uniche forme lecite
per questa istruzione, sono le seguenti:
Nella prima forma, l'istruzione INS richiede esplicitamente entrambi gli
operandi SRC e DEST; l'operando SRC è obbligatoriamente il
registro DX e contiene un indirizzo di porta (I/O address)
compreso tra 0 e 65535.
L'operando DEST rappresenta una locazione di memoria e deve essere
indirizzato obbligatoriamente con ES:DI; l'ampiezza in bit (8,
16 o 32) di DEST, rappresenta anche l'ampiezza in bit
del dato da leggere dalla porta hardware.
Siccome la coppia ES:DI non specifica la dimensione in bit della locazione
di memoria a cui punta, dobbiamo servirci necessariamente degli address size
operators; si possono presentare allora i seguenti tre casi:
- Scrittura in [ES:DI] di un BYTE letto dalla porta DX
ins byte [es:di], dx ; codice macchina 6Ch
Quando la CPU incontra il codice macchina 6Ch, legge un valore a
8 bit dalla porta hardware specificata da DX e lo copia nella
locazione di memoria puntata da ES:DI; subito dopo, la CPU aggiorna
il puntatore DI in base allo stato del flag DF. Se DF=0, il
contenuto di DI viene incrementato di 1; se DF=1, il
contenuto di DI viene, invece, decrementato di 1.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
- Scrittura in [ES:DI] di una WORD letta dalla porta DX
ins word [es:di], dx ; codice macchina 6Dh
Quando la CPU incontra il codice macchina 6Dh, legge un valore a
16 bit dalla porta hardware specificata da DX e lo copia nella
locazione di memoria puntata da ES:DI; subito dopo, la CPU aggiorna
il puntatore DI in base allo stato del flag DF. Se DF=0, il
contenuto di DI viene incrementato di 2; se DF=1, il
contenuto di DI viene, invece, decrementato di 2.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
- Scrittura in [ES:DI] di una DWORD letta dalla porta DX
ins dword [es:di], dx ; codice macchina 66h 6Dh
Quando la CPU incontra il codice macchina 66h 6Dh, legge un valore a
32 bit dalla porta hardware specificata da DX e lo copia nella
locazione di memoria puntata da ES:DI; subito dopo, la CPU aggiorna
il puntatore DI in base allo stato del flag DF. Se DF=0, il
contenuto di DI viene incrementato di 4; se DF=1, il contenuto
di DI viene, invece, decrementato di 4.
Trascurando l'aggiornamento di DI, la precedente istruzione equivale a:
L'istruzione INS determina una associazione predefinita tra DI e
ES; di conseguenza, se scriviamo:
ins word [di], dx
viene automaticamente utilizzato ES come registro di segmento!
Il segment override è proibito; se proviamo quindi a scrivere istruzioni del
tipo:
ins byte [ds:di], dx
otteniamo un messaggio di errore da parte dell'assembler!
In presenza, invece, di istruzioni del tipo:
ins word [bx], dx
o
ins word [es:ax], dx
viene ugualmente utilizzata la coppia predefinita ES:DI; in casi del genere,
molti assembler non mostrano alcun messaggio di errore!
Ciò è una diretta conseguenza del fatto che, il codice macchina di INS,
ha il solo scopo di specificare l'ampiezza in bit degli operandi; tutte le altre
informazioni non sono necessarie e, di fatto, vengono ignorate dall'assembler!
L'indirizzo della porta hardware da cui leggere i dati, deve trovarsi
obbligatoriamente nel registro DX (variable port); è proibito
quindi specificare la porta hardware attraverso un valore immediato di tipo
Imm8 (fixed port).
Si ricorda che tutte le CPU di classe inferiore all'80386,
indirizzano le porte hardware attraverso le prime 8 linee dell'Address
Bus (da A0 a A7); con tali CPU, il registro DX
deve quindi contenere un numero di porta compreso tra 0 e 255.
Le CPU di classe 80386 e superiori, invece, indirizzano le porte
hardware attraverso le prime 16 linee dell'Address Bus (da A0
a A15); con tali CPU, il registro DX deve quindi contenere
un numero di porta compreso tra 0 e 65535.
Usualmente, l'istruzione INS viene utilizzata in un loop per leggere
una sequenza di dati da una porta hardware; gli stessi dati, dopo essere stati
sottoposti ad eventuali elaborazioni, vengono poi memorizzati in una stringa.
Nella sezione Assembly Avanzato vedremo degli esempi pratici.
22.7.1 Forme implicite dell'istruzione INS
In virtù del fatto che gli operandi obbligatori dell'istruzione INS sono
ES:DI e DX, vengono resi disponibili i mnemonici INSB,
INSW e INSD, che evitano al programmatore di dover scrivere ogni
volta gli operandi SRC e DEST; come si può facilmente intuire:
insb ; Input from Port to String Byte
equivale a:
ins byte [es:di], dx
insw ; Input from Port to String Word
equivale a:
ins word [es:di], dx
insd ; Input from Port to String Dword
equivale a:
ins dword [es:di], dx
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi
previsti per INS.
22.7.2 Effetti provocati da INS, INSB, INSW e INSD,
sugli operandi e sui flags
L'esecuzione delle istruzioni INS, INSB, INSW e INSD,
modifica il contenuto della locazione di memoria puntata da ES:DI; inoltre,
il contenuto del registro puntatore DI viene aggiornato in base allo stato
del flag DF. Il contenuto dell'operando SRC rimane inalterato.
L'esecuzione delle istruzioni INS, INSB, INSW e INSD,
non modifica alcun campo del Flags Register.
22.8 Le istruzioni
OUTS,
OUTSB,
OUTSW,
OUTSD
Con il mnemonico OUTS si indica l'istruzione Output String to Port
(trasferimento dati da una stringa ad una porta hardware); le uniche forme lecite
per questa istruzione, sono le seguenti:
Nella prima forma, l'istruzione OUTS richiede esplicitamente entrambi gli
operandi SRC e DEST; l'operando DEST è obbligatoriamente il
registro DX e contiene un indirizzo di porta (I/O address)
compreso tra 0 e 65535.
L'operando SRC rappresenta una locazione di memoria e deve essere
indirizzato obbligatoriamente con il registro puntatore SI; in assenza di
diverse indicazioni da parte del programmatore, SI viene automaticamente
associato a DS.
L'ampiezza in bit (8, 16 o 32) di SRC, rappresenta
anche l'ampiezza in bit del dato da scrivere nella porta hardware; siccome la
coppia DS:SI non specifica la dimensione in bit della locazione di memoria
a cui punta, dobbiamo servirci necessariamente degli address size operators.
Si possono presentare allora i seguenti tre casi:
- Scrittura nella porta DX di un BYTE letto da [DS:SI]
outs dx, byte [ds:si] ; codice macchina 6Eh
Quando la CPU incontra il codice macchina 6Eh, legge un valore a
8 bit dalla locazione di memoria puntata da DS:SI e lo copia nella
porta hardware specificata da DX; subito dopo, la CPU aggiorna il
puntatore SI in base allo stato del flag DF. Se DF=0, il
contenuto di SI viene incrementato di 1; se DF=1, il
contenuto di SI viene, invece, decrementato di 1.
Trascurando l'aggiornamento di SI, la precedente istruzione equivale a:
- Scrittura nella porta DX di una WORD letta da [DS:SI]
outs dx, word [ds:si] ; codice macchina 6Fh
Quando la CPU incontra il codice macchina 6Fh, legge un valore a
16 bit dalla locazione di memoria puntata da DS:SI e lo copia nella
porta hardware specificata da DX; subito dopo, la CPU aggiorna il
puntatore SI in base allo stato del flag DF. Se DF=0, il
contenuto di SI viene incrementato di 2; se DF=1, il
contenuto di SI viene, invece, decrementato di 2.
Trascurando l'aggiornamento di SI, la precedente istruzione equivale a:
- Scrittura nella porta DX di una DWORD letta da [DS:SI]
outs dx, dword [ds:si] ; codice macchina 66h 6Fh
Quando la CPU incontra il codice macchina 66h 6Fh, legge un valore a
32 bit dalla locazione di memoria puntata da DS:SI e lo copia nella
porta hardware specificata da DX; subito dopo, la CPU aggiorna il
puntatore SI in base allo stato del flag DF. Se DF=0, il
contenuto di SI viene incrementato di 4; se DF=1, il
contenuto di SI viene, invece, decrementato di 4.
Trascurando l'aggiornamento di SI, la precedente istruzione equivale a:
L'istruzione OUTS determina una associazione predefinita tra SI e
DS; di conseguenza, se scriviamo:
outs dx, byte [si]
viene automaticamente utilizzato DS come registro di segmento!
L'uso dell'istruzione OUTS presenta un potenziale problema legato al
fatto che DS viene generalmente utilizzato per referenziare il segmento
dati principale di un programma; può capitare allora che l'impiego di OUTS
renda necessaria la modifica del contenuto dello stesso DS!
Naturalmente, in un caso del genere bisogna prendere tutte le opportune
precauzioni (che consistono nel preservare il contenuto originale di DS);
proprio per venire incontro alle esigenze del programmatore, l'istruzione
OUTS permette il segment override.
In pratica, al posto di DS possiamo specificare esplicitamente un diverso
registro di segmento (CS, ES, FS, GS o SS); in
questo modo, se la nostra intenzione è quella di non modificare DS, possiamo
scrivere istruzioni del tipo:
outs dx, dword [fs:si]
In presenza di istruzioni del tipo:
outs dx, word [bx]
o
outs dx, byte [gs:ax]
viene ugualmente utilizzato il puntatore predefinito SI; in casi del genere,
molti assembler non mostrano alcun messaggio di errore!
Ciò è una diretta conseguenza del fatto che, il codice macchina di OUTS,
ha il solo scopo di specificare l'ampiezza in bit degli operandi e la presenza
di un eventuale segment override; tutte le altre informazioni non sono necessarie
e, di fatto, vengono ignorate dall'assembler!
L'indirizzo della porta hardware in cui scrivere i dati, deve trovarsi
obbligatoriamente nel registro DX (variable port); è proibito
quindi specificare la porta hardware attraverso un valore immediato di tipo
Imm8 (fixed port).
Ricordiamo che tutte le CPU di classe inferiore all'80386,
indirizzano le porte hardware attraverso le prime 8 linee dell'Address
Bus (da A0 a A7); con tali CPU, il registro DX
deve quindi contenere un numero di porta compreso tra 0 e 255.
Le CPU di classe 80386 e superiori, invece, indirizzano le porte
hardware attraverso le prime 16 linee dell'Address Bus (da A0
a A15); con tali CPU, il registro DX deve quindi contenere
un numero di porta compreso tra 0 e 65535.
Usualmente, l'istruzione OUTS viene utilizzata in un loop per scrivere
una sequenza di dati in una porta hardware; prima di essere scritti nella porta
hardware, tali dati vengono letti da una stringa e sottoposti ad eventuali
elaborazioni. Nella sezione Assembly Avanzato vedremo degli esempi pratici.
22.8.1 Forme implicite dell'istruzione OUTS
In virtù del fatto che l'istruzione OUTS utilizza DX come operando
DEST e la coppia predefinita DS:SI per indirizzare l'operando
SRC, vengono resi disponibili i mnemonici OUTSB, OUTSW e
OUTSD, che evitano al programmatore di dover scrivere ogni volta gli
operandi SRC e DEST; come è facile intuire:
outsb ; Output String Byte to Port
equivale a:
outs dx, byte [ds:si]
outsw ; Output String Word to Port
equivale a:
outs dx, word [ds:si]
outsd ; Output String Dword to Port
equivale a:
outs dx, dword [ds:si]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi
previsti per OUTS.
Se vogliamo applicare il segment override all'operando SRC di OUTSB,
OUTSW e OUTSD, possiamo utilizzare la seguente sintassi supportata
da NASM:
gs outsb
Tale istruzione equivale a:
outs dx, byte [gs:si]
Come è stato già spiegato, il programmatore può solo indicare un registro di segmento
diverso da quello predefinito DS; l'uso del registro puntatore SI è,
invece, obbligatorio.
22.8.2 Effetti provocati da OUTS, OUTSB, OUTSW e OUTSD,
sugli operandi e sui flags
L'esecuzione delle istruzioni OUTS, OUTSB, OUTSW e OUTSD,
modifica il contenuto della porta hardware specificata da DX; inoltre, il
contenuto del registro puntatore SI viene aggiornato in base allo stato del
flag DF. Il contenuto dell'operando SRC rimane inalterato.
L'esecuzione delle istruzioni OUTS, OUTSB, OUTSW e OUTSD,
non modifica alcun campo del Flags Register.
22.9 I prefissi
REP,
REPE/REPZ,
REPNE/REPNZ
In base alle considerazioni esposte in questo capitolo, si capisce subito che
le istruzioni per le stringhe sono state concepite espressamente per operare su
blocchi di memoria di dimensioni medio grandi; nel caso, invece, di blocchi di
memoria di piccole dimensioni, queste istruzioni devono essere sostituite con
altre equivalenti (illustrate nei precedenti capitoli), che permettono di scrivere
codice molto più compatto ed efficiente.
Un altro aspetto emerso in modo chiaro è dato dal fatto che per operare
efficacemente su grossi blocchi di dati, le istruzioni per le stringhe vengono
usualmente inserite all'interno di un loop; si tratta di una situazione talmente
frequente, da aver indotto i progettisti delle CPU a creare appositi prefissi
che, applicati a queste istruzioni, permettono al programmatore di automatizzare
alcune operazioni fondamentali per il controllo dei loop.
Complessivamente, sono disponibili tre prefissi i cui mnemonici (REP,
REPE/REPZ e REPNE/REPNZ) sono stati già anticipati nel Capitolo 11;
tali prefissi, come accade per tutti gli altri, devono essere disposti nella prima
parte del codice macchina di una istruzione.
In generale, con i mnemonici REP, REPE/REPZ e REPNE/REPNZ,
si indica l'istruzione Repeat String Operation Prefix (prefisso per l'iterazione
di una istruzione per le stringhe); analizziamo in dettaglio il significato di questi
tre mnemonici, che presentano notevoli analogie con le istruzioni LOOP,
LOOPE/LOOPZ e LOOPNE/LOOPNZ.
22.9.1 Il prefisso REP
Il prefisso REP è rappresentato dal codice macchina F3h e dice
alla CPU di iterare la susseguente istruzione per le stringhe, per un numero
di volte indicato dal registro contatore CX; questo prefisso presuppone che
CX sia stato inizializzato dal programmatore con un valore che rappresenta
il numero di iterazioni da effettuare.
Ogni volta che la CPU incontra il prefisso REP seguito da una
istruzione per le stringhe, esegue l'istruzione stessa (secondo il procedimento
già descritto in questo capitolo) e decrementa di 1 il contenuto di
CX (tale decremento non altera alcun campo del Flags Register);
successivamente, la CPU effettua una scelta basata sul risultato prodotto
dal decremento di CX:
- se CX è uguale a zero (condizione di uscita dal loop), l'esecuzione
prosegue sequenzialmente con l'istruzione successiva
- se CX è diverso da zero, l'istruzione per le stringhe viene
nuovamente eseguita
Indicando allora con XXXS il mnemonico di una qualsiasi istruzione per le
stringhe (in forma implicita) e ponendo CX=n, possiamo dire che
l'istruzione:
rep XXXS
consiste nel ripetere n volte l'istruzione XXXS; tutto ciò equivale
a scrivere:
Teoricamente, il prefisso REP può essere applicato a qualsiasi istruzione
per le stringhe; in pratica, però, si intuisce subito che alcune combinazioni
tra REP e istruzioni per le stringhe, sono totalmente prive di senso!
Poniamo, ad esempio, CX=350 e consideriamo l'istruzione:
rep cmpsw
Tale istruzione, compara un vettore di 350 WORD puntato da ES:DI,
con un vettore di 350 WORD puntato da DS:SI; come si può notare,
si tratta di una istruzione senza senso in quanto effettua in un colpo solo
350 comparazioni consecutive, senza che il programmatore abbia la
possibilità di conoscere il risultato di ogni singola comparazione!
Le uniche istruzioni per le stringhe che si possono efficacemente associare a
REP sono, STOS, LODS, MOVS, INS e OUTS;
in particolare, il prefisso REP diventa estremamente vantaggioso in
combinazione con MOVS.
Applichiamo REP all'esempio già presentato nella sezione 22.2 per
l'istruzione STOS; supponiamo quindi di voler inizializzare con il valore
8Fh, tutti i 256 elementi del seguente vettore di BYTE,
definito in un segmento DATASEGM referenziato da DS:
vectByte resb 256
Con l'ausilio di REP e STOSB possiamo scrivere il seguente codice:
Come si può notare, tutto il loop si riduce alla semplice istruzione:
rep stosb
Ad ogni iterazione, il valore 8Fh viene trasferito nella locazione
di memoria puntata da ES:DI e il registro DI viene poi
incrementato di 1; la stessa istruzione si occupa, inoltre, del
controllo del loop attraverso il decremento di CX.
Osserviamo che in questo esempio, l'utilizzo di REP è molto vantaggioso
in quanto dobbiamo copiare un unico valore inizializzante (8Fh) in tutti
i 256 elementi di strByte; se i valori inizializzanti fossero più
di uno, dovremmo ricorrere ad un loop ordinario, contenente l'istruzione
STOSB senza prefisso REP!
Un vettore di 256 elementi da 1 byte ciascuno, può essere visto
come vettore di 128 elementi da 1 word ciascuno; ponendo allora
AX=8F8Fh e CX=128, possiamo velocizzare il precedente codice
modificando il loop in questo modo:
rep stosw
Un vettore di 128 elementi da 1 word ciascuno, può essere visto
come vettore di 64 elementi da 1 dword ciascuno; ponendo allora
EAX=8F8F8F8Fh e CX=64, possiamo velocizzare il precedente codice
modificando il loop in questo modo:
rep stosd
Applichiamo REP all'esempio già presentato nella sezione 22.4 per
l'istruzione MOVS; supponiamo quindi di avere un segmento dati
DATASEGM referenziato da DS, al cui interno sono presenti le
seguenti definizioni:
Vogliamo copiare strSource in StrDest; con l'ausilio di REP
e MOVSB possiamo scrivere il seguente codice:
Un vettore di 40 elementi da 1 byte ciascuno, può essere visto
come vettore di 20 elementi da 1 word ciascuno; possiamo
velocizzare quindi il precedente codice con le seguenti modifiche:
Un vettore di 20 elementi da 1 word ciascuno, può essere visto
come vettore di 10 elementi da 1 dword ciascuno; possiamo
velocizzare quindi il precedente codice con le seguenti modifiche:
In modalità reale, un registro puntatore come DI o SI può
contenere un offset compreso tra 0000h e FFFFh; di conseguenza,
l'utilizzo di MOVS in combinazione con REP ci permette di
copiare in un colpo solo, un blocco di memoria grande sino a 65536
byte!
22.9.2 Il prefisso REPE/REPZ
Il prefisso REPE/REPZ è rappresentato dal codice macchina F3h e
dice alla CPU di iterare la susseguente istruzione per le stringhe, per
un numero di volte indicato dal registro contatore CX, purché sia verificata
la condizione ZF=1; questo prefisso presuppone che CX sia stato
inizializzato dal programmatore con un valore che rappresenta il numero di
iterazioni da effettuare.
Ogni volta che la CPU incontra il prefisso REPE/REPZ seguito da una
istruzione per le stringhe, esegue l'istruzione stessa (secondo il procedimento
già descritto in questo capitolo) e decrementa di 1 il contenuto di
CX (tale decremento non altera alcun campo del Flags Register);
successivamente, la CPU effettua una scelta basata sul risultato prodotto
dal decremento di CX e sul contenuto di ZF:
- se (CX == 0) OR (ZF == 0) (condizione di uscita dal loop), l'esecuzione
prosegue sequenzialmente con l'istruzione successiva
- se (CX != 0) AND (ZF == 1), l'istruzione per le stringhe viene
nuovamente eseguita
Siccome il decremento di CX non modifica alcun campo del Flags
Register, è evidente che la modifica di ZF deve essere provocata dalla
istruzione per le stringhe che segue REPE/REPZ.
I due mnemonici, REPE e REPZ, sono del tutto equivalenti; sappiamo,
infatti, che la condizione "repeat if zero" può essere espressa anche come
"repeat if equal".
Indicando allora con XXXS il mnemonico di una qualsiasi istruzione per le
stringhe (in forma implicita) e ponendo CX=n, possiamo dire che
l'istruzione:
repe XXXS
o
repz XXXS
consiste nel ripetere n volte l'istruzione XXXS, purché sia verificata
la condizione ZF=1; tutto ciò equivale a scrivere:
Teoricamente, il prefisso REPE/REPZ può essere applicato a qualsiasi
istruzione per le stringhe; in pratica, però, si intuisce subito che alcune
combinazioni tra REPE/REPZ e istruzioni per le stringhe, sono
totalmente prive di senso!
Osserviamo, ad esempio, che l'esecuzione delle istruzioni STOS,
LODS, MOVS, INS e OUTS, non altera alcun campo del
registro FLAGS; non avrebbe senso quindi utilizzare tali istruzioni in
combinazione con il prefisso REPE/REPZ!
Le uniche istruzioni per le stringhe che si possono efficacemente associare
a REPE/REPZ sono SCAS e CMPS; infatti, l'esecuzione di
tali istruzioni modifica diversi campi del registro FLAGS (in particolare,
ZF).
Applichiamo REPE/REPZ all'esempio già presentato nella sezione 22.6
per l'istruzione CMPS; supponiamo quindi di avere un segmento dati
DATASEGM referenziato da DS, all'interno del quale sono presenti le
seguenti definizioni:
Vogliamo confrontare strSource con StrDest per sapere se le
due stringhe Pascal sono uguali; con l'ausilio di REPE/REPZ e
CMPSB possiamo scrivere il seguente codice:
Come si può notare, tutto il loop si riduce alla semplice istruzione:
repe cmpsb
Ad ogni iterazione, il BYTE puntato da ES:DI viene comparato
con il BYTE puntato da DS:SI e i registri DI e SI
vengono poi incrementati di 1; la stessa istruzione si occupa, inoltre,
del controllo del loop attraverso il decremento di CX e il test su
ZF.
Se una qualunque comparazione produce ZF=0 (elementi diversi), il
loop termina in anticipo; in tal caso, JNZ provoca un salto all'etichetta
stringhe_diverse.
Se il loop termina regolarmente (CX=0), le due stringhe sono ovviamente
uguali; in tal caso, l'esecuzione prosegue a partire dall'etichetta
stringhe_uguali.
22.9.3 Il prefisso REPNE/REPNZ
Il prefisso REPNE/REPNZ è rappresentato dal codice macchina F2h e
dice alla CPU di iterare la susseguente istruzione per le stringhe, per
un numero di volte indicato dal registro contatore CX, purché sia verificata
la condizione ZF=0; questo prefisso presuppone che CX sia stato
inizializzato dal programmatore con un valore che rappresenta il numero di
iterazioni da effettuare.
Ogni volta che la CPU incontra il prefisso REPNE/REPNZ seguito da
una istruzione per le stringhe, esegue l'istruzione stessa (secondo il procedimento
già descritto in questo capitolo) e decrementa di 1 il contenuto di
CX (tale decremento non altera alcun campo del Flags Register);
successivamente, la CPU effettua una scelta basata sul risultato prodotto
dal decremento di CX e sul contenuto di ZF:
- se (CX == 0) OR (ZF == 1) (condizione di uscita dal loop), l'esecuzione
prosegue sequenzialmente con l'istruzione successiva
- se (CX != 0) AND (ZF == 0), l'istruzione per le stringhe viene
nuovamente eseguita
Siccome il decremento di CX non modifica alcun campo del Flags
Register, è evidente che la modifica di ZF deve essere provocata dalla
istruzione per le stringhe che segue REPNE/REPNZ.
I due mnemonici, REPNE e REPNZ, sono del tutto equivalenti; sappiamo,
infatti, che la condizione "repeat if not zero" può essere espressa anche
come "repeat if not equal".
Indicando allora con XXXS il mnemonico di una qualsiasi istruzione per le
stringhe (in forma implicita) e ponendo CX=n, possiamo dire che
l'istruzione:
repne XXXS
o
repnz XXXS
consiste nel ripetere n volte l'istruzione XXXS, purché sia verificata
la condizione ZF=0; tutto ciò equivale a scrivere:
In analogia a quanto è stato già esposto per REPE/REPZ, le uniche
istruzioni per le stringhe che si possono efficacemente associare a
REPNE/REPNZ sono SCAS e CMPS; infatti, l'esecuzione di
tali istruzioni modifica diversi campi del registro FLAGS (in particolare,
ZF).
Applichiamo REPNE/REPNZ all'esempio già presentato nella sezione 22.5
per l'istruzione SCAS; supponiamo quindi di voler calcolare la lunghezza
della seguente stringa C definita in un segmento DATASEGM referenziato
da DS:
strByte db 'Stringa C da esaminare', 0
Dobbiamo cioè cercare il pattern 00h nella stringa; con l'ausilio di
REPNE/REPNZ e SCASB, possiamo scrivere allora il seguente codice:
Come si può notare, tutto il loop si riduce alla semplice istruzione:
repne scasb
Ad ogni iterazione, il BYTE puntato da ES:DI viene comparato
con il contenuto (00h) di AL e il registro DI viene poi
incrementato di 1; la stessa istruzione si occupa, inoltre, del
controllo del loop attraverso il decremento di CX e il test su
ZF.
Quando viene raggiunta la fine della stringa ([ES:DI]=00h), la
comparazione produce ZF=1 (pattern trovato) e il loop termina; come
al solito, sottraendo da DI l'offset di strByte più 1,
otteniamo la lunghezza (escluso lo zero finale) della stessa stringa
strByte!
Osserviamo, inoltre, che nell'esempio appena presentato, ci interessa
principalmente il controllo del loop attraverso il test su ZF; al
registro CX viene quindi assegnato il massimo numero possibile di
iterazioni in modalità reale.
22.10 Istruzioni per le stringhe e registri di segmento
Per un programmatore Assembly, l'aspetto più delicato da affrontare
quando si utilizzano le istruzioni per le stringhe, è rappresentato dalla
eventuale necessità di preservare il contenuto originale dei registri di
segmento; infatti, come abbiamo visto in questo capitolo, l'impiego delle
istruzioni per le stringhe può rendere necessaria la modifica del contenuto
di determinati SegReg che stiamo già utilizzando per referenziare dei
segmenti di programma.
Gli esempi presentati in precedenza dovrebbero essere sufficienti per affrontare
questo tipo di problema; in ogni caso, per chiarire qualsiasi dubbio, analizziamo
un esempio pratico.
La Figura 22.10 mostra il listato di un programma chiamato STRINSTR.ASM; tale
programma utilizza diverse istruzioni per le stringhe, mostrando anche come ci si
deve comportare quando si presenta la necessità di preservare il contenuto di
determinati registri di segmento.