Assembly Base con MASM

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, offset 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:
strDword[0], strDword[4], strDword[8], ...
Nel caso di accesso diretto al vettore di Figura 22.2, dobbiamo ricordare che il nome strDword identifica dati di tipo DWORD; di conseguenza, dobbiamo servirci degli address size operators, scrivendo:
word ptr strDword[0], word ptr strDword[2], word ptr strDword[4], ...
Analogamente, per il vettore di Figura 22.3 dobbiamo scrivere:
byte ptr strDword[0], byte ptr strDword[1], byte ptr 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 ptr [BX+0], dword ptr [BX+4], dword ptr [BX+8], ...
Per il vettore di WORD di Figura 22.2, la sequenza è:
word ptr [BX+0], word ptr [BX+2], word ptr [BX+4], ...
Per il vettore di BYTE di Figura 22.3, la sequenza è:
byte ptr [BX+0], byte ptr [BX+1], byte ptr [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: 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: 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: 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, offset 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:
stos byte ptr 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
stos word ptr 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
stos dword ptr 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 ptr [di]
viene automaticamente utilizzato ES come registro di segmento!

Il segment override è proibito; se proviamo quindi a scrivere istruzioni del tipo:
stos byte ptr ds:[di]
otteniamo un messaggio di errore da parte dell'assembler!

In presenza, invece, di istruzioni del tipo:
stos word ptr [bx]
o
stos word ptr 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 db 256 dup (0)
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 ptr es:[di]
stosw ; Store String Word
equivale a:
stos word ptr es:[di]
stosd ; Store String Dword
equivale a:
stos dword ptr 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:
lods byte ptr 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]
lods word ptr 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]
lods dword ptr 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 ptr [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 ptr fs:[si]
In presenza di istruzioni del tipo:
lods word ptr [bx]
o
lods byte ptr 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 ptr ds:[si]
lodsw ; Load String Word
equivale a:
lods word ptr ds:[si]
lodsd ; Load String Dword
equivale a:
lods dword ptr ds:[si]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi previsti per LODS; inoltre, appare evidente che con LODSB, LODSW e LODSD, l'operando SRC può essere indirizzato esclusivamente con DS:SI.

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:
movs byte ptr 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:
movs word ptr 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:
movs dword ptr 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 ptr [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 ptr es:[di], fs:[si]
Il segment override relativo all'operando DEST è proibito; se proviamo quindi a scrivere istruzioni del tipo:
movs byte ptr fs:[di], ds:[si]
otteniamo un messaggio di errore da parte dell'assembler!

In presenza, invece, di istruzioni del tipo:
movs word ptr [cx], [dx]
o
movs word ptr [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 ptr es:[di], ds:[si]
movsw ; Move String Word
equivale a:
movs word ptr es:[di], ds:[si]
movsd ; Move String Dword
equivale a:
movs dword ptr es:[di], ds:[si]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi previsti per MOVS; inoltre, appare evidente che con MOVSB, MOVSW e MOVSD, l'operando SRC può essere indirizzato esclusivamente con DS:SI.

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:
scas byte ptr 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]
scas word ptr 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]
scas dword ptr 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 ptr [di]
viene automaticamente utilizzato ES come registro di segmento!

Il segment override è proibito; se proviamo quindi a scrivere istruzioni del tipo:
scas byte ptr ds:[di]
otteniamo un messaggio di errore da parte dell'assembler!

In presenza, invece, di istruzioni del tipo:
scas dword ptr [cx]
o
scas byte ptr 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 ptr es:[di]
scasw ; Scan String Word
equivale a:
scas word ptr es:[di]
scasd ; Scan String Dword
equivale a:
scas dword ptr 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:
cmps byte ptr ds:[si], es:[di] ; codice macchina A6h
Quando la CPU incontra il codice macchina A6h, calcola:
Temp8 = BYTE PTR 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:
cmps word ptr ds:[si], es:[di] ; codice macchina A7h
Quando la CPU incontra il codice macchina A7h, calcola:
Temp16 = WORD PTR 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:
cmps dword ptr ds:[si], es:[di] ; codice macchina 66h A7h
Quando la CPU incontra il codice macchina 66h A7h, calcola:
Temp32 = DWORD PTR 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 ptr [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 ptr gs:[si], es:[di]
Il segment override relativo all'operando DEST è proibito; se proviamo quindi a scrivere istruzioni del tipo:
cmps word ptr ds:[si], fs:[di]
otteniamo un messaggio di errore da parte dell'assembler!

In presenza, invece, di istruzioni del tipo:
cmps dword ptr [cx], [ax]
o
cmps byte ptr 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, offset strSource + 1
Naturalmente, possiamo anche scrivere:
sub di, offset 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 ptr ds:[si], es:[di]
cmpsw ; Compare String Word
equivale a:
cmps word ptr ds:[si], es:[di]
cmpsd ; Compare String Dword
equivale a:
cmps dword ptr ds:[si], es:[di]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi previsti per CMPS; inoltre, appare evidente che con CMPSB, CMPSW e CMPSD, l'operando SRC può essere indirizzato esclusivamente con DS:SI.

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:
ins byte ptr 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:
ins word ptr 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:
ins dword ptr 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 ptr [di], dx
viene automaticamente utilizzato ES come registro di segmento!

Il segment override è proibito; se proviamo quindi a scrivere istruzioni del tipo:
ins byte ptr ds:[di], dx
otteniamo un messaggio di errore da parte dell'assembler!

In presenza, invece, di istruzioni del tipo:
ins word ptr [bx], dx
o
ins word ptr 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 ptr es:[di], dx
insw ; Input from Port to String Word
equivale a:
ins word ptr es:[di], dx
insd ; Input from Port to String Dword
equivale a:
ins dword ptr 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:
outs dx, byte ptr 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:
outs dx, word ptr 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:
outs dx, dword ptr 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 ptr [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 ptr fs:[si]
In presenza di istruzioni del tipo:
outs dx, word ptr [bx]
o
outs dx, byte ptr 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 ptr ds:[si]
outsw ; Output String Word to Port
equivale a:
outs dx, word ptr ds:[si]
outsd ; Output String Dword to Port
equivale a:
outs dx, dword ptr ds:[si]
I codici macchina di questi tre mnemonici sono ovviamente gli stessi dei tre casi previsti per OUTS; inoltre, appare evidente che con OUTSB, OUTSW e OUTSD, l'operando SRC può essere indirizzato esclusivamente con DS:SI.

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: 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 db 256 dup (0)
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: 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: 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.