Assembly Base con MASM

Capitolo 20: Istruzioni per il trasferimento del controllo


Le CPU che equipaggiavano i primi calcolatori elettronici, erano dotate di un set di istruzioni molto limitato, che permetteva l'elaborazione dei programmi, esclusivamente in modo sequenziale; tale tipo di elaborazione consiste nel fatto che le varie istruzioni che compongono un programma, vengono eseguite ordinatamente, una di seguito all'altra, senza la possibilità di saltare da un punto all'altro del codice.
L'elaborazione sequenziale di un programma si svolge in modo estremamente semplice e presenta quindi il vantaggio di garantire la massima velocità di esecuzione possibile delle istruzioni; ciò è vero soprattutto per le CPU dotate di prefetch queue (coda di pre-carica). Infatti, in un precedente capitolo abbiamo visto che (semplificando al massimo), mentre la CPU sta elaborando una determinata istruzione (che possiamo chiamare I1), la logica di controllo (CL) provvede a pre-caricare l'istruzione successiva (che possiamo chiamare I2); quando la CPU ha finito di elaborare l'istruzione I1, trova già pronta l'istruzione I2, mentre la CL, a sua volta, provvede a pre-caricare l'istruzione successiva (che possiamo chiamare I3).
Lo svantaggio evidente della elaborazione sequenziale di un programma è rappresentato, invece, dalla totale assenza di flessibilità; dovendo scrivere, ad esempio, un programma sequenziale che deve ripetere per dieci volte uno stesso calcolo, siamo costretti a riscrivere per dieci volte consecutive, una stessa sequenza di istruzioni. Questo stile di programmazione (l'unico possibile con le vecchie CPU), viene appunto definito sequenziale; gli esempi presentati nei precedenti capitoli, per le istruzioni aritmetiche e logiche, seguono proprio lo stile di programmazione sequenziale.

Anziché riscrivere per dieci volte consecutive una stessa sequenza di istruzioni, appare più logico scriverla una sola volta, con la possibilità di rieseguire tale sequenza per il numero di volte necessario; questo differente stile di programmazione viene definito strutturato, in quanto permette di assegnare ad un programma, una struttura ben precisa attraverso la quale è possibile alterare il classico ordine sequenziale con cui vengono elaborate le varie istruzioni.

Tutte le CPU della famiglia 80x86, rendono disponibili una serie di istruzioni che permettono di programmare in stile strutturato attraverso il cosiddetto trasferimento del controllo; questa definizione è legata al fatto che sfruttando tali istruzioni, da un qualsiasi punto di un programma è possibile trasferire il controllo (cioè, saltare) ad un qualsiasi altro indirizzo di memoria!
Per capire l'importanza di questo aspetto, basti pensare al fatto che grazie alle istruzioni per il trasferimento del controllo, possiamo facilmente implementare costrutti fondamentali come i sottoprogrammi (procedure), le iterazioni (loop) e i salti (jumps) incondizionati o condizionati dal risultato di una operazione appena eseguita; questo discorso vale, non solo per l'Assembly, ma anche per i linguaggi di alto livello.
Consideriamo, ad esempio, la porzione di codice in linguaggio C illustrata in Figura 20.1: Questo codice "intelligente" è in grado di compiere tre scelte distinte in base al risultato di una comparazione aritmetica (CMP) tra i due dati numerici var1 e var2; in particolare: Per implementare il "blocco condizionale" if - else if - else di Figura 20.1, il compilatore C sfrutta proprio (oltre a CMP) le istruzioni per il trasferimento del controllo fornite dalla CPU; lo stesso discorso vale per le iterazioni (loop), come quella mostrata in Figura 20.2. Il loop di Figura 20.2 viene controllato dalla variabile contatore i e consiste nell'eseguire per 10 volte consecutive, le due istruzioni comprese tra parentesi graffe; infatti, osserviamo che i parte dal valore 0 (inizializzazione) e viene incrementata di 1 ad ogni ciclo (incremento del contatore), sino a raggiungere il valore 10 (condizione di uscita dal loop).

Un programma che fa un uso massiccio di sottoprogrammi, blocchi condizionali e loop, può assumere una struttura piuttosto contorta, che porta ad una inevitabile diminuzione delle prestazioni in termini di velocità di esecuzione; questo problema diventa particolarmente evidente nel caso CPU dotate di prefetch queue. Abbiamo appena visto, infatti, che mentre la CPU sta elaborando una determinata istruzione I1, la CL provvede a pre-caricare l'istruzione successiva I2; se, però, I1 è una istruzione che prevede un salto, tutto il lavoro svolto dalla CL per pre-caricare l'istruzione I2, si rivela totalmente inutile!
In un caso del genere, la CL provvede a svuotare la prefetch queue e a ricaricarla con l'istruzione che si trova all'indirizzo di destinazione del salto; come si può facilmente immaginare, tutta la fase di svuotamento e ricarica della prefetch queue, comporta un serio rallentamento nella fase di esecuzione di un programma. Si tratta di un problema talmente grave, da aver indotto i progettisti delle CPU ad escogitare svariate soluzioni come, la cache memory, la doppia pipeline, etc; questi aspetti sono stati già esaminati nel Capitolo 10.

In sostanza, se vogliamo spingere al massimo la velocità dei nostri programmi, dobbiamo puntare il più possibile sullo stile di programmazione sequenziale; se, invece, vogliamo scrivere programmi compatti e flessibili, destinati ad eseguire compiti molto complessi, dobbiamo ricorrere necessariamente ad uno stile di programmazione strutturato. È chiaro che se si riesce a trovare un giusto equilibrio tra queste due esigenze, è possibile scrivere programmi che, pur essendo di tipo strutturato, vengono eseguiti molto velocemente dalla CPU; per raggiungere un tale obbiettivo, è necessaria una conoscenza approfondita delle varie istruzioni per il trasferimento del controllo.

20.1 Nozioni generali sulle istruzioni di salto

Appare intuitivo il fatto che per poter trasferire il controllo (saltare) da una "origine" ad una "destinazione", la CPU deve caricare nella coppia CS:IP, l'indirizzo logico Seg:Offset della stessa "destinazione"; in questo modo, la prossima istruzione che verrà eseguita sarà proprio quella che si trova all'indirizzo di destinazione del salto.
Nel caso generale, il trasferimento del controllo può svolgersi secondo quattro modalità distinte che vengono illustrate in dettaglio nel seguito; come al solito, tutte le considerazioni esposte in questo capitolo e nei capitoli successivi, sono riferite, esclusivamente, alla modalità di indirizzamento reale (e a segmenti di programma con attributo USE16).

20.1.1 Salto diretto intrasegmento

Si ha il salto intrasegmento quando l'indirizzo di origine e l'indirizzo di destinazione del salto, si trovano all'interno dello stesso segmento di programma; questa situazione si verifica, ad esempio, quando vogliamo saltare dall'offset 03BFh all'offset 083Dh di uno stesso segmento di programma chiamato CODESEGM.
Per poter effettuare un salto intrasegmento, la CPU ha la necessità di modificare solamente il contenuto di IP, lasciando invariato il contenuto di CS; infatti, l'indirizzo logico di origine e l'indirizzo logico di destinazione, hanno la stessa componente Seg.
In sostanza, l'indirizzo di destinazione di un salto intrasegmento può essere specificato attraverso la sola componente Offset (la componente Seg si trova, implicitamente, in CS), ed è quindi un indirizzo di tipo NEAR (vicino); proprio per questo motivo, il salto intrasegmento viene anche definito NEAR jump (salto vicino).

Il salto intrasegmento viene definito diretto, quando specifichiamo direttamente l'indirizzo a cui la CPU deve saltare; il metodo più ovvio per indicare direttamente l'indirizzo di destinazione del salto, consiste nel servirsi di una etichetta (label).
Nel codice macchina di un salto diretto intrasegmento, l'assembler inserisce un valore relativo che rappresenta la distanza in byte tra l'offset di destinazione e l'offset successivo a quello dell'istruzione che origina il salto; nel seguito del capitolo e nei capitoli successivi, tale valore relativo verrà indicato simbolicamente con Rel.
Il valore Rel viene codificato come numero intero con segno a 16 bit in complemento a 2; per sottolineare questo aspetto, si utilizza il simbolo Rel16.

Come esempio pratico, supponiamo di voler saltare ad una etichetta dest_label che si trova all'offset 0CF2h di un segmento di codice chiamato CODESEGM; l'istruzione che origina il salto si trova all'offset 03B8h di CODESEGM e ha un codice macchina da 2 byte, per cui l'offset successivo è 03BAh.
Per ricavare la distanza in byte che esiste tra dest_label e l'offset 03BAh, l'assembler calcola:
0CF2h - 03BAh = 0938h
Nel codice macchina dell'istruzione di salto, l'assembler inserisce quindi un Rel16 pari a 0938h.
Osserviamo subito che, nel momento in cui la CPU incontra l'istruzione di salto, abbiamo CS=CODESEGM; dopo aver decodificato il codice macchina di questa istruzione, la CPU calcola:
0938h + 03BAh = 0CF2h
Successivamente, la CPU carica in IP il valore 0CF2h e salta a CS:IP (cioè, a CODESEGM:0CF2h), che è proprio l'indirizzo logico dell'etichetta dest_label; come si può notare, il contenuto (CODESEGM) di CS rimane invariato in quanto stiamo saltando da CODESEGM:03B8h a CODESEGM:0CF2h.

Supponiamo, invece, che l'etichetta dest_label si trovi all'offset 01CDh di un segmento di codice chiamato CODESEGM; l'istruzione che origina il salto si trova all'offset 0DA0h di CODESEGM e ha un codice macchina da 2 byte, per cui l'offset successivo è 0DA2h.
Questa volta, notiamo che il salto si svolge all'indietro in quanto dest_label precede l'istruzione che origina il salto!
Per ricavare la distanza in byte che esiste tra dest_label e l'offset 0DA2h, l'assembler calcola:
01CDh - 0DA2h = 461 - 3490 = -3029 = 216 - 3029 = 62507 = F42Bh
Nel codice macchina dell'istruzione di salto, l'assembler inserisce quindi un Rel16 pari a F42Bh.
Osserviamo subito che, nel momento in cui la CPU incontra l'istruzione di salto, abbiamo CS=CODESEGM; dopo aver decodificato il codice macchina di questa istruzione, la CPU calcola:
F42Bh + 0DA2h = 01CDh
Successivamente, la CPU carica in IP il valore 01CDh e salta a CS:IP (cioè, a CODESEGM:01CDh), che è proprio l'indirizzo logico dell'etichetta dest_label; come si può notare, il contenuto (CODESEGM) di CS rimane invariato in quanto stiamo saltando da CODESEGM:0DA0h a CODESEGM:01CDh.

Può capitare che in un NEAR jump, la distanza tra l'offset di destinazione e l'offset successivo a quello dell'istruzione che origina il salto, sia compresa tra -128 e +127 byte; in un caso del genere, il NEAR jump viene anche definito SHORT jump (salto corto).
Quando si verifica questa condizione, il valore Rel relativo allo SHORT jump può essere rappresentato con soli 8 bit; per indicare un Rel a 8 bit viene utilizzato il simbolo Rel8. Come si può facilmente intuire, gli SHORT jumps (quando sono possibili) permettono di ottenere un codice macchina più compatto; infatti, un Rel16 occupa 2 byte, mentre un Rel8 occupa 1 solo byte.

20.1.2 Salto indiretto intrasegmento

Il salto intrasegmento viene definito indiretto quando specifichiamo l'offset di destinazione, indirettamente, attraverso un operando di tipo Reg16 o Mem16; questo è l'aspetto fondamentale che differenzia il salto diretto intrasegmento dal salto indiretto intrasegmento.
Un eventuale operando di tipo Reg16, non deve essere necessariamente un registro puntatore; infatti, lo scopo del Reg16 è solamente quello di contenere una componente Offset a 16 bit.

Per capire il meccanismo su cui si basa il salto indiretto intrasegmento, supponiamo di voler saltare ad una etichetta dest_label specificando il suo indirizzo (ad esempio, 0FA8h), indirettamente, attraverso il registro generale AX; in un caso del genere, dobbiamo scrivere innanzi tutto:
mov ax, offset dest_label
Questa istruzione carica in AX il valore 0FA8h; a questo punto, possiamo chiedere alla CPU di "saltare ad AX"!
Quando la CPU incontra una tale istruzione, legge il contenuto 0FA8h di AX, lo carica direttamente in IP e salta all'indirizzo logico CS:IP; come al solito, il contenuto di CS rimane invariato in quanto il salto è intrasegmento.

Come è stato già detto, al posto di un Reg16 possiamo anche utilizzare un Mem16; in riferimento al precedente esempio, dopo aver definito una WORD chiamata dest_addr, possiamo scrivere l'istruzione:
mov dest_addr, offset dest_label
Questa istruzione carica in dest_addr il valore 0FA8h; a questo punto, possiamo chiedere alla CPU di "saltare a dest_addr"!
Anche in questo caso, la CPU legge il contenuto 0FA8h di dest_addr, lo carica direttamente in IP e salta all'indirizzo logico CS:IP.

Il salto indiretto intrasegmento viene sempre trattato come un NEAR jump generico, anche quando l'offset di destinazione si trova ad una distanza compresa tra -128 e +127 byte dall'offset successivo a quello dell'istruzione che origina il salto; non avrebbe quindi alcun senso parlare di SHORT jump indiretto intrasegmento, in quanto tale concetto è legato, esclusivamente, al caso di un salto diretto intrasegmento esprimibile con un Rel8!

20.1.3 Salto diretto intersegmento

Si ha il salto intersegmento quando l'indirizzo di origine del salto si trova in un segmento di programma differente da quello che contiene l'indirizzo di destinazione; questa situazione si verifica, ad esempio, quando vogliamo saltare dall'offset 03BFh di un segmento di programma chiamato CODESEGM, all'offset 083Dh di un segmento di programma chiamato LIBCODE.
Per poter effettuare un salto intersegmento, la CPU ha la necessità di modificare, sia il contenuto di CS, sia il contenuto di IP; infatti, l'indirizzo logico di origine e l'indirizzo logico di destinazione, hanno componenti Seg differenti.
In sostanza, l'indirizzo di destinazione di un salto intersegmento deve essere necessariamente specificato attraverso una coppia completa Seg:Offset, ed è quindi un indirizzo di tipo FAR (lontano); proprio per questo motivo, il salto intersegmento viene anche definito FAR jump (salto lontano).

Il salto intersegmento viene definito diretto, quando specifichiamo direttamente l'indirizzo a cui la CPU deve saltare; come al solito, il metodo più ovvio per indicare direttamente l'indirizzo di destinazione del salto, consiste nel servirsi di una etichetta (label).
Nel codice macchina di un salto diretto intersegmento, l'assembler inserisce sempre un valore a 32 bit che rappresenta l'indirizzo logico completo Seg:Offset della destinazione del salto; nel rispetto della convenzione enunciata nel Capitolo 16.8 (disposizione in memoria degli indirizzi FAR), la componente Offset precede sempre la componente Seg. Nei manuali tecnici delle CPU (e anche nel seguito del capitolo), un generico indirizzo FAR, destinazione di un salto diretto intersegmento, viene indicato con il simbolo Ptr16:Ptr16; il simbolo Ptr sta per pointer (puntatore).

Come esempio pratico, consideriamo una istruzione di salto diretto intersegmento, che si trova all'offset 03BFh di un segmento di programma chiamato CODESEGM; il salto da effettuare consiste in un FAR jump ad una etichetta, dest_label, che si trova all'offset 083Dh di un segmento di programma chiamato LIBCODE.
Il valore Ptr16:Ptr16 ricavato dall'assembler, vale quindi LIBCODE:083Dh; a questo punto, possiamo chiedere alla CPU di "saltare a dest_label"!
Osserviamo subito che, nel momento in cui la CPU incontra l'istruzione di salto, abbiamo CS=CODESEGM; dopo aver decodificato il codice macchina di questa istruzione, la CPU pone CS=LIBCODE, IP=083Dh e salta all'indirizzo CS:IP (cioè, a LIBCODE:083Dh). Come si può notare, questa volta la CPU è costretta a modificare anche CS; infatti, stiamo saltando da CODESEGM:03BFh a LIBCODE:083Dh.

20.1.4 Salto indiretto intersegmento

Il salto intersegmento viene definito indiretto, quando specifichiamo l'indirizzo completo Seg:Offset di destinazione, indirettamente, attraverso un operando di tipo Mem16:Mem16; questo è l'unico aspetto che differenzia il salto diretto intersegmento dal salto indiretto intersegmento.
La locazione di memoria Mem16:Mem16 a 32 bit, deve contenere quindi un indirizzo logico completo Seg:Offset; nel rispetto della convenzione enunciata nel Capitolo 16.8 (disposizione in memoria degli indirizzi FAR), la componente Offset deve trovarsi, rigorosamente, nella WORD meno significativa dell'operando a 32 bit.
È proibito l'uso di una coppia Reg16:Reg16!

Riprendiamo il precedente esempio che si riferiva ad un salto diretto intersegmento da CODESEGM:03BFh ad una etichetta dest_label che si trova all'indirizzo LIBCODE:083Dh; vediamo ora come dobbiamo procedere se vogliamo effettuare un salto indiretto intersegmento.
Prima di tutto, definiamo una DWORD chiamata dest_addr; successivamente, scriviamo le seguenti istruzioni: A questo punto possiamo chiedere alla CPU di "saltare a dest_addr"!
Osserviamo subito che, nel momento in cui la CPU incontra l'istruzione di salto, abbiamo CS=CODESEGM. Dopo aver decodificato il codice macchina di questa istruzione, la CPU legge il contenuto (083Dh) della WORD bassa di dest_addr e lo copia in IP; successivamente, la CPU legge il contenuto (LIBCODE) della WORD alta di dest_addr e lo copia in CS. Infine, la CPU salta a CS:IP (cioè, a LIBCODE:083Dh).
Si noti che se non rispettiamo la convenzione sulla disposizione in memoria degli indirizzi FAR, la componente 083Dh viene copiata in CS, mentre la componente LIBCODE viene copiata in IP; in un caso del genere, la CPU salta ad un indirizzo senza senso e il programma va in crash!

20.2 Gli operatori di distanza

Nei limiti del possibile, l'assembler cerca di interpretare correttamente tutte le istruzioni di salto da convertire in codice macchina; in teoria, l'assembler è quindi in grado di distinguere in modo autonomo, tra SHORT jump, NEAR jump e FAR jump.
In presenza, ad esempio, di un salto diretto intrasegmento con distanza compresa tra -128 e +127 byte, l'assembler genera il codice macchina di uno SHORT jump; se, invece, la distanza è maggiore, l'assembler genera il codice macchina di un NEAR jump.
In ogni caso, le considerazioni esposte in precedenza sui vari tipi di salto, possono determinare situazioni equivoche, tali da trarre in inganno l'assembler; ciò è vero, in particolare, per il salto di tipo diretto.
Nel caso del salto di tipo indiretto, l'assembler non incontra alcuna difficoltà in quanto stiamo specificando, in modo esplicito, l'ampiezza in bit dell'indirizzo di destinazione del salto; infatti, un operando di tipo Reg16 o Mem16 indica esplicitamente un salto NEAR indiretto, mentre un operando di tipo Mem16:Mem16 indica esplicitamente un salto FAR indiretto. In caso di necessità, possiamo anche servirci degli operatori WORD PTR e DWORD PTR; come sappiamo, l'operatore WORD PTR indica che il suo operando ha una ampiezza di 16 bit, mentre l'operatore DWORD PTR indica che il suo operando ha una ampiezza di 32 bit.
Nel caso del salto di tipo diretto, la situazione è più delicata; infatti, in mancanza di diverse indicazioni da parte del programmatore, gli assembler come MASM assumono implicitamente che i salti di tipo diretto siano intrasegmento. Se, invece, vogliamo effettuare un salto diretto intersegmento, dobbiamo indicarlo in modo esplicito all'assembler; in caso contrario, viene generato un messaggio di errore!
Per indicare in modo esplicito il tipo di salto diretto che vogliamo effettuare, vengono resi disponibili i cosiddetti distance operators (operatori di distanza), illustrati in Figura 20.3; è importante ribadire che questi operatori devono essere impiegati, esclusivamente, con i salti di tipo diretto. Nel caso di un salto diretto intrasegmento, dest è una label definita all'interno dello stesso segmento di programma che contiene l'istruzione di salto; come è stato detto in precedenza, MASM gestisce correttamente questa situazione ed è anche in grado di generare il codice macchina più opportuno (SHORT jump o NEAR jump).
Se conosciamo con certezza la distanza del salto diretto intrasegmento, possiamo anche servirci in modo esplicito degli operatori SHORT o NEAR PTR.

Nel caso di un salto diretto intersegmento, dest è una label definita in un segmento di programma differente da quello che contiene l'istruzione di salto; come è stato detto in precedenza, in una situazione di questo genere, gli assembler come MASM richiedono che il programmatore indichi esplicitamente la presenza di un FAR jump. Ovviamente, per indicare all'assembler che vogliamo effettuare un salto diretto intersegmento, dobbiamo servirci dell'operatore FAR PTR.

Nel seguito del capitolo, verranno presentati numerosi esempi che chiariranno tutti questi aspetti.

20.3 L'istruzione JMP

Con il mnemonico JMP si indica l'istruzione Unconditional Jump (salto incondizionato); lo scopo di questa istruzione è quello di costringere la CPU a saltare, senza condizioni, ad un determinato indirizzo di memoria Seg:Offset specificato dal programmatore attraverso un apposito operando esplicito (DEST).
In modalità reale, le uniche forme lecite per l'istruzione JMP sono le seguenti: Come si può notare, sono permessi tutti i tipi di salto descritti nella sezione 20.1; i codici macchina relativi ai vari casi, sono i seguenti: Il programma JMP.ASM di Figura 20.4, illustra l'uso pratico dell'istruzione JMP. Nei commenti che accompagnano il listato di Figura 20.4, notiamo la presenza degli offset e dei codici macchina delle varie istruzioni; il simbolo (r) sta per relocatable (componente Seg o Offset rilocabile), mentre il simbolo (e) sta per extern (identificatore definito in un modulo esterno).
Avendo a disposizione queste informazioni, possiamo seguire, istante per istante, le varie posizioni che vengono assunte dall'instruction pointer durante la fase di elaborazione del programma; tutti gli aspetti relativi al funzionamento dell'istruzione JMP valgono, in generale, anche per le altre istruzioni di salto, per cui il listato di Figura 20.4 verrà analizzato in estremo dettaglio.

Innanzi tutto, il programma stampa sullo schermo i valori assegnati dal SO a CODESEGM e LIBCODE; in questo modo, abbiamo la possibilità di conoscere il segmento di destinazione di ciascun salto. Come sappiamo, il valore che il SO assegna all'identificatore di un segmento di programma, rappresenta il segmento di memoria all'interno del quale è stato inserito lo stesso segmento di programma; se, ad esempio, il SO inserisce CODESEGM all'interno del segmento di memoria n. 16C8h, allora avremo CODESEGM=16C8h.

Partiamo dall'offset 003Dh di CODESEGM, dove notiamo la presenza di un JMP diretto intrasegmento verso l'offset 0069h dell'etichetta dest_label1; mentre la CPU elabora questa istruzione, IP è stato aggiornato all'offset 003Fh, per cui la distanza del salto è pari a:
0069h - 003Fh = 002Ah = +42
Si tratta quindi di uno SHORT jump il cui Rel8 vale +42; tale valore può essere memorizzato in soli 8 bit (2Ah) e, infatti, l'assembler genera il codice macchina:
EBh 2Ah
Se abbiamo la certezza che il salto è SHORT, possiamo anche scrivere, esplicitamente:
jmp short dest_label1
In presenza del precedente codice macchina, la CPU calcola:
2Ah + 3Fh = 69h
Il valore 69h viene caricato in IP e viene effettuato un salto a CS:IP (cioè, CODESEGM:0069h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta dest_label1!
Per dimostrare che il salto si svolge all'interno di CODESEGM, le istruzioni successive a dest_label1 visualizzano, oltre alla stringa strMsg1, gli indirizzi logici Seg:Offset dell'origine (orig_label1) e della destinazione del salto (dest_label1); in questo modo, possiamo constatare che per entrambi gli indirizzi, la componente Seg vale sempre CODESEGM.
Dall'indirizzo CODESEGM:00A0h viene effettuato un salto diretto intrasegmento verso l'offset 003Fh dell'etichetta return_label1; mentre la CPU elabora questa istruzione, IP è stato aggiornato all'offset 00A2h, per cui la distanza del salto è pari a:
003Fh - 00A2h = 63 - 162 = -99 = 28 - 99 = 157 = 9Dh
Si tratta quindi di uno SHORT jump il cui Rel8 vale -99; tale valore può essere memorizzato in soli 8 bit (9Dh) e, infatti, l'assembler genera il codice macchina:
EBh 9Dh
In presenza del precedente codice macchina, la CPU calcola:
9Dh + A2h = 3Fh
Il valore 3Fh viene caricato in IP e viene effettuato un salto a CS:IP (cioè, CODESEGM:003Fh); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta return_label1!

L'istruzione successiva a return_label1 consiste in un JMP diretto intrasegmento dall'offset 003Fh di CODESEGM, all'offset 0200h dell'etichetta dest_label2; mentre la CPU elabora questa istruzione, IP è stato aggiornato all'offset 0042h, per cui la distanza del salto è pari a:
0200h - 0042h = 01BEh = +446
Si tratta quindi di un NEAR jump il cui Rel16 vale +446; tale valore richiede almeno 16 bit (01BEh) e, infatti, l'assembler genera il codice macchina:
E9h 01BEh
Per fare in modo che la distanza del salto diretto intrasegmento superi i limiti di -128 e +127 byte, è stata utilizzata la direttiva ORG che sposta il location counter a CODESEGM:0200h; l'istruzione successiva a ORG parte quindi dall'offset 0200h (512) di CODESEGM.

Se abbiamo la certezza che il salto è NEAR, possiamo anche scrivere, esplicitamente:
jmp near dest_label2
In presenza del precedente codice macchina, la CPU calcola:
01BEh + 0042h = 0200h
Il valore 0200h viene caricato in IP e viene effettuato un salto a CS:IP (cioè, CODESEGM:0200h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta dest_label2!
Per dimostrare che il salto si svolge all'interno di CODESEGM, le istruzioni successive a dest_label2 visualizzano, oltre alla stringa strMsg2, gli indirizzi logici Seg:Offset dell'origine (orig_label2) e della destinazione del salto (dest_label2); in questo modo, possiamo constatare che per entrambi gli indirizzi, la componente Seg vale sempre CODESEGM.
Dall'indirizzo CODESEGM:0237h viene effettuato un salto diretto intrasegmento verso l'offset 0042h dell'etichetta return_label2; mentre la CPU elabora questa istruzione, IP è stato aggiornato all'offset 023Ah, per cui la distanza del salto è pari a:
0042h - 023Ah = 66 - 570 = -504 = 216 - 504 = 65032 = FE08h
Si tratta quindi di un NEAR jump il cui Rel16 vale -504; tale valore richiede almeno 16 bit (FE08h) e, infatti, l'assembler genera il codice macchina:
E9h FE08h
In presenza del precedente codice macchina, la CPU calcola:
FE08h + 023Ah = 0042h
Il valore 0042h viene caricato in IP e viene effettuato un salto a CS:IP (cioè, CODESEGM:0042h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta return_label2!

L'istruzione successiva a return_label2 consiste in un JMP indiretto intrasegmento dall'offset 0045h di CODESEGM, all'offset 023Ah dell'etichetta dest_label3; per rappresentare, indirettamente, l'offset 023Ah di dest_label3, viene utilizzato il registro CX.
Per il campo mod_100_r/m abbiamo mod=11b, r/m=001b (registro CX) e quindi:
mod_100_r/m = 11100001b = E1h
Il codice macchina generato dall'assembler è, infatti:
FFh E1h
(la presenza dell'operando CX rende esplicito il fatto che il salto è indiretto intrasegmento).

In presenza di questo codice macchina, la CPU legge il valore 023Ah da CX, lo carica direttamente in IP e salta a CS:IP (cioè, CODESEGM:023Ah); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta dest_label3!
Per dimostrare che il salto si svolge all'interno di CODESEGM, le istruzioni successive a dest_label3 visualizzano, oltre alla stringa strMsg3, gli indirizzi logici Seg:Offset dell'origine (orig_label3) e della destinazione del salto (dest_label3); in questo modo, possiamo constatare che per entrambi gli indirizzi, la componente Seg vale sempre CODESEGM.
Dall'indirizzo CODESEGM:0271h viene effettuato un salto diretto intrasegmento verso l'offset 0047h dell'etichetta return_label3; mentre la CPU elabora questa istruzione, IP è stato aggiornato all'offset 0274h, per cui la distanza del salto è pari a:
0047h - 0274h = 71 - 628 = -557 = 216 - 557 = 64979 = FDD3h
Si tratta quindi di un NEAR jump il cui Rel16 vale -557; tale valore richiede almeno 16 bit (FDD3h) e, infatti, l'assembler genera il codice macchina:
E9h FDD3h
In presenza del precedente codice macchina, la CPU calcola:
FDD3h + 0274h = 0047h
Il valore 0047h viene caricato in IP e viene effettuato un salto a CS:IP (cioè, CODESEGM:0047h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta return_label3!

L'istruzione successiva a return_label3 consiste in un JMP indiretto intrasegmento dall'offset 004Dh di CODESEGM, all'offset 0274h dell'etichetta dest_label4; per rappresentare, indirettamente, l'offset 0274h di dest_label4, viene utilizzato il dato label_addr16 di tipo WORD.
Per il campo mod_100_r/m abbiamo mod=00b, r/m=110b (operando di tipo Disp) e quindi:
mod_100_r/m = 00100110b = 26h
L'offset di label_addr16 è 0000h, per cui l'assembler genera il codice macchina:
FFh 26h 0000h
Se vogliamo enfatizzare il fatto che label_addr16 è un operando a 16 bit contenente la destinazione di un salto NEAR, possiamo anche scrivere:
jmp word ptr label_addr16
In presenza del precedente codice macchina, la CPU accede all'indirizzo DS:0000h, legge il valore 0274h, lo carica direttamente in IP e salta a CS:IP (cioè, CODESEGM:0274h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta dest_label4!
Per dimostrare che il salto si svolge all'interno di CODESEGM, le istruzioni successive a dest_label4 visualizzano, oltre alla stringa strMsg4, gli indirizzi logici Seg:Offset dell'origine (orig_label4) e della destinazione del salto (dest_label4); in questo modo, possiamo constatare che per entrambi gli indirizzi, la componente Seg vale sempre CODESEGM.
Dall'indirizzo CODESEGM:02ABh viene effettuato un salto diretto intrasegmento verso l'offset 0051h dell'etichetta return_label4; mentre la CPU elabora questa istruzione, IP è stato aggiornato all'offset 02AEh, per cui la distanza del salto è pari a:
0051h - 02AEh = 81 - 686 = -605 = 216 - 605 = 64931 = FDA3h
Si tratta quindi di un NEAR jump il cui Rel16 vale -605; tale valore richiede almeno 16 bit (FDA3h) e, infatti, l'assembler genera il codice macchina:
E9h FDA3h
In presenza del precedente codice macchina, la CPU calcola:
FDA3h + 02AEh = 0051h
Il valore 0051h viene caricato in IP e viene effettuato un salto a CS:IP (cioè, CODESEGM:0051h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta return_label4!

L'istruzione successiva a return_label4 consiste in un JMP diretto intersegmento dall'indirizzo CODESEGM:0051h all'indirizzo LIBCODE:0000h dell'etichetta dest_label5; la coppia Ptr16:Ptr16 ricavata dall'assembler è appunto LIBCODE:0000h e quindi, per questo FAR jump diretto, l'assembler genera il codice macchina:
EAh ----h:0000h
Ricordiamo che l'assembler non è in grado di conoscere in anticipo il valore che il SO assegnerà a LIBCODE, per cui viene utilizzata una componente Seg simbolica pari a ----h.
Osserviamo che, in questo caso (e in tutti i casi di salto diretto intersegmento), è necessario scrivere in modo esplicito:
jmp far ptr dest_label5
In presenza del precedente codice macchina, la CPU carica il valore 0000h in IP, il valore LIBCODE in CS e salta a CS:IP (cioè, LIBCODE:0000h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta dest_label5!
Per dimostrare che il salto si svolge da CODESEGM a LIBCODE, le istruzioni successive a dest_label5 visualizzano, oltre alla stringa strMsg5, gli indirizzi logici Seg:Offset dell'origine (orig_label5) e della destinazione del salto (dest_label5); in questo modo, possiamo constatare che per l'indirizzo di origine la componente Seg vale CODESEGM, mentre per l'indirizzo di destinazione la componente Seg vale LIBCODE.
Dall'indirizzo LIBCODE:0037h viene effettuato un salto diretto intersegmento verso l'indirizzo CODESEGM:0056h dell'etichetta return_label5; la coppia Ptr16:Ptr16 ricavata dall'assembler è appunto CODESEGM:0056h, e quindi, per questo FAR jump diretto, l'assembler genera il codice macchina:
EAh ----h:0056h
In presenza di questo codice macchina, la CPU carica il valore 0056h in IP, il valore CODESEGM in CS e salta a CS:IP (cioè, CODESEGM:0056h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta return_label5!

L'istruzione successiva a return_label5 consiste in un JMP indiretto intersegmento dall'indirizzo CODESEGM:0062h all'indirizzo LIBCODE:003Ch dell'etichetta dest_label6; per rappresentare, indirettamente, l'indirizzo LIBCODE:003Ch di dest_label6, viene utilizzato il dato label_addr32 di tipo DWORD.
Per il campo mod_101_r/m abbiamo mod=00b, r/m=110b (operando di tipo Disp), e quindi:
mod_101_r/m = 00101110b = 2Eh
L'offset di label_addr32 è 0002h, per cui l'assembler genera il codice macchina:
FFh 2Eh 0002h
Si noti che l'assembler non genera il prefisso 66h, che indica la presenza di operandi a 32 bit; infatti, è stato già sottolineato che in un segmento di programma con attributo USE16, una locazione di memoria a 32 bit, contenente la destinazione di un FAR jump indiretto, viene trattata come coppia Mem16:Mem16 (lo stesso discorso vale per il codice macchina di un FAR jump diretto)!
Se vogliamo enfatizzare il fatto che label_addr32 è un operando a 32 bit, possiamo anche scrivere:
jmp dword ptr label_addr32
In presenza del precedente codice macchina, la CPU accede all'indirizzo DS:0002h, legge la coppia LIBCODE:003Ch, la carica direttamente in CS:IP e salta a CS:IP (cioè, LIBCODE:003Ch); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta dest_label6!
Per dimostrare che il salto si svolge da CODESEGM a LIBCODE, le istruzioni successive a dest_label6 visualizzano, oltre alla stringa strMsg6, gli indirizzi logici Seg:Offset dell'origine (orig_label6) e della destinazione del salto (dest_label6); in questo modo, possiamo constatare che per l'indirizzo di origine la componente Seg vale CODESEGM, mentre per l'indirizzo di destinazione la componente Seg vale LIBCODE.
Dall'indirizzo LIBCODE:0073h viene effettuato un salto diretto intersegmento verso l'indirizzo CODESEGM:0066h dell'etichetta return_label6; la coppia Ptr16:Ptr16 ricavata dall'assembler è appunto CODESEGM:0066h e quindi, per questo FAR jump diretto, l'assembler genera il codice macchina:
EAh ----h:0066h
In presenza di questo codice macchina, la CPU carica il valore 0066h in IP, il valore CODESEGM in CS e salta a CS:IP (cioè, CODESEGM:0066h); come si può notare, si tratta proprio dell'indirizzo logico dell'etichetta return_label6!

L'istruzione successiva a return_label6 consiste in un normale NEAR jump all'etichetta exit_label, dopo la quale sono presenti le solite istruzioni per la terminazione del programma; in questo modo, evitiamo che vengano rieseguite le istruzioni successive a dest_label1, con conseguente salto a return_label1.
Come si può facilmente constatare, se dimentichiamo di eseguire il salto finale a exit_label, il programma continua a girare all'infinito (loop infinito) e deve essere terminato forzatamente; in un caso del genere, se si sta lavorando in ambiente DOS "puro", è necessario riavviare il computer!

20.3.1 Casi particolari

In riferimento al listato di Figura 20.4, cosa succede se non è presente la direttiva ASSUME che associa DATASEGM a DS?
Ovviamente, in un caso del genere dobbiamo ricorrere al segment override; questa necessità si presenta nei salti indiretti, che si servono di operandi definiti in DATASEGM.
Ad esempio, l'istruzione:
jmp label_addr16
deve essere riscritta, esplicitamente, come:
jmp ds:label_addr16
Questa istruzione indica chiaramente un salto indiretto intrasegmento ad un indirizzo Seg:Offset; la componente Seg è contenuta, implicitamente, in CS, mentre la componente Offset è contenuta, esplicitamente, in una locazione da 16 bit puntata da DS:label_addr16.

Sempre in riferimento al listato di Figura 20.4, cosa succede se, ad esempio, un operando di tipo Mem16:Mem16 (come label_addr32) è contenuto nella seconda DWORD del dato:
var64 dq 0
In un caso del genere, dobbiamo servirci dell'operatore DWORD PTR; possiamo scrivere quindi:
jmp dword ptr var64[4]
Questa istruzione indica chiaramente un salto indiretto intersegmento ad un indirizzo Seg:Offset contenuto nella locazione a 32 bit puntata da DS:var64[4].
Se, inoltre, non è presente la direttiva ASSUME che associa DATASEGM a DS, dobbiamo scrivere:
jmp dword ptr ds:var64[4]
Il significato di questa istruzione è identico a quello dell'istruzione precedente.

Dalle considerazioni appena esposte, si deduce facilmente un metodo per creare un vettore di indirizzi, ciascuno dei quali è la destinazione di un salto; a tale proposito, consideriamo il seguente dato:
vett_addr16 dw 10 dup (0)
Dopo aver caricato in ciascuna WORD di vett_addr16, gli offset di destinazione, possiamo effettuare dei salti indiretti intrasegmento, attraverso istruzioni del tipo:
jmp word ptr vett_addr16[0]
jmp word ptr vett_addr16[2]
jmp word ptr vett_addr16[4]
e così via.

Come si può notare, si tratta di applicare i soliti semplici concetti già esposti nei precedenti capitoli; l'importante è avere le idee chiare su ciò che si vuole fare.

20.3.2 Il salto incondizionato nei linguaggi di alto livello

Come molti avranno intuito, l'istruzione JMP viene utilizzata dai linguaggi di alto livello per implementare la "famigerata" istruzione GOTO; tale istruzione viene letteralmente "criminalizzata" da quasi tutti i libri sulla programmazione (spesso copiati l'uno dall'altro).
In effetti, in base ai concetti appena esposti, si capisce subito che è meglio non abusare dell'istruzione JMP; in caso contrario, si perviene facilmente alla scrittura di programmi particolarmente contorti (come l'esempio di Figura 20.4). Nei casi estremi, l'utilizzo eccessivo dei salti incondizionati, porta alla scrittura di programmi incomprensibili e quindi impossibili da sottoporre ad eventuali modifiche (o a cicli periodici di aggiornamento); questo problema ha assunto una certa gravità con l'avvento dei linguaggi come il BASIC che, incoraggiando un uso indiscriminato dell'istruzione GOTO, hanno favorito uno "stile" di programmazione universalmente conosciuto come "spaghetti code"!

È anche importante ribadire che ciascuna istruzione di salto, provoca un sensibile rallentamento del programma in esecuzione; vediamo un esempio pratico che si riferisce sempre al listato di Figura 20.4.
Supponiamo che la CPU stia elaborando l'istruzione:
jmp far ptr dest_label5
che si trova all'indirizzo CODESEGM:0051h; nel frattempo, la CL provvede a pre-caricare nella prefetch queue, il codice macchina B8h 003Ch dell'istruzione successiva:
mov ax, offset dest_label6
che si trova all'indirizzo CODESEGM:0056h (dando per scontato che si tratti della prossima istruzione da eseguire).
Purtroppo, però, l'istruzione presente all'indirizzo CODESEGM:0051h, prevede un salto all'indirizzo LIBCODE:0000h, per cui tutto il lavoro di pre-carica svolto dalla CL, si rivela inutile; in un caso del genere, la CL svuota la prefetch queue e la ricarica con il codice macchina BFh 012Ch dell'istruzione che si trova all'indirizzo LIBCODE:0000h.
A maggior ragione, è necessario quindi limitare al massimo l'utilizzo delle istruzioni di salto nei programmi scritti in stile strutturato; appare anche evidente che la situazione appena descritta, non può mai verificarsi in un programma scritto in stile sequenziale!

In ogni caso, la criminalizzazione dell'istruzione JMP appare del tutto ingiustificata; infatti, nel seguito del capitolo vedremo che in determinate situazioni l'istruzione JMP, se utilizzata in modo razionale, si rivela molto utile se non indispensabile.
Consideriamo subito un esempio pratico, relativo ad un programma eseguibile in formato COM; come sappiamo, in questo tipo di programmi è proibito inserire dati inizializzati prima dell'entry point (che deve trovarsi, rigorosamente, all'offset 0100h dell'unico segmento presente). Si presenta allora il problema di come evitare che la CPU finisca per sbaglio nel blocco contenente le definizioni dei vari dati del programma; un modo per risolvere tale problema, consiste nel ricorrere all'espediente mostrato in Figura 20.5. Come si può notare, subito dopo l'entry point è presente una istruzione JMP che provoca un salto incondizionato verso l'etichetta start_code (dopo la quale inizia il blocco codice del programma); in questo modo, viene scavalcata la zona compresa tra le etichette start: e start_code, contenente il blocco dati del programma!

20.3.3 Effetti provocati da JMP sugli operandi e sui flags

L'esecuzione dell'istruzione JMP non provoca alcuna modifica sull'unico operando presente (DEST).

L'esecuzione dell'istruzione JMP, in modalità reale, non modifica alcun campo del Flags Register.

20.4 Le istruzioni Jcond

In contrapposizione a JMP che costringe la CPU a saltare in modo incondizionato, viene resa disponibile una numerosa famiglia di istruzioni la cui esecuzione provoca un salto solo se si verificano determinate condizioni; tale famiglia viene indicata simbolicamente con il mnemonico Jcond, che significa Jump if Condition Is Met (salta se la condizione cond è verificata). Il termine cond indica, appunto, la condizione che deve verificarsi affinché possa essere eseguito il salto; se la condizione cond non si verifica, l'esecuzione prosegue sequenzialmente con l'istruzione successiva alla stessa Jcond.
In modalità reale, le uniche forme lecite per l'istruzione Jcond sono le seguenti: Come si può notare, è permesso solamente il salto diretto intrasegmento; in particolare, con le CPU 8086 e 80286, è permesso solo lo SHORT jump, mentre con le CPU 80386 e superiori, è permesso anche il NEAR jump.

La condizione che, verificandosi, determina l'esecuzione del salto, è legata al risultato prodotto da una operazione logico aritmetica appena eseguita; come sappiamo, immediatamente dopo l'esecuzione di una istruzione logico aritmetica, la CPU modifica una serie di flags, in modo da fornire un resoconto dettagliato sul risultato appena ottenuto.
Al momento di eseguire una istruzione Jcond, la CPU consulta proprio gli opportuni campi del Flags Register per sapere se la condizione di salto è verificata o meno; se la condizione è verificata, il salto viene eseguito, mentre in caso contrario, l'esecuzione prosegue sequenzialmente con l'istruzione successiva alla stessa Jcond.

Le istruzioni Jcond possono essere suddivise in due grandi categorie in quanto, la condizione che determina l'esecuzione del salto può fare riferimento, esplicitamente o implicitamente, al valore assunto da determinati flags; analizziamo in dettaglio queste due categorie.

20.4.1 Istruzioni Jcond riferite esplicitamente ai flags

In questa particolare categoria di istruzioni Jcond, la condizione cond che provoca il salto è legata esplicitamente al valore (0 o 1) assunto da un determinato flag come conseguenza del risultato prodotto da una operazione logico aritmetica appena eseguita; la Figura 20.6 illustra l'insieme completo di queste istruzioni (nel seguito del capitolo, per rappresentare la condizione che provoca il salto, vengono utilizzati i soliti operatori relazionali del linguaggio C). Il significato di questi mnemonici è abbastanza chiaro; JO significa jump if overflow (salta se si è verificato un overflow), JC significa jump if carry/borrow (salta se si è verificato un riporto o un prestito), etc.
Bisogna ricordare che, nella rappresentazione dei numeri interi relativi in complemento a 2, lo zero figura come numero positivo; di conseguenza, la condizione JNS (SF=0) è verificata quando il risultato è positivo o nullo.

Come si può notare, alcune condizioni di salto sono associate a due mnemonici equivalenti; ciò è dovuto al fatto che una determinata condizione, può essere espressa in diversi modi.
Ad esempio, nella comparazione aritmetica (CMP) tra due operandi A e B, se il risultato è nullo (ZF=1), allora la condizione:
JZ = jump if zero (salta se ZF = 1)
può essere espressa anche come:
JE = jump if equal (salta se A = B)
Se CMP produce, invece, un risultato diverso da zero (ZF=0), allora la condizione:
JNZ = jump if not zero (salta se ZF = 0)
può essere espressa anche come:
JNE = jump if not equal (salta se A ≠ B)
Se gli 8 bit meno significativi del risultato di una operazione, contengono un numero pari di bit a livello logico 1 (PF=1), allora la condizione:
JP = jump if parity (salta se PF = 1)
può essere espressa anche come:
JPE = jump if parity is even (salta se la parità è pari)
Se gli 8 bit meno significativi del risultato di una operazione, contengono un numero dispari di bit a livello logico 1 (PF=0), allora la condizione:
JNP = jump if not parity (salta se PF = 0)
può essere espressa anche come:
JPO = jump if parity is odd (salta se la parità è dispari)
Naturalmente, il programmatore è libero di utilizzare la forma che più si adatta al contesto del programma che sta scrivendo.

20.4.2 Istruzioni Jcond riferite implicitamente ai flags

L'altra categoria di istruzioni Jcond fa esplicito riferimento alla relazione d'ordine che esiste tra due operandi, DEST e SRC; in sostanza, attraverso questa categoria di istruzioni Jcond, possiamo effettuare dei salti condizionati dal risultato di una comparazione tra DEST e SRC.
Appare chiaro, però, che il riferimento ai vari campi del Flags Register è implicito; infatti, in un precedente capitolo abbiamo visto che la CPU, per conoscere la relazione d'ordine che esiste tra due operandi, si serve dei flags CF, ZF e OF.

Abbiamo anche visto che nella comparazione tra due operandi, è importantissimo distinguere tra numeri interi con o senza segno; per rendercene conto, consideriamo i due codici numerici esadecimali 3FB8h e BC2Dh.
Nell'insieme dei numeri interi senza segno, 3FB8h rappresenta 16312, mentre BC2Dh rappresenta 48173; in questo caso, appare evidente che 16312 è strettamente minore di 48173 (cioè, 3FB8h è strettamente minore di BC2Dh).
Nell'insieme dei numeri interi con segno in complemento a 2, 3FB8h rappresenta +16312, mentre BC2Dh rappresenta -17363; in questo caso, appare evidente che +16312 è strettamente maggiore di -17363 (cioè, 3FB8h è strettamente maggiore di BC2Dh)!

Proprio per i motivi appena illustrati, le istruzioni Jcond riferite implicitamente ai flags, si suddividono in due ulteriori gruppi; il primo gruppo fa esplicito riferimento ai numeri interi senza segno, mentre il secondo gruppo fa esplicito riferimento ai numeri interi con segno.
Ovviamente, è compito del programmatore stabilire quale gruppo di istruzioni Jcond debba essere utilizzato; tutto dipende cioè, dalla nostra intenzione di voler operare sui numeri interi con o senza segno.

La Figura 20.7 illustra le istruzioni Jcond riferite, esplicitamente, ai numeri interi senza segno. Per capire il significato di questi mnemonici, è necessario tenere presente che la Intel, per i numeri interi senza segno, ha deciso di utilizzare il termine above (al di sopra) per indicare "maggiore di" e il termine below (al di sotto) per indicare "minore di".
Questi due termini possono essere combinati con equal (che sta per "uguale") e con not (che indica una negazione); in questo modo, possiamo costruire espressioni del tipo "below or equal" (minore o uguale), "not below" (non minore), "not above or equal" (né maggiore, né uguale), etc.

Ciascuna istruzione di Figura 20.7 ha un doppio nome in virtù del fatto che una determinata relazione d'ordine può essere espressa in modi differenti; analizziamo, infatti, i quattro casi che si possono presentare.

Dire:
JB = jump if below (salta se DEST è minore di SRC)
equivale a dire:
JNAE = jump if not above or equal (salta se DEST non è, né maggiore, né uguale a SRC)
Dire:
JBE = jump if below or equal (salta se DEST è minore o uguale a SRC)
equivale a dire:
JNA = jump if not above (salta se DEST non è maggiore di SRC)
Dire:
JNB = jump if not below (salta se DEST non è minore di SRC)
equivale a dire:
JAE = jump if above or equal (salta se DEST è maggiore o uguale a SRC)
Dire:
JNBE = jump if not below or equal (salta se DEST non è, né minore, né uguale a SRC)
equivale a dire:
JA = jump if above (salta se DEST è maggiore di SRC)
La Figura 20.8 illustra le istruzioni Jcond riferite, esplicitamente, ai numeri interi con segno. Per capire il significato di questi mnemonici, è necessario tenere presente che la Intel, per i numeri interi con segno, ha deciso di utilizzare il termine greater (più grande) per indicare "maggiore di" e il termine less (meno) per indicare "minore di".
Questi due termini possono essere combinati con equal (che sta per "uguale") e con not (che indica una negazione); in questo modo, possiamo costruire espressioni del tipo "less or equal" (minore o uguale), "not less" (non minore), "not greater or equal" (né maggiore, né uguale), etc.

Ciascuna istruzione di Figura 20.8 ha un doppio nome in virtù del fatto che una determinata relazione d'ordine può essere espressa in modi differenti; analizziamo, infatti, i quattro casi che si possono presentare.

Dire:
JL = jump if less (salta se DEST è minore di SRC)
equivale a dire:
JNGE = jump if not greater or equal (salta se DEST non è, né maggiore, né uguale a SRC)
Dire:
JLE = jump if less or equal (salta se DEST è minore o uguale a SRC)
equivale a dire:
JNG = jump if not greater (salta se DEST non è maggiore di SRC)
Dire:
JNL = jump if not less (salta se DEST non è minore di SRC)
equivale a dire:
JGE = jump if greater or equal (salta se DEST è maggiore o uguale a SRC)
Dire:
JNLE = jump if not less or equal (salta se DEST non è, né minore, né uguale a SRC)
equivale a dire:
JG = jump if greater (salta se DEST è maggiore di SRC)
Come al solito, il programmatore è libero di utilizzare la forma che più si adatta al contesto del programma che sta scrivendo.

20.4.3 Esempi pratici per le istruzioni Jcond

Come si può facilmente immaginare, attraverso l'uso combinato delle istruzioni JMP e Jcond, possiamo implementare numerosi costrutti sintattici, tipici dei linguaggi di alto livello; il programma JCOND.ASM illustrato in Figura 20.9, mostra diversi esempi relativi a blocchi condizionali IF - ELSE e IF - ELSE IF - ELSE, cicli FOR, cicli WHILE - DO, cicli DO - WHILE, etc.

20.4.4 Reverse logic (logica inversa)

Abbiamo visto che con le istruzioni Jcond è permesso solamente il salto diretto intrasegmento; può anche capitare, però, di dover utilizzare Jcond per compiere dei salti "fuori portata". Una tale situazione si presenta, ad esempio, quando vogliamo effettuare un Jcond NEAR o un Jcond FAR con una CPU 8086 o 80286; oppure, quando vogliamo effettuare un Jcond FAR con una CPU 80386 o superiore.

Questo problema si risolve in modo molto semplice, attraverso una tecnica chiamata reverse logic (logica inversa); in pratica, anziché scrivere:
Jcond salto_troppo_lontano
possiamo scrivere: In sostanza, la condizione cond viene sostituita dalla sua negazione notcond per effettuare un normale salto "vicino"; il salto "lontano" (associato a cond) viene, invece, effettuato da JMP (che può raggiungere qualsiasi indirizzo di memoria)!

Supponiamo, ad esempio, di voler utilizzare JZ per effettuare un FAR jump ad una etichetta far_label; applicando la logica inversa, dobbiamo sostituire JZ con JNZ e scrivere: Quindi, se ZF=0, l'istruzione JNZ fa saltare il programma a near_label; se, invece, ZF=1, l'istruzione JMP fa saltare il programma a far_label!

20.4.5 Effetti provocati dalle istruzioni Jcond, sugli operandi e sui flags

L'esecuzione di una qualsiasi istruzione Jcond non provoca alcuna modifica sull'unico operando presente (DEST).

L'esecuzione di una qualsiasi istruzione Jcond, in modalità reale, non modifica alcun campo del Flags Register.
Ciò permette di utilizzare diverse istruzioni Jcond in sequenza; possiamo scrivere, ad esempio:

20.5 Le istruzioni JCXZ e JECXZ

La famiglia Jcond comprende anche due istruzioni particolari, indicate dai mnemonici JCXZ e JECXZ; come si intuisce dai mnemonici, tali istruzioni si distinguono dalle altre Jcond, in quanto fanno esplicito riferimento al contenuto del registro contatore CX/ECX!
Con il mnemonico JCXZ si indica l'istruzione Jump Short if CX is Zero (salto corto se il contenuto di CX è zero); con il mnemonico JECXZ si indica l'istruzione Jump Short if ECX is Zero (salto corto se il contenuto di ECX è zero).
In modalità reale, le uniche forme lecite per le istruzioni JCXZ e JECXZ sono le seguenti: A differenza di quanto accade per le altre Jcond, con le istruzioni JCXZ e JECXZ si possono effettuare solamente salti diretti SHORT intrasegmento; ovviamente, l'istruzione JECXZ è disponibile solo con le CPU 80386 e superiori.

Per capire la funzione svolta da JCXZ e JECXZ, è necessario tenere presente che molte istruzioni della CPU utilizzano CX o ECX come registro contatore predefinito; non a caso, tale registro viene chiamato proprio counter register.
Usualmente, CX/ECX viene impiegato per gestire il controllo di un loop; nel caso più frequente, il registro viene inizializzato con il numero n di iterazioni da effettuare e viene poi decrementato di 1 ad ogni ciclo, finché non raggiunge il valore zero (condizione di uscita dal loop).
Un tale procedimento, può portare ad una situazione potenzialmente pericolosa, rappresentata dalla eventualità che un loop inizi con il registro contatore che vale zero; questa situazione non è infrequente in quanto, spesso, il registro contatore viene inizializzato attraverso una o più istruzioni aritmetiche. Possiamo avere, ad esempio: Nell'eventualità che le precedenti istruzioni carichino il valore zero in CX, vediamo quello che succede quando, all'interno del loop, viene incontrata l'istruzione:
dec cx
Il risultato prodotto da questa istruzione è, ovviamente:
CX = CX - 1 = 0 - 1 = FFFFh = 65535
Di conseguenza, la successiva istruzione, usualmente del tipo:
jnz start_loop
anziché terminare il loop, lo fa ripetere altre 65535 volte, per un totale di 65536 iterazioni!

Per evitare una situazione del genere, possiamo utilizzare proprio l'istruzione JCXZ (o JECXZ) scrivendo: Come si può notare, se CX=0 il loop viene aggirato con uno SHORT jump compiuto da JCXZ; trattandosi, appunto, di uno SHORT jump, l'etichetta end_loop deve trovarsi ad una distanza compresa tra -128 e +127 byte dall'etichetta start_loop!

A rigore quindi, il caso CX=0 (o ECX=0) all'inizio di un loop (controllato dallo stesso CX/ECX), deve essere considerato come una situazione di errore; molti programmatori Assembly, però, sfruttano proprio questa situazione quando hanno la necessità di ripetere un loop 65536 volte con una CPU 8086 (o 80286), dotata di registri a soli 16 bit!

Avendo a disposizione una CPU 80386 o superiore, è anche possibile utilizzare l'istruzione JECXZ che forza la CPU a lavorare su ECX (anche in modalità reale); in tal caso, possiamo gestire dei loop che effettuano sino a 232 iterazioni!

20.5.1 Effetti provocati dalle istruzioni JCXZ e JECXZ, sugli operandi e sui flags

L'esecuzione di una istruzione JCXZ/JECXZ non provoca alcuna modifica sul registro CX/ECX.

L'esecuzione di una istruzione JCXZ o JECXZ, in modalità reale, non modifica alcun campo del Flags Register.

20.6 Le istruzioni LOOP, LOOPZ/LOOPE, LOOPNZ/LOOPNE

Nel mondo della programmazione, le iterazioni si sono rivelate uno strumento talmente utile e potente, da aver indotto i progettisti delle CPU a creare apposite istruzioni, con lo scopo principale di automatizzare la fase di controllo del loop che, normalmente, ricade sul programmatore; in particolare, la famiglia delle CPU 80x86 rende disponibili le istruzioni LOOP, LOOPZ/LOOPE e LOOPNZ/LOOPNE.
Tutte queste istruzioni utilizzano CX/ECX come registro contatore predefinito per il controllo del loop; a tale proposito, tutto dipende dall'attributo Dimensione del segmento di programma contenente il loop. Se l'attributo è USE16, il registro contatore predefinito è CX, mentre se l'attributo è USE32, il registro contatore predefinito è ECX; nel seguito viene fatto riferimento, esclusivamente, a segmenti di programma con attributo USE16, per cui è sottinteso che il registro contatore predefinito sia CX!

20.6.1 L'istruzione LOOP

Con il mnemonico LOOP si indica l'istruzione Loop According to CX/ECX Counter (controllo di un loop attraverso il registro contatore CX); questa istruzione 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 l'istruzione LOOP, decrementa di 1 il contenuto di CX, senza alterare alcun campo del Flags Register; successivamente, la CPU effettua una scelta basata sul risultato prodotto dal decremento di CX: In modalità reale, l'unica forma lecita per l'istruzione LOOP è la seguente:
LOOP Rel8
Come si può notare, LOOP permette di effettuare, esclusivamente, salti diretti SHORT intrasegmento; di conseguenza, l'offset di destinazione deve trovarsi ad una distanza compresa tra -128 e +127 byte dall'offset successivo a quello della stessa istruzione LOOP.

Nel programma di Figura 20.9, ad esempio, il ciclo FOR che inizializza il vettore vectWord, può essere riscritto in questo modo:

20.6.2 L'istruzione LOOPZ/LOOPE

Con il mnemonico LOOPZ (o LOOPE) si indica l'istruzione Loop According to CX/ECX Counter and ZF (controllo di un loop attraverso il registro contatore CX e lo Zero Flag); questa istruzione 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 l'istruzione LOOPZ/LOOPE, decrementa di 1 il contenuto di CX, senza alterare 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 LOOPZ/LOOPE non modifica alcun campo del Flags Register, è evidente che la modifica di ZF deve essere provocata da una istruzione posta all'interno del loop; normalmente, tale istruzione è quella che precede LOOPZ/LOOPE.
I due mnemonici, LOOPZ e LOOPE, sono del tutto equivalenti; sappiamo, infatti, che la condizione "jump if zero" può essere espressa anche come "jump if equal".

In modalità reale, l'unica forma lecita per l'istruzione LOOPZ/LOOPE è la seguente: Come si può notare, LOOPZ/LOOPE permette di effettuare, esclusivamente, salti diretti SHORT intrasegmento; di conseguenza, valgono le limitazioni già esposte per LOOP.

Come esempio pratico, supponiamo di utilizzare AX per contenere un numero intero con segno a 16 bit in complemento a 2; tale numero, inizialmente è positivo (bit di segno uguale a 0) e viene decrementato di 1 ad ogni loop. Il loop termina quando il numero diventa negativo (bit di segno uguale a 1); in tal caso, l'istruzione:
test ah, 80h ; ah and 10000000b
fornisce un risultato diverso da zero (ZF=0), senza che venga alterato il contenuto di AH.
Possiamo scrivere allora il seguente codice:

20.6.3 L'istruzione LOOPNZ/LOOPNE

Con il mnemonico LOOPNZ (o LOOPNE) si indica l'istruzione Loop According to CX/ECX Counter and ZF (controllo di un loop attraverso il registro contatore CX e lo Zero Flag); questa istruzione 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 l'istruzione LOOPNZ/LOOPNE, decrementa di 1 il contenuto di CX, senza alterare 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 LOOPNZ/LOOPNE non modifica alcun campo del Flags Register, è evidente che la modifica di ZF deve essere provocata da una istruzione posta all'interno del loop; normalmente, tale istruzione è quella che precede LOOPNZ/LOOPNE.
I due mnemonici, LOOPNZ e LOOPNE, sono del tutto equivalenti; sappiamo, infatti, che la condizione "jump if not zero" può essere espressa anche come "jump if not equal".

In modalità reale, l'unica forma lecita per l'istruzione LOOPNZ/LOOPNE è la seguente: Come si può notare, LOOPNZ/LOOPNE permette di effettuare, esclusivamente, salti diretti SHORT intrasegmento; di conseguenza, valgono le limitazioni già esposte per LOOP.

Nel programma di Figura 20.9, ad esempio, il ciclo DO - WHILE che copia stringaC in strBuffer, può essere riscritto in questo modo: In un segmento di programma con attributo USE16, possiamo definire una stringa avente lunghezza massima pari a 65535 elementi (byte), i cui offset sono compresi tra 0000h e FFFFh; proprio per questo motivo, il registro contatore CX viene inizializzato con il massimo numero possibile di iterazioni (FFFFh)!

20.6.4 Considerazioni generali sulle istruzioni LOOP, LOOPZ/LOOPE, LOOPNZ/LOOPNE

In base alle considerazioni appena esposte, risulta che le istruzioni LOOP, LOOPZ/LOOPE e LOOPNZ/LOOPNE, presentano diverse limitazioni; la più evidente è rappresentata dalla possibilità di effettuare solamente salti diretti SHORT intrasegmento.
In effetti, lo SHORT jump sembrerebbe troppo limitato per la gestione di loop molto complessi; bisogna tenere presente, però, che nella stragrande maggioranza dei casi, si ha bisogno di loop molto piccoli e veloci, mentre i loop molto grandi e contorti, sono indice di un pessimo stile di programmazione!

In ogni caso, può anche presentarsi la necessità di scrivere dei loop che richiedono un NEAR jump; per risolvere un tale problema, conviene decisamente rinunciare a LOOP, LOOPZ/LOOPE e LOOPNZ/LOOPNE, utilizzando al loro posto le istruzioni Jcond in combinazione con JMP.

Analizzando gli esempi presentati in precedenza, risulta che una iterazione gestita con LOOP equivale ad un ciclo FOR, mentre una iterazione gestita con LOOPZ/LOOPE o LOOPNZ/LOOPNE equivale ad un ciclo DO - WHILE; possiamo dire allora che nel caso di cicli FOR piccoli e semplici, risulta molto comodo l'utilizzo dell'istruzione LOOP, mentre in tutti gli altri casi (cicli FOR, DO - WHILE o WHILE - DO, più o meno complessi), conviene optare per le istruzioni Jcond e JMP, che appaiono anche più chiare ed espressive (come si vede negli esempi di Figura 20.9).

È anche importante ricordare che in una iterazione gestita con LOOP, LOOPZ/LOOPE o LOOPNZ/LOOPNE, bisogna evitare qualsiasi modifica al contenuto del registro contatore CX; se si presenta l'assoluta necessità di modificare CX/ECX, bisogna preservarne il vecchio contenuto con l'ausilio delle istruzioni PUSH e POP!

20.6.5 Effetti provocati dalle istruzioni LOOP, LOOPZ/LOOPE e LOOPNZ/LOOPNE, sugli operandi e sui flags

L'esecuzione di una istruzione LOOP, LOOPZ/LOOPE, LOOPNZ/LOOPNE, modifica il contenuto del registro contatore CX/ECX; il contenuto dell'operando DEST non subisce, invece, alcuna modifica.

L'esecuzione di una istruzione LOOP, LOOPZ/LOOPE, LOOPNZ/LOOPNE, in modalità reale, non modifica alcun campo del Flags Register.

20.7 Le istruzioni CALL e RET

Analizzando l'esempio presentato in Figura 20.4, ci rendiamo conto che grazie alle istruzioni per il trasferimento del controllo, possiamo costringere la CPU ad effettuare dei salti verso qualunque indirizzo di memoria; a tale proposito, non dobbiamo fare altro che indicare alla stessa CPU l'indirizzo logico Seg:Offset da caricare nella coppia CS:IP, in modo da poter raggiungere la destinazione del salto.
È chiaro che in una situazione di questo genere, il programmatore deve fare in modo che la CPU sia sempre in grado di trovare la "strada per il ritorno"; in sostanza, i vari salti presenti in un programma, devono essere tali da permettere alla CPU di seguire un percorso che, in modo più o meno diretto, deve condurre sempre alle istruzioni di terminazione del programma stesso.

Il concetto di "strada per il ritorno" è molto importante, in quanto suggerisce l'idea di far assumere ad un programma una struttura piuttosto particolare; in pratica, possiamo pensare di suddividere le istruzioni di un programma in un blocco principale chiamato programma principale e in tanti altri sottoblocchi, distinti ed indipendenti dallo stesso programma principale.
Ciascuno dei sottoblocchi è destinato a svolgere un compito specifico come, ad esempio, la conversione in maiuscolo di una stringa C, la copia di una stringa C, etc; i vari sottoblocchi così ottenuti, possono essere paragonati a tanti strumenti, a disposizione del programma principale. Ogni volta che il programma principale ha bisogno di svolgere un determinato compito come, ad esempio, convertire in maiuscolo una stringa C, non deve fare altro che "chiamare" (attraverso un salto) l'opportuno sottoblocco; lo stesso sottoblocco, dopo aver svolto il proprio lavoro, deve essere in grado di restituire il controllo (attraverso un altro salto) al cosiddetto "chiamante" (cioè, alla porzione di programma principale che ha effettuato la chiamata).

Le considerazioni appena esposte stanno alla base del concetto fondamentale di sottoprogramma; i sottoprogrammi rappresentano un potentissimo strumento che ha letteralmente rivoluzionato l'arte della programmazione, permettendo di raggiungere obiettivi come la modularità, la riusabilità e la manutenibilità del codice!

20.7.1 I sottoprogrammi

Con il termine sottoprogramma, si indica un blocco di istruzioni distinte ed indipendenti dal programma principale; un sottoprogramma è destinato a svolgere un compito specifico e le istruzioni che lo compongono rappresentano, nel loro insieme, un vero e proprio mini-programma.
Un sottoprogramma può essere chiamato da qualunque punto del programma principale ed in qualsiasi momento della fase di esecuzione; più in generale, un sottoprogramma può essere chiamato anche da un altro sottoprogramma (inoltre, come vedremo nei capitoli successivi, un sottoprogramma può anche chiamare se stesso, direttamente o indirettamente).
Chiamare un sottoprogramma significa effettuare un salto all'indirizzo della prima istruzione del sottoprogramma stesso; una volta che il sottoprogramma ha terminato il proprio lavoro, deve essere in grado di restituire il controllo a chi lo ha chiamato. L'indirizzo a cui salta un sottoprogramma che ha appena terminato il proprio lavoro, prende il nome di return address (indirizzo di ritorno); come è stato già anticipato in un precedente capitolo, l'indirizzo di ritorno più logico è chiaramente quello dell'istruzione immediatamente successiva a quella che ha effettuato la chiamata.

Utilizzando le istruzioni che abbiamo studiato in questo capitolo, possiamo facilmente implementare le due fasi fondamentali che caratterizzano la chiamata di un sottoprogramma; la prima fase consiste in un salto che il chiamante effettua verso il sottoprogramma (chiamata), mentre la seconda fase consiste in un salto dal sottoprogramma verso l'indirizzo di ritorno (restituzione del controllo).
Supponiamo, ad esempio, di aver creato un sottoprogramma il cui indirizzo iniziale è delimitato dall'etichetta start_subroutine1; tale sottoprogramma effettua la conversione in maiuscolo di una stringa C che deve essere puntata da DS:BX. Lo stesso sottoprogramma, richiede che l'indirizzo di ritorno si trovi nel registro DX; in base a queste premesse, se vogliamo richiedere la conversione in maiuscolo di una stringa C chiamata stringa_da_convertire, possiamo scrivere le seguenti istruzioni: Come è facile intuire, il sottoprogramma dopo aver terminato il proprio lavoro, non deve fare altro che eseguire un semplicissimo:
jmp dx
Nell'esempio appena presentato, abbiamo supposto che il sottoprogramma sia raggiungibile con un NEAR jump; inoltre, il registro DS referenzia il segmento di programma in cui è stata definita la stringa stringa_da_convertire.

Questo procedimento appare concettualmente molto semplice, ma può risultare piuttosto fastidioso da applicare quando si ha a che fare con decine e decine di chiamate; per ogni chiamata, il programmatore deve ricordarsi di caricare in DX l'indirizzo di ritorno, con l'evidente rischio di commettere qualche errore.
Per evitare tutti questi potenziali problemi, le CPU della famiglia 80x86 mettono a disposizione due potentissime istruzioni rappresentate dai mnemonici CALL e RET; come si può facilmente intuire, l'istruzione CALL effettua la chiamata di un sottoprogramma, mentre l'istruzione RET restituisce il controllo al chiamante.

20.7.2 L'istruzione CALL

Con il mnemonico CALL si indica l'istruzione CALL Procedure (chiamata di una procedura); come già sappiamo, in Assembly si utilizza il termine procedura per indicare un generico sottoprogramma. Nei linguaggi di alto livello, vengono utilizzati termini come, procedure, function, subroutine, etc; nel seguito del capitolo e in tutta la sezione Assembly Base, verrà sempre utilizzato il termine generico procedura.

L'istruzione CALL è del tutto simile all'istruzione JMP; il suo scopo quindi è quello di costringere la CPU a saltare, senza condizioni, ad un determinato indirizzo di memoria Seg:Offset specificato dal programmatore attraverso un apposito operando esplicito (DEST). La differenza fondamentale tra JMP e CALL sta nel fatto che, prima di effettuare il salto specificato da CALL, la CPU inserisce sulla cima dello stack l'indirizzo di ritorno; come già sappiamo, l'indirizzo di ritorno è quello dell'istruzione immediatamente successiva alla stessa CALL. Ovviamente, prima di inserire l'indirizzo di ritorno nello stack, la CPU decrementa opportunamente il registro SP!

In modalità reale, le uniche forme lecite per l'istruzione CALL sono le seguenti: Come si può notare, sono permessi tutti i tipi di salto descritti nella sezione 20.1, ad eccezione della SHORT call (chiamata corta) che viene sempre ricondotta ad una NEAR call (chiamata vicina); i codici macchina relativi ai vari casi, sono i seguenti: Analizziamo subito i vari casi che si possono presentare; ovviamente, restano valide tutte le considerazioni esposte nella sezione 20.1 in relazione al procedimento seguito dalla CPU per effettuare i diversi tipi di salto.

Abbiamo una procedura start_proc1 che si trova all'offset 0FC8h di un segmento di programma chiamato CODESEGM; se vogliamo chiamare tale procedura dall'interno dello stesso CODESEGM, possiamo effettuare una NEAR call diretta intrasegmento in questo modo: Come possiamo notare, l'assembler ha calcolato:
0FC8h - 0040h = 0F88h
Quando la CPU incontra l'istruzione CALL, prima di tutto sottrae 2 byte a SP e salva nello stack l'offset 0040h dell'istruzione successiva; non è necessario salvare anche la componente Seg dell'indirizzo di ritorno, in quanto il contenuto (CODESEGM) di CS resterà invariato (salto intrasegmento). Successivamente, la CPU calcola:
0F88h + 0040h = 0FC8h
Il valore 0FC8h viene caricato in IP e viene effettuato un salto diretto intrasegmento a CS:IP (cioè, a CODESEGM:0FC8h); come si può notare, si tratta proprio dell'indirizzo iniziale di start_proc1.
Se abbiamo la certezza che il salto è diretto intrasegmento, possiamo anche scrivere in modo esplicito:
call near ptr start_proc1
Vogliamo chiamare start_proc1 attraverso una NEAR call indiretta intrasegmento; utilizzando, ad esempio, il registro CX, possiamo scrivere: Quando la CPU incontra l'istruzione CALL, prima di tutto sottrae 2 byte a SP e salva nello stack l'offset 0047h dell'istruzione successiva; non è necessario salvare anche la componente Seg dell'indirizzo di ritorno, in quanto il contenuto (CODESEGM) di CS resterà invariato (salto intrasegmento). Successivamente, la CPU carica direttamente in IP il valore 0FC8h contenuto in CX ed effettua un salto indiretto intrasegmento a CS:IP (cioè, a CODESEGM:0FC8h); come si può notare, si tratta proprio dell'indirizzo iniziale di start_proc1.

Al posto di CX possiamo utilizzare un dato di tipo WORD chiamato, ad esempio, near_addr e definito all'offset 000Ah di un segmento DATASEGM referenziato da DS; possiamo scrivere allora: Quando la CPU incontra l'istruzione CALL, prima di tutto sottrae 2 byte a SP e salva nello stack l'offset 0053h dell'istruzione successiva; non è necessario salvare anche la componente Seg dell'indirizzo di ritorno, in quanto il contenuto (CODESEGM) di CS resterà invariato (salto intrasegmento). Successivamente, la CPU accede all'indirizzo DS:near_addr (cioè, a DS:000Ah), legge il valore 0FC8h, lo carica direttamente in IP ed effettua un salto indiretto intrasegmento a CS:IP (cioè, a CODESEGM:0FC8h); come si può notare, si tratta proprio dell'indirizzo iniziale di start_proc1.
Per enfatizzare il fatto che near_addr contiene un indirizzo di tipo NEAR, possiamo anche scrivere in modo esplicito:
call word ptr near_addr
Abbiamo una procedura start_proc2 che si trova all'offset 00B8h di un segmento di programma chiamato LIBCODE; se vogliamo chiamare tale procedura da un altro segmento CODESEGM, possiamo effettuare una FAR call diretta intersegmento in questo modo: Quando la CPU incontra l'istruzione CALL, prima di tutto sottrae 4 byte a SP e salva nello stack l'indirizzo completo CODESEGM:005Ah dell'istruzione successiva; è necessario salvare anche la componente Seg in quanto il contenuto (CODESEGM) di CS verrà modificato (salto intersegmento).
Nel rispetto della convenzione relativa alla disposizione in memoria degli indirizzi FAR, la CPU salva nello stack, prima la componente Seg (cioè, CODESEGM) e poi la componente Offset (cioè, 005Ah) dell'indirizzo di ritorno; in questo modo, la componente Offset si trova a precedere nello stack la componente Seg.
Successivamente, la CPU carica la coppia LIBCODE:00B8h direttamente in CS:IP ed effettua un salto diretto intersegmento a CS:IP (cioè, a LIBCODE:00B8h); come si può notare, si tratta proprio dell'indirizzo iniziale di start_proc2.

Vogliamo chiamare start_proc2 attraverso una FAR call indiretta intersegmento utilizzando, ad esempio, un dato di tipo DWORD chiamato far_addr e definito all'offset 000Ch di un segmento DATASEGM2 referenziato da ES; possiamo scrivere allora: Se non vogliamo inserire ogni volta il registro ES davanti a tutti gli identificatori definiti in DATASEGM2, possiamo servirci di una direttiva ASSUME che associa DATASEGM2 a ES.
Bisogna ribadire ancora una volta che il programmatore deve attenersi alla convenzione relativa alla disposizione in memoria degli indirizzi FAR; notiamo quindi che la componente Offset di start_proc2 viene disposta nella WORD bassa di far_addr, mentre la componente Seg di start_proc2 viene disposta nella WORD alta di far_addr!
Quando la CPU incontra l'istruzione CALL, prima di tutto sottrae 4 byte a SP e salva nello stack l'indirizzo completo CODESEGM:006Fh dell'istruzione successiva; è necessario salvare anche la componente Seg in quanto il contenuto (CODESEGM) di CS verrà modificato (salto intersegmento).
Successivamente, la CPU accede all'indirizzo ES:000Ch, legge la coppia LIBCODE:00B8h, la carica direttamente in CS:IP ed effettua un salto indiretto intersegmento a CS:IP (cioè, a LIBCODE:00B8h); come si può notare, si tratta proprio dell'indirizzo iniziale di start_proc2.
Per enfatizzare il fatto che far_addr è un dato di tipo DWORD, possiamo anche scrivere in modo esplicito:
call dword ptr es:far_addr

20.7.3 L'istruzione RET

Con il mnemonico RET si indica l'istruzione Return from Procedure (ritorno da una procedura); questa istruzione è chiaramente complementare a CALL e permette ad una procedura di restituire il controllo al chiamante.

L'istruzione RET è del tutto simile all'istruzione JMP; il suo scopo quindi è quello di costringere la CPU a saltare, senza condizioni, ad un determinato indirizzo di memoria Seg:Offset. La differenza fondamentale tra JMP e RET sta nel fatto che incontrando una istruzione RET, la CPU estrae dalla cima dello stack l'indirizzo Seg:Offset da utilizzare come destinazione del salto; la logica vuole che si tratti di un indirizzo inserito nello stack da una precedente istruzione CALL. Ovviamente, dopo aver estratto l'indirizzo di ritorno dallo stack, la CPU incrementa opportunamente il registro SP!

In modalità reale, le uniche forme lecite per l'istruzione RET sono le seguenti: La forma semplice (RET) comporta l'estrazione dallo stack del solo indirizzo di ritorno; la forma speciale (RET Imm16) comporta la somma:
SP = SP + Imm16
a cui fa seguito l'estrazione dallo stack dell'indirizzo di ritorno. La forma speciale dell'istruzione RET viene utilizzata dai linguaggi di alto livello, per eliminare eventuali variabili locali create nello stack in fase di chiamata di una procedura; nei capitoli successivi, questo argomento verrà analizzato in estremo dettaglio.

I codici macchina relativi ai vari casi, sono i seguenti: Analizziamo subito i vari casi che si possono presentare; a tale proposito, vediamo come si svolge il ritorno dalle procedure chiamate nei cinque esempi presentati per l'istruzione CALL.

Nei primi tre esempi, la procedura start_proc1 è stata chiamata attraverso una NEAR call intrasegmento; di conseguenza, la stessa procedura deve terminare con un NEAR return. A tale proposito, viene utilizzato il codice macchina C3h.
In presenza del codice macchina C3h, la CPU estrae una WORD dallo stack, la carica in IP e, dopo aver incrementato SP di 2 byte, salta a CS:IP; se il programmatore non ha commesso errori nella gestione dello stack, la WORD estratta dalla CPU coincide con l'indirizzo di ritorno inserito da una precedente NEAR call!
In base alle conoscenze che abbiamo acquisito, possiamo facilmente simulare un NEAR return in questo modo: Nel quarto e quinto esempio, la procedura start_proc2 è stata chiamata attraverso una FAR call intersegmento; di conseguenza, la stessa procedura deve terminare con un FAR return. A tale proposito, viene utilizzato il codice macchina CBh.
In presenza del codice macchina CBh, la CPU estrae una DWORD dallo stack, la carica in CS:IP e, dopo aver incrementato SP di 4 byte, salta a CS:IP; se il programmatore non ha commesso errori nella gestione dello stack, la DWORD estratta dalla CPU coincide con l'indirizzo di ritorno inserito da una precedente FAR call!
Sfruttando il dato far_addr definito nel quinto esempio, possiamo facilmente simulare un FAR return in questo modo: Ancora una volta, bisogna osservare che questo esempio funziona in quanto vengono rigorosamente seguite le convenzioni utilizzate dalla CPU per la disposizione in memoria degli indirizzi FAR!

20.7.4 Esempio pratico per le istruzioni CALL e RET

Prima di illustrare un esempio pratico per le istruzioni CALL e RET, è necessario analizzare un aspetto importante; tale aspetto riguarda il fatto che la CPU deve essere messa nelle condizioni di poter chiaramente distinguere tra il codice appartenente al programma principale e il codice appartenente ad una procedura. Ci chiediamo allora: qual'è l'area più adatta di un programma, dove poter inserire le varie procedure?

Nel caso dei programmi eseguibili di tipo COM, sicuramente l'area più indicata è quella che si trova oltre le istruzioni di terminazione; come già sappiamo, è impossibile che la CPU possa finire per sbaglio in tale area.

Nel caso dei programmi eseguibili di tipo EXE, possiamo adottare varie soluzioni; anche in questo caso, l'area più indicata è quella che si trova oltre le istruzioni di terminazione. In alternativa, possiamo disporre le procedure anche prima dell'entry point; come sappiamo, tale alternativa non è permessa nei programmi di tipo COM.
Una ulteriore soluzione consiste nel servirsi di un apposito segmento di codice, diverso da quello che contiene il programma principale; è chiaro che tutte le procedure definite in tale segmento, devono essere chiamate, necessariamente, attraverso FAR call.
Generalizzando, possiamo pensare di disporre le varie procedure in appositi segmenti che si trovano in uno o più moduli Assembly esterni; in tal caso, otteniamo delle cosiddette librerie di procedure. Il vantaggio offerto da questa soluzione, consiste nella possibilità di linkare le varie librerie a tutti i programmi che ne hanno bisogno; questo argomento assume una importanza enorme e verrà quindi trattato in estremo dettaglio nei capitoli successivi.

Come esempio pratico, scriviamo un programma che illustra tutti i possibili metodi di chiamata delle procedure; inoltre, visto che abbiamo acquisito sufficienti conoscenze, possiamo sfruttare questo esempio per cominciare a lavorare anche con indirizzi di tipo FAR per i dati.
Il programma di Figura 20.10 definisce cinque stringhe che dovranno essere convertite in maiuscolo; le prime tre stringhe si trovano in un segmento dati chiamato DATASEGM, mentre altre due stringhe si trovano in un secondo segmento dati chiamato DATASEGM2. Il segmento DATASEGM viene gestito attraverso DS; il segmento DATASEGM2 viene gestito attraverso FS, in modo da non dover modificare ES (utilizzato dalla procedura writeString).
La procedura start_maiuscole1 si trova nel segmento di codice principale CODESEGM ed opera su stringhe che devono essere puntate dalla coppia DS:BX; la procedura start_maiuscole2 si trova in un segmento di codice chiamato LIBCODE ed opera su stringhe che devono essere puntate dalla coppia FS:BX. Analizzando il programma di Figura 20.10 possiamo notare che tutte le chiamate a start_maiuscole1, che partono dall'interno di CODESEGM, sono intrasegmento e richiedono quindi delle NEAR call; nell'esempio si presuppone che start_maiuscole1 venga sempre chiamata con una NEAR call e quindi la procedura termina con una istruzione RET che viene sempre interpretata dall'assembler come NEAR return (codice macchina C3h).
Tutte le chiamate a start_maiuscole2, che partono dall'interno di CODESEGM, sono intersegmento e richiedono quindi delle FAR call; nell'esempio si presuppone che start_maiuscole2 venga sempre chiamata con una FAR call e quindi la procedura termina con un codice macchina CBh che rappresenta un FAR return.

Il segmento dati DATASEGM viene referenziato da DS; di conseguenza, tutti i dati definiti in DATASEGM possono essere gestiti attraverso puntatori NEAR. Il segmento dati DATASEGM2 viene referenziato da FS; di conseguenza, tutti i dati definiti in DATASEGM2 devono essere gestiti, necessariamente, attraverso puntatori FAR.
Se inseriamo una direttiva ASSUME che associa DATASEGM2 a FS, possiamo evitare di specificare ogni volta il registro FS davanti agli identificatori creati nello stesso segmento DATASEGM2; in questo modo possiamo scrivere, ad esempio:
call far_addr
La procedura start_maiuscole2 opera su stringhe che devono essere puntate da FS:BX; in questo caso, siccome stiamo utilizzando i registri puntatori, la direttiva ASSUME citata in precedenza non serve. Ogni volta che dereferenziamo il registro puntatore BX, dobbiamo quindi specificare sempre il segment override FS; se proviamo a scrivere:
mov al, [bx]
la CPU carica in AL un BYTE che viene letto dall'indirizzo DS:BX e non da FS:BX!

Per garantire il corretto funzionamento di writeString, dobbiamo ricordare che questa procedura visualizza una stringa che deve essere puntata da ES:DI; sappiamo, inoltre, che il segmento DATASEGM viene gestito con DS, mentre il segmento DATASEGM2 viene gestito con FS. Affinché writeString visualizzi correttamente una delle stringhe definite in DATASEGM, dobbiamo porre quindi ES=DS; analogamente, affinché writeString visualizzi correttamente una delle stringhe definite in DATASEGM2, dobbiamo porre ES=FS.

Osserviamo infine che le due procedure, start_maiuscole1 e start_maiuscole2, prima di effettuare qualunque operazione, provvedono a visualizzare il return address; ovviamente, appena si entra in una qualsiasi procedura, il return address è disponibile a partire dall'indirizzo SS:SP dello stack. Naturalmente, nel caso di start_maiuscole1 l'indirizzo di ritorno è di tipo NEAR, mentre nel caso di start_maiuscole2 l'indirizzo di ritorno è di tipo FAR; per verificare la correttezza di tali indirizzi, si può confrontare il contenuto del listing file CALLRET.LST, con l'output prodotto dal programma CALLRET.EXE.

20.7.5 Effetti provocati dalle istruzioni CALL e RET sugli operandi e sui flags

L'esecuzione dell'istruzione CALL non provoca alcuna modifica sull'unico operando presente; lo stesso discorso vale per l'istruzione RET con operando Imm16.

L'esecuzione delle istruzioni CALL e RET, in modalità reale, non modifica alcun campo del Flags Register.

20.8 Le istruzioni INT, INTO e IRET

In un precedente capitolo è stato spiegato che in seguito ad una importantissima convenzione adottata dai produttori di PC, i primi 1024 byte della RAM devono essere rigorosamente riservati ai cosiddetti interrupt vectors (vettori di interruzione); questi 1024 byte vengono suddivisi in 256 locazioni (100h locazioni) da 4 byte ciascuna.
Ogni locazione da 4 byte può ospitare un indirizzo FAR, formato quindi da una coppia Seg:Offset (con la componente Offset che precede sempre la componente Seg); tale indirizzo, se è presente, prende appunto il nome di vettore di interruzione. Un vettore di interruzione rappresenta l'indirizzo di una procedura presente nella memoria RAM; tale procedura prende il nome di ISR o Interrupt Service Routine (procedura di servizio per una interruzione).

Il fatto che un vettore di interruzione sia un indirizzo Seg:Offset da 2+2 byte, implica il chiaro riferimento alla modalità reale 8086; provando ad eseguire uno di questi vettori di interruzione in modalità protetta, si provoca un sicuro crash. Con un indirizzo Seg:Offset da 2+2 byte, possiamo accedere solo al primo MiB della RAM; ciò implica che le ISR associate a tali indirizzi, debbano trovarsi anch'esse nello stesso primo MiB!

Numerosi vettori di interruzione (con le ISR associate) vengono installati dal BIOS durante la fase di avvio del PC e dal DOS durante la fase di avvio del SO; altri vettori di interruzione vengono installati dai cosiddetti device drivers (piloti di dispositivo) che permettono ai programmi di accedere alle periferiche del PC.
Naturalmente, è stata prevista anche la presenza di un certo numero di vettori di interruzione, liberamente disponibili per le eventuali esigenze dei programmatori; per l'elenco completo dei 256 vettori di interruzione, si può consultare l'apposita Lista dei vettori di interruzione in modalità reale.
Tutto ciò che riguarda i vettori di interruzione, verrà trattato in dettaglio nella sezione Assembly Avanzato.

Nella modalità reale, i vettori di interruzione assumono una importanza enorme in quanto rappresentano il metodo che permette ai programmi di interfacciarsi con i servizi offerti dal BIOS, dal DOS o dai device drivers; i programmi che intendono usufruire di tali servizi, non devono fare altro che chiamare l'opportuno vettore di interruzione.

La Figura 20.11 illustra un semplice programma in formato COM che visualizza, attraverso un loop, tutti i 256 vettori di interruzione presenti in un PC; naturalmente, l'output di questo programma può differire da PC a PC. Quando una periferica del PC vuole comunicare con la CPU, invia un apposito segnale che prende il nome di IRQ o interrupt request (richiesta di interruzione); il procedimento che permette alla CPU di elaborare questo tipo di richieste, è stato già esposto in un precedente capitolo e verrà analizzato in dettaglio nella sezione Assembly Avanzato.
L'elaborazione, da parte della CPU, di una richiesta di interruzione proveniente da una periferica, prende il nome di hardware interrupt (interruzione hardware); come già sappiamo, tali interruzioni avvengono in modo asincrono in quanto possono presentarsi in qualsiasi momento della fase di esecuzione di un programma.

Esiste però anche la possibilità di richiedere interruzioni che avvengono in modo sincrono con il programma in esecuzione; ciò accade quando è il programma stesso a richiedere, ad esempio, un determinato servizio del DOS o del BIOS, attraverso un apposito vettore di interruzione.
In un caso del genere, si parla di software interrupt (interruzione software); in pratica, le interruzioni software vengono richieste in modo consapevole da un programma in esecuzione, mentre le interruzioni hardware possono presentarsi in qualsiasi momento e senza alcun preavviso per il programma stesso.

L'elaborazione, da parte della CPU, di una qualsiasi richiesta di interruzione, sia hardware, sia software, comporta in ogni caso la chiamata di una apposita ISR; come sappiamo, il compito della ISR è quello di soddisfare le richieste di chi ha effettuato la chiamata.
Dalle considerazioni appena esposte si deduce quindi che l'elaborazione di una richiesta di interruzione presenta aspetti perfettamente analoghi al caso della gestione di una procedura attraverso CALL e RET; siccome però le ISR sono procedure speciali, le CPU della famiglia 80x86 mettono a disposizione apposite istruzioni rappresentate dai mnemonici INT, INTO e IRET.

20.8.1 L'istruzione INT

Con il mnemonico INT si indica l'istruzione Call to Interrupt Procedure (chiamata di una ISR); l'istruzione INT è del tutto simile all'istruzione CALL in quanto il suo scopo è quello di chiamare una procedura. La differenza fondamentale sta nel fatto che prima di effettuare la chiamata specificata da INT, la CPU inserisce sulla cima dello stack il contenuto del registro FLAGS seguito dall'indirizzo di ritorno; prima di effettuare tali inserimenti, la CPU stessa decrementa opportunamente il registro SP!

In modalità reale, l'unica forma lecita per l'istruzione INT è la seguente:
INT Imm8
Il valore immediato Imm8 deve essere compreso tra 00h e FFh e indica naturalmente il vettore di interruzione da chiamare; come vedremo più avanti, il caso Imm8=3 viene trattato in modo speciale dalla CPU.
Il codice macchina generale per l'istruzione INT è formato dall'Opcode CDh seguito da un Imm8; quando la CPU incontra tale codice macchina, effettua le seguenti operazioni: Osserviamo subito che la CPU salva nello stack l'indirizzo di ritorno completo Seg:Offset; tutto ciò è una logica conseguenza del fatto che la chiamata di una ISR è sempre di tipo FAR!

Indicando con far_addr un dato a 32 bit referenziato da DS e supponendo di avere ES=0000h, possiamo facilmente simulare una INT Imm8 in questo modo: (Siccome ogni vettore occupa 4 byte, il vettore n. Imm8 si trova all'offset Imm8*4 del segmento di memoria n. 0000h).

L'istruzione INT permette ai normali programmi di generare una interruzione software; in questo modo, è possibile richiedere numerosi servizi forniti, principalmente, dal DOS e dal BIOS.
Nei precedenti capitoli abbiamo già conosciuto alcuni servizi offerti dal DOS; la maggior parte dei servizi DOS sono ottenibili attraverso il vettore di interruzione n. 21h (chiamato, appunto, INT dei servizi DOS).
Analizzando, ad esempio, i listati dei vari programmi di esempio presentati anche in questo capitolo, possiamo notare la presenza del servizio 4Ch della INT n. 21h; attraverso tale servizio, è possibile terminare un programma e restituire il controllo al DOS.
In un precedente capitolo è stato utilizzato il servizio n. 09h, fornito ugualmente dalla INT n. 21h; le caratteristiche di tale servizio vengono riassunte dalla Figura 20.12. In un segmento di programma referenziato da DS possiamo definire, ad esempio, la stringa:
strTest db 'Stringa da visualizzare', '$'
A questo punto possiamo scrivere: Osserviamo che, spesso, le ISR richiedono una lista di argomenti che il programmatore deve passare attraverso i registri generali; inoltre, uno stesso vettore può fornire numerosissimi servizi. Per un elenco completo di tutti i servizi offerti dai 256 vettori di interruzione, si può consultare la famosissima Ralf Brown's Interrupt List; a tale proposito, è presente un link nella pagina dei Siti con argomenti correlati di questo sito.

Nel caso particolare di una istruzione INT che specifica un operando Imm8=3, gli assembler generano un codice macchina formato dal solo Opcode CCh; come già sappiamo, la INT n. 03h è stata espressamente concepita per permettere ai debuggers di gestire facilmente i cosiddetti breakpoint. In pratica, il programmatore che ha la necessità di testare un proprio programma, può chiedere al debugger di eseguire il programma stesso sino ad una determinata istruzione; per svolgere un tale lavoro, il debugger sostituisce con il codice CCh, il primo byte dell'istruzione prescelta.
Consideriamo, ad esempio, l'istruzione:
mov di, offset strTitle
del programma di Figura 20.11; il codice macchina di tale istruzione è:
BFh 0170h
Se chiediamo l'inserimento di un breakpoint in quel preciso punto, il debugger salva il primo byte (BFh) del codice macchina e lo sostituisce con CCh, ottenendo quindi:
CCh 0170h
A questo punto, possiamo chiedere al debugger di avviare l'esecuzione del programma INTLIST.COM; quando la CPU incontra il codice macchina CCh, chiama automaticamente il vettore di interruzione n. 03h. Il debugger ha precedentemente installato una propria ISR per la gestione di tale INT; lo scopo di questa ISR è proprio quello di interrompere il programma in esecuzione, mostrando lo stato che in quel preciso istante viene assunto da tutti i registri della CPU.
Quando la fase di debugging è terminata, il debugger ripristina il precedente codice macchina rimettendo BFh nel punto in cui era stato inserito il valore CCh!
Osserviamo che diverse istruzioni Assembly (come PUSHF), possono avere un codice macchina formato da un solo byte; grazie al fatto che anche la INT 03h ha un codice macchina formato da un solo byte (mentre le altre INT utilizzano due byte), si evita il rischio di sovrascrivere l'istruzione successiva al breakpoint!

20.8.2 L'istruzione INTO

Con il mnemonico INTO si indica l'istruzione Call to Interrupt Procedure on Overflow (chiamata di una ISR in caso di overflow); lo scopo di questa istruzione è quello di far generare alla CPU una INT 04h nel caso in cui una operazione logico aritmetica appena eseguita abbia provocato un overflow.

In modalità reale, l'unica forma lecita per l'istruzione INTO è la seguente:
INTO
Il codice macchina associato a questa istruzione è formato dal solo Opcode CEh; quando la CPU incontra questo codice macchina, verifica lo stato dell'overflow flag per poter decidere il comportamento da seguire: In sostanza, si tratta di una situazione perfettamente analoga a quella relativa alla INT 03h; il programmatore può installare una propria ISR per poter adattare alle proprie esigenze la gestione dei casi di overflow.
Si tenga presente che generalmente, il DOS non installa alcuna ISR predefinita per la gestione delle INT come le 03h e 04h; spesso, tali vettori di interruzione puntano ad una locazione di memoria contenente una semplice istruzione IRET (ritorno da una ISR).

20.8.3 L'istruzione IRET

Con il mnemonico IRET si indica l'istruzione Interrupt Return (ritorno da una ISR); l'istruzione IRET è del tutto simile all'istruzione RET in quanto il suo scopo è quello di permettere ad una procedura di restituire il controllo al chiamante. La differenza fondamentale sta nel fatto che in presenza di una istruzione IRET, la CPU estrae dalla cima dello stack l'indirizzo di ritorno e una ulteriore WORD che viene caricata nel registro FLAGS; dopo aver effettuato tali estrazioni, la CPU stessa incrementa opportunamente il registro SP!

In modalità reale, l'unica forma lecita per l'istruzione IRET è la seguente:
IRET
Il codice macchina per l'istruzione IRET è formato dal solo Opcode CFh; quando la CPU incontra tale codice macchina, effettua le seguenti operazioni: Una ISR è tenuta rigorosamente a restituire il controllo attraverso una istruzione IRET; infatti, una istruzione RET di tipo FAR, non ripristina il registro FLAGS, provocando così un sicuro crash (a causa della WORD rimasta nello stack)!

Indicando con far_addr un dato a 32 bit referenziato da DS, possiamo facilmente simulare una IRET in questo modo:

20.8.4 Esempio pratico sulla gestione delle ISR

In base alle considerazioni esposte in precedenza, appare evidente che il programmatore deve evitare nella maniera più assoluta, la modifica casuale degli indirizzi contenuti nei vettori di interruzione; è anche chiaro il fatto che l'installazione di ISR personalizzate, per la gestione di vettori di interruzione riservati al DOS, al BIOS o ai Device Drivers, è un compito che compete ai programmatori particolarmente esperti (principalmente, programmatori di sistemi operativi)!
Se si ha la necessità di installare ISR personalizzate, per scopi "meno importanti", ci si può servire dei vettori di interruzione marcati come "reserved for user interrupt"; è anche possibile installare proprie ISR, destinate ad intercettare particolari INT generate dalla CPU come la 00h (Divide Error), la 01h (Single Step), la 03h (Breakpoint), la 04h (INTO), etc.

Prima di analizzare alcuni esempi pratici, è necessario definire il procedimento che deve essere rigorosamente seguito per la corretta gestione di una ISR; tale procedimento può essere suddiviso nelle seguenti fasi, riferite ad una generica INT n: Inoltre, è importante che la ISR preservi il contenuto di tutti i registri che deve utilizzare; questo aspetto diventa fondamentale nel caso delle ISR destinate a gestire interruzioni hardware.
Naturalmente, nel preservare il contenuto di tutti i registri utilizzati, il programmatore deve anche garantire una corretta gestione dello stack all'interno della ISR; in caso contrario, l'istruzione IRET estrae dallo stack valori privi di senso ed effettua un salto ad un indirizzo sbagliato, con conseguente crash del programma!

Analizziamo un programma di esempio che mostra come intercettare la INT 01h (Single Step); come sappiamo, se poniamo TF=1 (Trap Flag), la CPU genera automaticamente una INT 01h per ogni istruzione che esegue. I debugger intercettano la INT 01h e rendono possibile così l'esecuzione a "passo singolo" di un programma; la Figura 20.13 mostra un programma in formato COM, che intercetta la INT 01h e si "autodebugga" da solo. Osserviamo che il flag TF si trova in posizione 8 nel registro FLAGS; di conseguenza:

OR FLAGS, 0000000100000000b pone TF=1 e lascia inalterati tutti gli altri flags;
AND FLAGS, 1111111011111111b pone TF=0 e lascia inalterati tutti gli altri flags.

Dal punto in cui viene posto TF=1, al punto in cui viene posto TF=0, sono presenti 9 istruzioni; infatti, la stringa strDebug viene visualizzata (da new_int01h) 9 volte!
È importantissimo notare che la INT 01h non viene chiamata per le istruzioni interne a new_int01h; ciò è una diretta conseguenza del comportamento seguito dalla CPU nella elaborazione delle interruzioni.

Al momento di chiamare una ISR, la CPU disabilita tutte le INT mascherabili; tali INT vengono riabilitate al termine della ISR stessa!

Se la CPU non adottasse questo accorgimento, una ISR come new_int01h finirebbe per chiamare se stessa; in tal caso, si innescherebbe un loop infinito con conseguente blocco del programma!

È importante ribadire che le ISR devono preservare tutti i registri che utilizzano; in caso contrario, si può andare incontro ad un sicuro crash dovuto al fatto che la ISR "sporca" il contenuto di registri in uso al chiamante!
Nel nostro caso, si può notare che new_int01h modifica il contenuto di DI; il programma principale, a sua volta, si aspetta che al termine di new_int01h, il registro DI contenga ancora il valore 0004h!
All'interno di new_int01h, vengono chiamate procedure come, writeString, writeHex16, waitChar, le quali provvedono anch'esse a preservare il contenuto di tutti i registri che utilizzano; la procedura waitChar restituisce AX modificato, per cui questo registro deve essere preservato dalla new_int01h.

Come esercitazione pratica, si consiglia di scrivere un programma analogo a INT01h.ASM, destinato però ad intercettare la INT 04h (Overflow); a tale proposito, dopo ogni operazione logico aritmetica che vogliamo "monitorare", dobbiamo inserire una istruzione INTO.
Si noti, inoltre, che il vettore relativo alla INT 04h, si trova all'offset 4*4=16=10h del segmento di memoria 0000h.

20.8.5 Effetti provocati dalle istruzioni INT, INTO e IRET, sugli operandi e sui flags

L'esecuzione di una istruzione INT con operando Imm8, non provoca, ovviamente, alcuna modifica sullo stesso operando.

L'esecuzione delle istruzioni INT e INTO pone OF=0 e TF=0; tutti gli altri campi del registro dei flags rimangono inalterati.
L'esecuzione dell'istruzione IRET altera, ovviamente, tutti i campi del registro dei flags.

Al momento di elaborare una richiesta di interruzione, la CPU disabilita automaticamente tutte le INT mascherabili; tali INT vengono riabilitate al termine della elaborazione dell'interruzione stessa.

20.9 Allineamento degli indirizzi di destinazione dei salti

In un precedente capitolo è stato detto che sarebbe assurdo pensare di allineare correttamente in memoria tutte le istruzioni di un programma; se vogliamo affrontare questo problema, la strada migliore da seguire consiste nel garantire l'allineamento solo per quelle istruzioni che si trovano in posizioni delicate.
Tra queste particolari istruzioni, troviamo sicuramente quelle che vengono raggiunte dalla CPU attraverso un salto; allineando tali istruzioni in modo opportuno, si permette alla CPU di ricaricare la prefetch queue nel modo più rapido possibile!

Nel capitolo 10 è stato spiegato che i diversi modelli di CPU della famiglia 80x86, caricano i blocchi di codice nella prefetch queue, in base a precisi metodi di allineamento (allineamento di prefetch); per comodità, viene riportata in Figura 20.14 la tabella relativa all'allineamento di prefetch nei principali modelli di CPU. In pratica, una CPU 8086, 80186, 80286 o 80386 SX, carica le istruzioni nella prefetch queue, a blocchi da 16 bit ciascuno; per le CPU 80386 DX i blocchi sono da 32 bit (4 byte), mentre per le CPU 80486 DX i blocchi sono da 16 byte.

Consideriamo allora il caso di una CPU 80386 DX; prima di tutto, è importante che i segmenti di codice vengano allineati almeno alla DWORD (indirizzi fisici multipli interi di 4 byte). A questo punto, possiamo facilmente allineare alla DWORD tutte le istruzioni raggiungibili attraverso un salto; tali istruzioni, sono principalmente quelle che delimitano l'inizio di un loop o di una procedura.
Supponendo, ad esempio, di avere un loop che inizia da una etichetta di nome start_loop, possiamo scrivere: Analogamente, nel caso delle CPU 80486 DX, bisogna innanzi tutto allineare i segmenti di codice al paragrafo (indirizzi fisici multipli interi di 16 byte); a questo punto, davanti a tutte le istruzioni che vogliamo allineare, dobbiamo inserire una direttiva del tipo:
ALIGN 16 ; allinea al paragrafo (con NOP)
Nel caso delle CPU 80486 e inferiori, questi accorgimenti possono portare ad un aumento veramente notevole della velocità di esecuzione; nel caso, invece, delle CPU di classe Pentium, la presenza di dispositivi come il branch prediction, la doppia pipeline, etc, permette di rendere meno importante il problema dell'allineamento delle istruzioni.