Assembly Base con MASM

Capitolo 16: Istruzioni per il trasferimento dati


Come è stato spiegato nei precedenti capitoli, ogni famiglia di CPU mette a disposizione un vasto numero di istruzioni che, nel loro insieme, formano il cosiddetto set di istruzioni della CPU. In base al compito che devono svolgere, le istruzioni possono essere suddivise in varie categorie; si possono citare, ad esempio, le istruzioni per il trasferimento dati, le istruzioni aritmetiche, le istruzioni logiche, le istruzioni per le stringhe, le istruzioni per il controllo della CPU e così via. In questo capitolo vengono esaminate in dettaglio, le principali istruzioni destinate al trasferimento dati da una sorgente a una destinazione.

Per ricavare i codici macchina di tutte le istruzioni presentate in questo capitolo e nei capitoli successivi, sono stati utilizzati i documenti ufficiali della Intel, denominati 231455.PDF (dal titolo Intel 8086 16 BIT HMOS MICROPROCESSOR) e 24319101.PDF (dal titolo Intel Architecture Software Developer's Manual - Volume 2 - Instruction Set Reference); questi documenti (in formato PDF) possono essere scaricati liberamente dal sito ufficiale della Intel o dalla sezione Documentazione tecnica di supporto al corso assembly dell’ area downloads di questo sito.

Ovviamente, tutte le istruzioni che coinvolgono operandi a 32 bit e/o indirizzamenti a 32 bit, presuppongono la presenza di una CPU 80386 o superiore; questo aspetto viene dato per scontato nel seguito del capitolo e nei capitoli successivi (per maggiori dettagli, si può consultare la tabella delle istruzioni della CPU).

16.1 L'istruzione MOV

Come si può facilmente intuire, l'istruzione MOV è sicuramente quella che viene utilizzata in modo più massiccio nei programmi Assembly; infatti, attraverso questo mnemonico si indica l'istruzione MOVe (trasferimento dati), che ha lo scopo di copiare una informazione da un operando sorgente (SRC) ad un operando destinazione (DEST).
La copia avviene bit per bit, nel senso che: e così via.

Tutto ciò implica che SRC e DEST debbano avere la stessa ampiezza in bit; in caso contrario, l'assembler genera un messaggio di errore.

Sono permesse tutte le combinazioni tra SRC e DEST, ad eccezione di quelle illegali o prive di senso; possiamo avere quindi i seguenti casi: Come sappiamo, le CPU della famiglia 80x86 non permettono il trasferimento dati (o qualsiasi altra operazione) tra Mem e Mem; nel caso particolare del trasferimento dati, ci si può servire della tecnica del DMA che verrà analizzata nella sezione Assembly Avanzato.
È illegale anche il trasferimento dati da SegReg a SegReg e da Imm a SegReg; ovviamente, in tutte le istruzioni che coinvolgono registri di segmento, gli operandi sono necessariamente a 16 bit.
Il trasferimento dati (o qualsiasi altra operazione) verso una destinazione di tipo Imm è chiaramente una istruzione priva di senso; infatti, sappiamo che un operando di tipo Imm è un semplice valore numerico che non è contenuto, né in un registro, né in una locazione di memoria. Ciò impedisce alla CPU di accedere in scrittura a questo tipo di operando, per modificarne il contenuto; nei precedenti capitoli abbiamo visto che un Imm che figura come operando SRC di una istruzione, viene incorporato dall'assembler, direttamente nel codice macchina dell'istruzione stessa. Molte delle considerazioni esposte in questo capitolo in relazione alla istruzione MOV, valgono nel caso generale, per tutte le istruzioni della CPU; di conseguenza, l'istruzione MOV verrà analizzata con estremo dettaglio, in modo da non dover ripetere per le altre istruzioni tutti questi concetti di validità generale.

L'aspetto che crea più (ingiustificati) problemi ai programmatori Assembly meno esperti, è dato dalla corretta gestione degli indirizzamenti relativi ad eventuali operandi di tipo Mem presenti nelle istruzioni; sfruttiamo allora proprio l'istruzione MOV per analizzare i vari casi che si possono presentare.
A tale proposito, consideriamo un programma Assembly in formato EXE, dotato di un segmento di dati chiamato DATASEGM, un segmento di codice chiamato CODESEGM e un segmento di stack chiamato STACKSEGM; vediamo allora quello che succede quando una istruzione coinvolge un operando di tipo Mem, cioè un dato statico, definito in uno qualsiasi dei tre segmenti appena citati.
Negli esempi che seguono, si assume per semplicità che la componente Offset del dato non subisca alcuna rilocazione e che il segmento di programma nel quale viene definito il dato stesso, sia allineato al paragrafo; inoltre, assumiamo che il dato sia correttamente allineato in memoria, in modo che la CPU lo possa leggere o scrivere, con un unico accesso.

16.1.1 Il dato si trova in DATASEGM

Supponiamo che all'offset 0008h del blocco DATASEGM del nostro programma, sia presente la seguente definizione:
Variabile8 db 0D6h ; 11010110b
Quando l'assembler incontra questa definizione, crea all'offset 0008h di DATASEGM, una locazione da 8 bit nella quale inserisce il valore iniziale D6h, cioè 11010110b.
Supponiamo inoltre che quando inizia l'esecuzione del programma, si abbia:
DATASEGM = 0CA5h
Di conseguenza, il dato Variabile8 viene caricato in memoria all'indirizzo logico DATASEGM:0008h, cioè 0CA5h:0008h.
Ad un certo punto del nostro programma, abbiamo la necessità di effettuare un trasferimento dati da Variabile8 al registro BL; analizziamo allora i vari metodi di indirizzamento che ci permettono di svolgere questa operazione. La Figura 16.1 illustra in modo schematico come avviene il trasferimento dati da Variabile8 a BL e viceversa, attraverso un Data Bus a 32 bit. La Figura 16.1 ci permette di sottolineare ancora una volta che conviene sempre disegnare il vettore delle celle di memoria, con gli indirizzi crescenti da destra verso sinistra; in questo modo, tutti i dati di tipo WORD, DWORD, etc, presenti in memoria, appaiono disposti nel verso previsto dalla notazione posizionale (cioè, con il peso delle cifre che cresce da destra verso sinistra).

Normalmente, subito dopo l'entry point del programma, sono presenti le classiche istruzioni che caricano DATASEGM in DS; inoltre, è presente anche una direttiva ASSUME che associa DATASEGM a DS. Questa situazione ci permette di gestire Variabile8 nel modo più semplice possibile; infatti, sempre in riferimento alla Figura 16.1, possiamo scrivere l'istruzione:
mov bl, Variabile8
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m e da un Disp16; nel nostro caso, l'istruzione è to register (d=1) e gli operandi sono a 8 bit (w=0). Otteniamo quindi:
Opcode = 10001010b = 8Ah
Il registro destinazione è BL, per cui (Capitolo 11) reg=011b; la sorgente è un Disp16, per cui mod=00b, r/m=110b. Otteniamo allora:
mod_reg_r/m = 00011110b = 1Eh
Il Disp16 è:
Disp16 = 0000000000001000b = 0008h
Non essendo necessario alcun segment override (in quanto DS è il registro di segmento predefinito per i dati), l'assembler genera quindi il codice macchina:
10001010b 00011110b 0000000000001000b = 8Ah 1Eh 0008h
Quando la CPU incontra questa istruzione, esegue le seguenti operazioni: Cosa succede se non è presente la direttiva ASSUME?
In tal caso, siamo costretti a scrivere in modo esplicito:
mov bl, ds:Variabile8
Incontrando questa istruzione, l'assembler genera lo stesso codice macchina precedente, in quanto DS è il registro di segmento predefinito per i dati; ovviamente, l'assembler sa che la CPU associa automaticamente l'offset 0008h alla componente Seg contenuta in DS.

Negli esempi appena presentati, il dato Variabile8 è stato gestito attraverso un indirizzo NEAR; infatti, nel codice macchina viene inserita la sola componente Offset di Variabile8.

Supponiamo ora di voler gestire DATASEGM con ES; a tale proposito, carichiamo DATASEGM in ES e utilizziamo una direttiva ASSUME per associare DATASEGM ad ES.
In base a queste premesse, analizziamo ciò che accade quando l'assembler incontra la solita istruzione:
mov bl, Variabile8
Grazie alla precedente direttiva ASSUME, l'assembler tratta il nome Variabile8 come un Disp16 riferito a ES; questo registro di segmento non è quello predefinito per i dati e quindi è necessario un segment override. L'assembler genera un codice macchina del tutto simile a quello del precedente esempio, preceduto però dal codice 26h, relativo al registro ES; si ottiene quindi:
00100110b 10001010b 00011110b 0000000000001000b = 26h 8Ah 1Eh 0008h
Quando la CPU incontra questa istruzione, associa l'offset 0008h ad ES, accede in memoria all'indirizzo ES:0008h (cioè a 0CA5h:0008h), legge gli 8 bit 11010110b e li trasferisce negli 8 bit del registro BL; in assenza del codice 26h, la CPU avrebbe associato l'offset 0008h a DS.

Come al solito, se non è presente la direttiva ASSUME che associa DATASEGM a ES, siamo costretti a scrivere in modo esplicito:
mov bl, es:Variabile8
Il codice macchina che si ottiene è identico a quello precedente.

L'utilizzo di ES, che non è il registro di segmento predefinito per i dati, comporta la gestione di Variabile8 attraverso un indirizzo di tipo FAR, cioè, un indirizzo logico completo Seg:Offset; ogni riferimento a es:Variabile8 in una istruzione, costringe l'assembler a generare un codice macchina preceduto da un segment override.
Un segment override inserito in una istruzione, fa crescere di 1 byte la dimensione del codice macchina dell'istruzione stessa; di conseguenza, una istruzione contenente un segment override, occupa maggiore spazio in memoria e viene elaborata meno velocemente dalla CPU. L'ideale sarebbe quindi utilizzare sempre indirizzamenti di tipo NEAR; come vedremo però nel seguito del capitolo e nei capitoli successivi, in molti casi non esiste alternativa all'utilizzo degli indirizzamenti di tipo FAR.

Come si può facilmente intuire, se carichiamo DATASEGM, sia in DS, sia in ES, e poi scriviamo:
ASSUME DS: DATASEGM, ES:DATASEGM
allora l'assembler non inserirà alcun segment override nel codice macchina dell'istruzione:
mov bl, Variabile8
Infatti, in un caso di questo genere, l'assembler sfrutta automaticamente l'associazione tra DS e DATASEGM.

Passiamo ora alla gestione di Variabile8 attraverso i registri puntatori; in questo caso, la direttiva ASSUME diventa del tutto superflua in quanto stiamo indicando esplicitamente all'assembler (e quindi anche alla CPU) i registri da utilizzare per indirizzare Variabile8.
Per evitare errori grossolani, è importante ricordare che, tra registri puntatori e registri di segmento, in assenza di segment override valgono le seguenti associazioni predefinite: Negli effective address del tipo [base+index+disp], in assenza di segment override, se la base è BP viene utilizzato il registro di segmento predefinito SS; se la base è BX, il registro di segmento predefinito è DS. Supponiamo allora di voler gestire Variabile8 attraverso il registro puntatore DI; carichiamo innanzi tutto DATASEGM in DS e scriviamo poi:
mov di, offset Variabile8
Questa istruzione carica il valore 0008h (offset di Variabile8) in DI.
In base a queste premesse, possiamo scrivere l'istruzione:
mov bl, [di]
Dalle tabelle, si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m; nel nostro caso, l'istruzione è to register, per cui d=1. Grazie a BL, l'assembler capisce che gli operandi sono a 8 bit (w=0); otteniamo quindi:
Opcode = 10001010b = 8Ah
Il registro destinazione è BL, per cui reg=011b; la sorgente è un effective address del tipo [DI], per cui mod=00b, r/m=101b. Otteniamo allora:
mod_reg_r/m = 00011101b = 1Dh
In mancanza di diverse indicazioni da parte del programmatore, il registro puntatore DI viene associato automaticamente a DS, per cui non è necessario alcun segment override; l'assembler genera quindi il codice macchina:
10001010b 00011101b = 8Ah 1Dh
Quando la CPU incontra questa istruzione, legge l'offset 0008h contenuto in DI, accede in memoria all'indirizzo DS:0008h (cioè a 0CA5h:0008h), legge gli 8 bit 11010110b e li trasferisce negli 8 bit del registro BL.

Nell'esempio appena illustrato, il dato Variabile8 è stato gestito con un indirizzo NEAR contenuto in DI; ciò è una diretta conseguenza della associazione predefinita tra DI e DS.

Se abbiamo caricato DATASEGM in ES, la precedente istruzione deve essere scritta, necessariamente, come:
mov bl, es:[di]
Il registro DI non viene associato automaticamente a ES, per cui l'assembler genera un codice macchina simile a quello visto prima, preceduto però dal segment override 26h relativo allo stesso ES; in caso contrario, DI verrebbe associato automaticamente a DS. Si ottiene quindi:
00100110b 10001010b 00011101b = 26h 8Ah 1Dh
Quando la CPU incontra questa istruzione, legge l'offset 0008h contenuto in DI, accede in memoria all'indirizzo ES:0008h (cioè a 0CA5h:0008h), legge gli 8 bit 11010110b e li trasferisce negli 8 bit del registro BL; in assenza del codice 26h, la CPU avrebbe associato l'offset 0008h al registro DS.

Nell'esempio appena illustrato, il dato Variabile8 è stato gestito con un indirizzo FAR contenuto in ES:DI; ciò è una diretta conseguenza del fatto che DI non viene associato automaticamente a ES.

Se vogliamo complicarci la vita, nessuno ci impedisce di accedere a Variabile8 con BP; in questo caso, dobbiamo ricordarci che BP viene associato automaticamente a SS, per cui dobbiamo ricorrere, in ogni caso, al segment override.
Anche se abbiamo caricato DATASEGM in DS, dobbiamo scrivere quindi:
mov bl, ds:[bp]
Dalle tabelle, si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m; nel nostro caso, l'istruzione è to register, per cui d=1. Grazie a BL, l'assembler capisce che gli operandi sono a 8 bit (w=0); otteniamo quindi:
Opcode = 10001010b = 8Ah
Il registro destinazione è BL, per cui reg=011b; la sorgente è un effective address del tipo [BP]. Come abbiamo visto nel Capitolo 11, in questo caso si usa la forma [BP+Disp8] aggiungendo al codice macchina un Disp8=00h (spiazzamento a 8 bit); la codifica di questo tipo di indirizzamento è mod=01b, r/m=110b. Otteniamo allora:
mod_reg_r/m = 01011110b = 5Eh
Il Disp8 è:
Disp8 = 00000000b = 00h
Il registro BP non viene associato automaticamente a DS, per cui l'assembler, genera un codice macchina preceduto dal segment override 3Eh relativo allo stesso DS; in caso contrario, BP verrebbe associato automaticamente a SS. Si ottiene quindi:
00111110b 10001010b 01011110b 00000000b = 3Eh 8Ah 5Eh 00h
Quando la CPU incontra questa istruzione, calcola:
BP + 00h = 0008h + 00h = 0008h
Poi accede in memoria all'indirizzo DS:0008h (cioè a 0CA5h:0008h), legge gli 8 bit 11010110b e li trasferisce negli 8 bit del registro BL; in assenza del codice 3Eh, la CPU avrebbe associato l'offset 0008h al registro SS.

Nell'esempio appena illustrato, il dato Variabile8 è stato gestito con un indirizzo FAR contenuto in DS:BP; ciò è una diretta conseguenza del fatto che BP non viene associato automaticamente a DS.

La situazione è perfettamente analoga nel caso di un effective address del tipo [base+index+disp]; supponiamo, ad esempio, di voler accedere a Variabile8 attraverso [BX+SI+Disp], con BX=0004h, SI=0002h e Disp=0002h. Se DATASEGM è stato caricato in DS, possiamo evitare il segment override scrivendo:
mov bl, [bx+si+0002h]
Osserviamo che in questa istruzione, figura il registro (puntatore) BX come sorgente e il registro BL come destinazione; ovviamente, dopo il trasferimento dati, gli 8 bit meno significativi dell'offset 0004h contenuto in BX, vengono sovrascritti!
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m e da un Disp a 8 bit; infatti (Capitolo 11), 0002h è minore di 127, per cui l'assembler utilizza un Disp8=02h. Nel nostro caso, l'istruzione è to register, per cui d=1; grazie a BL, l'assembler capisce che gli operandi sono a 8 bit (w=0). Otteniamo quindi:
Opcode = 10001010b = 8Ah
Il registro destinazione è BL, per cui reg=011b; la sorgente è un effective address del tipo [BX+SI+Disp8], per cui mod=01b, r/m=000b. Otteniamo allora:
mod_reg_r/m = 01011000b = 58h
Il Disp8 è:
Disp8 = 00000010b = 02h
In assenza di diverse indicazioni da parte del programmatore, la base BX viene associata automaticamente a DS, per cui non è necessario alcun segment override; l'assembler genera quindi il codice macchina:
10001010b 01011000b 00000010b = 8Ah 58h 02h
Quando la CPU incontra questa istruzione, calcola:
BX + SI + 02h = 0004h + 0002h + 02h = 0008h
Poi accede in memoria all'indirizzo DS:0008h (cioè a 0CA5h:0008h), legge gli 8 bit 11010110b e li trasferisce negli 8 bit del registro BL.

Nell'esempio appena illustrato, il dato Variabile8 è stato gestito con un indirizzo NEAR contenuto in (BX+SI+Disp); ciò è una diretta conseguenza del fatto che la base BX, viene associata automaticamente a DS.

Se abbiamo caricato DATASEGM in ES, la precedente istruzione deve essere scritta, necessariamente, come:
mov bl, es:[bx+si+0002h]
La base BX non viene associata automaticamente a ES, per cui l'assembler deve generare un codice macchina simile a quello visto prima, ma preceduto dal segment override 26h relativo allo stesso ES; in caso contrario, la base BX verrebbe associata automaticamente a DS. Si ottiene quindi:
00100110b 10001010b 01011000b 00000010b = 26h 8Ah 58h 02h
Quando la CPU incontra questa istruzione, calcola:
BX + SI + 02h = 0004h + 0002h + 02h = 0008h
Poi accede in memoria all'indirizzo ES:0008h (cioè a 0CA5h:0008h), legge gli 8 bit 11010110b e li trasferisce negli 8 bit del registro BL; in assenza del codice 26h, la CPU avrebbe associato l'offset 0008h al registro DS.

Nell'esempio appena illustrato, il dato Variabile8 è stato gestito con un indirizzo FAR contenuto in ES:(BX+SI+Disp); ciò è una diretta conseguenza del fatto che la base BX, non viene associata automaticamente a ES.

Supponiamo infine di voler accedere a Variabile8 attraverso [BP+DI+Disp], con BP=0004h, DI=0002h e Disp=0002h; anche se DATASEGM è stato caricato in DS, non possiamo evitare il segment override e dobbiamo per forza scrivere:
mov bl, ds:[bp+di+0002h]
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m e da un Disp8=02h; nel nostro caso, l'istruzione è to register, per cui d=1. Grazie a BL, l'assembler capisce che gli operandi sono a 8 bit (w=0); otteniamo quindi:
Opcode = 10001010b = 8Ah
Il registro destinazione è BL, per cui reg=011b; la sorgente è un effective address del tipo [BP+DI+Disp8], per cui mod=01b, r/m=011b. Otteniamo allora:
mod_reg_r/m = 01011011b = 5Bh
Il Disp8 è:
Disp8 = 00000010b = 02h
La base BP non viene associata automaticamente a DS, per cui l'assembler deve generare un codice macchina preceduto dal segment override 3Eh relativo allo stesso DS; in caso contrario, la base BP verrebbe associata automaticamente a SS. L'assembler genera quindi il codice macchina:
00111110b 10001010b 01011011b 00000010b = 3Eh 8Ah 5Bh 02h
Quando la CPU incontra questa istruzione, calcola:
BP + DI + 02h = 0004h + 0002h + 02h = 0008h
Poi accede in memoria all'indirizzo DS:0008h (cioè a 0CA5h:0008h), legge gli 8 bit 11010110b e li trasferisce negli 8 bit del registro BL; in assenza del codice 3Eh, la CPU avrebbe associato l'offset 0008h al registro SS.

Nell'esempio appena illustrato, il dato Variabile8 è stato gestito con un indirizzo FAR contenuto in DS:(BP+DI+Disp); ciò è una diretta conseguenza del fatto che la base BP, non viene associata automaticamente a DS.

16.1.2 Il dato si trova in CODESEGM

Supponiamo che all'offset 0008h del blocco CODESEGM del nostro programma, sia presente la seguente definizione:
Variabile16 dw ? ; dato non inizializzato a 16 bit
Quando l'assembler incontra questa definizione, crea all'offset 0008h di CODESEGM, una locazione da 16 bit, nella quale non viene inserito alcun valore iniziale.
Supponiamo inoltre che, quando inizia l'esecuzione del programma, si abbia:
CODESEGM = 0CA5h
Di conseguenza, il dato Variabile16 viene caricato in memoria all'indirizzo logico CODESEGM:0008h, cioè 0CA5h:0008h.
Ad un certo punto del nostro programma, abbiamo la necessità di effettuare un trasferimento dati dal registro DI=28D6h a Variabile16; analizziamo allora i vari metodi di indirizzamento che ci permettono di svolgere questa operazione. La Figura 16.2 illustra in modo schematico come avviene il trasferimento dati da Variabile16 a DI e viceversa, attraverso un Data Bus a 32 bit. È importante ricordare che tutte le CPU della famiglia 80x86, seguono la convenzione little endian per la disposizione dei dati in memoria; in base a tale convenzione, un dato di tipo WORD, DWORD, etc, viene disposto in memoria in modo che il suo BYTE meno significativo occupi l'indirizzo più basso. In Figura 16.2, ad esempio, osserviamo che il dato a 16 bit 28D6h, viene disposto in memoria con il BYTE meno significativo D6h che occupa l'indirizzo fisico 0CA58h e il BYTE più significativo 28h che occupa l'indirizzo fisico 0CA59h.
Sempre in base alle convenzioni seguite dalle CPU della famiglia 80x86, l'indirizzo fisico di un dato di tipo WORD, DWORD, etc, coincide con l'indirizzo fisico del suo BYTE meno significativo; nel caso di Figura 16.2, l'indirizzo fisico del dato 28D6h è 0CA58h.

La Figura 16.2 ci permette anche di ricordare che i registri BX, SI, DI e BP, possono essere utilizzati, sia come registri puntatori, sia come normalissimi registri generali, destinati a contenere dei generici valori numerici; nel nostro esempio, infatti, stiamo utilizzando DI come registro generale.

Nel momento in cui la CPU sta eseguendo le istruzioni presenti in CODESEGM si ha, ovviamente, CS=CODESEGM; è necessario ricordare sempre che il compito di gestire CS spetta rigorosamente al SO e alla CPU e non al programmatore.

Nei precedenti capitoli, abbiamo visto che l'assembler MASM, all'inizio di ogni segmento di codice, come CODESEGM, richiede una direttiva del tipo:
ASSUME CS: CODESEGM
In base a queste premesse, possiamo subito intuire che per accedere al dato Variabile16, non possiamo fare a meno del segment override; nel caso più semplice, possiamo scrivere l'istruzione:
mov Variabile16, di
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m e da un Disp16; nel nostro caso, l'istruzione è from register (d=0) e gli operandi sono a 16 bit (w=1). Otteniamo quindi:
Opcode = 10001001b = 89h
Il registro sorgente è DI, per cui reg=111b; la destinazione è un Disp16, per cui mod=00b, r/m=110b. Otteniamo allora:
mod_reg_r/m = 00111110b = 3Eh
Il Disp16 è:
Disp16 = 0000000000001000b = 0008h
Grazie alla precedente direttiva ASSUME, l'assembler tratta Variabile16 come un Disp16 riferito a CS; non essendo CS il registro di segmento predefinito per i dati, si rende necessario il segment override 2Eh, relativo allo stesso CS, altrimenti, la CPU assocerebbe automaticamente l'offset 0008h a DS. L'assembler genera quindi il codice macchina:
00101110b 10001001b 00111110b 0000000000001000b = 2Eh 89h 3Eh 0008h
Quando la CPU incontra questa istruzione, legge i 16 bit 0010100011010110b contenuti in DI e li trasferisce nella locazione da 16 bit che si trova in memoria all'indirizzo CS:0008h (cioè 0CA5h:0008h); in assenza del codice 2Eh, la CPU avrebbe associato l'offset 0008h a DS.

Nell'esempio appena illustrato, il dato Variabile16 è stato gestito con un indirizzo di tipo FAR; ciò è una diretta conseguenza del fatto che CS non è il registro di segmento predefinito per i dati.

La situazione non cambia nel momento in cui decidiamo di utilizzare i registri puntatori; indipendentemente quindi dal registro puntatore utilizzato, siamo obbligati a specificare in modo esplicito il segment override CS.
Supponiamo, ad esempio, di caricare in SI l'offset di Variabile16; Vediamo allora quello che succede quando l'assembler incontra l'istruzione:
mov cs:[si], di
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m; nel nostro caso, l'istruzione è from register, per cui d=0. Grazie alla presenza di DI, l'assembler capisce che gli operandi sono a 16 bit, per cui w=1; otteniamo quindi:
Opcode = 10001001b = 89h
Il registro sorgente è DI, per cui reg=111b; la destinazione è un effective address del tipo [SI], per cui mod=00b, r/m=100b. Otteniamo allora:
mod_reg_r/m = 00111100b = 3Ch
Il registro SI non viene associato automaticamente a CS, per cui l'assembler genera un codice macchina preceduto dal segment override 2Eh relativo allo stesso CS; in caso contrario, SI verrebbe associato automaticamente a DS. L'assembler genera quindi il codice macchina:
00101110b 10001001b 00111100b = 2Eh 89h 3Ch
Quando la CPU incontra questa istruzione, legge i 16 bit 0010100011010110b contenuti in DI e li trasferisce nella locazione da 16 bit che si trova in memoria all'indirizzo CS:SI (cioè 0CA5h:0008h); in assenza del codice 2Eh, la CPU avrebbe associato l'offset 0008h a DS.

Nell'esempio appena illustrato, il dato Variabile16 è stato gestito con un indirizzo FAR contenuto in CS:SI; ciò è una diretta conseguenza del fatto che SI, non viene associato automaticamente a CS.

Supponiamo infine di voler accedere a Variabile16 attraverso [BP+SI+Disp], con BP=0004h, SI=0002h e Disp=0002h; anche in questo caso, non possiamo evitare il segment override e dobbiamo per forza scrivere:
mov cs:[bp+si+0002h], di
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m e da un Disp8=02h; nel nostro caso, l'istruzione è from register, per cui d=0. Grazie alla presenza di DI, l'assembler capisce che gli operandi sono a 16 bit, per cui w=1; otteniamo quindi:
Opcode = 10001001b = 89h
Il registro sorgente è DI, per cui reg=111b; la destinazione è un effective address del tipo [BP+SI+Disp8], per cui mod=01b, r/m=010b. Otteniamo allora:
mod_reg_r/m = 01111010b = 7Ah
Il Disp8 è:
Disp8 = 00000010b = 02h
La base BP non viene associata automaticamente a CS, per cui l'assembler genera un codice macchina preceduto dal segment override 2Eh relativo allo stesso CS; in caso contrario, la base BP verrebbe associata automaticamente a SS. L'assembler genera quindi il codice macchina:
00101110b 10001001b 01111010b 00000010b = 2Eh 89h 7Ah 02h
Quando la CPU incontra questa istruzione, legge i 16 bit 0010100011010110b contenuti in DI e li trasferisce nella locazione da 16 bit che si trova in memoria all'indirizzo CS:(BP+SI+0002h) (cioè 0CA5h:0008h); in assenza del codice 2Eh, la CPU avrebbe associato l'offset 0008h a DS.

Nell'esempio appena illustrato, il dato Variabile16 è stato gestito con un indirizzo FAR contenuto in CS:(BP+SI+0002h); ciò è una diretta conseguenza del fatto che la base BP, non viene associata automaticamente a CS.

16.1.3 Il dato si trova in STACKSEGM

Supponiamo che all'offset 0008h del blocco STACKSEGM del nostro programma, sia presente la seguente definizione:
Variabile32 dd 3ABF28D6h ; 00111010101111110010100011010110b
Quando l'assembler incontra questa definizione, crea all'offset 0008h di STACKSEGM, una locazione da 32 bit, nella quale inserisce il valore iniziale 3ABF28D6h, cioè 00111010101111110010100011010110b.
Supponiamo, inoltre, che quando inizia l'esecuzione del programma, si abbia:
STACKSEGM = 0CA5h
Di conseguenza, il dato Variabile32 viene caricato in memoria, all'indirizzo logico STACKSEGM:0008h, cioè 0CA5h:0008h.
Ad un certo punto del nostro programma, abbiamo la necessità di effettuare un trasferimento dati da Variabile32 al registro accumulatore EAX; analizziamo allora i vari metodi di indirizzamento che ci permettono di svolgere questa operazione. La Figura 16.3 illustra in modo schematico come avviene il trasferimento dati da Variabile32 a EAX e viceversa, attraverso un Data Bus a 32 bit. In base alle convenzioni citate in precedenza, in Figura 16.3 l'indirizzo fisico del dato 3ABF28D6h è 0CA58h; osserviamo, inoltre, che il dato è allineato alla DWORD, per cui la sua lettura o scrittura richiede un unico accesso in memoria da parte della CPU.

Se il segmento STACKSEGM ha l'attributo di combinazione STACK, allora il SO pone SS=STACKSEGM; in caso contrario, il compito di inizializzare SS spetta al programmatore che, a sua volta, deve porre SS=STACKSEGM.

Se vogliamo servirci dell'identificatore Variabile32, dobbiamo inserire nel blocco codice una direttiva del tipo:
ASSUME SS: STACKSEGM
In base a queste premesse, possiamo affermare che per accedere nel modo più semplice possibile al dato Variabile32, possiamo scrivere l'istruzione:
mov eax, Variabile32
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 1010000w, seguito da un Disp16 (forma compatta, dovuta alla presenza dell'accumulatore); nel nostro caso, gli operandi sono a 32 bit (full size), per cui w=1. Otteniamo quindi:
Opcode = 10100001b = A1h
Il Disp16 è:
Disp16 = 0000000000001000b = 0008h
Grazie alla precedente direttiva ASSUME, l'assembler tratta Variabile32 come un Disp16 riferito a SS; non essendo SS il registro di segmento predefinito per i dati, si rende necessario il segment override 36h, relativo allo stesso SS. Gli operandi sono a 32 bit, per cui è necessario anche il prefisso 66h; l'assembler genera quindi il codice macchina:
01100110b 00110110b 10100001b 0000000000001000b = 66h 36h A1h 0008h
Quando la CPU incontra questa istruzione, associa l'offset 0008h a SS, accede in memoria all'indirizzo SS:0008h (cioè a 0CA5h:0008h), legge i 32 bit 00111010101111110010100011010110b e li trasferisce nei 32 bit di EAX; in assenza del codice 36h, la CPU avrebbe associato l'offset 0008h a DS.

Se omettiamo la precedente direttiva ASSUME, siamo costretti a scrivere in modo esplicito:
mov eax, ss:Variabile32
In tal caso, l'assembler genera lo stesso codice macchina precedente.

Nell'esempio appena illustrato, il dato Variabile32 è stato gestito con un indirizzo di tipo FAR; ciò è una diretta conseguenza del fatto che SS non è il registro di segmento predefinito per i dati.

Se decidiamo di utilizzare i registri puntatori, possiamo evitare o meno il segment override; infatti, BX, SI e DI vengono associati automaticamente a DS, mentre BP viene associato automaticamente a SS.
Supponiamo, ad esempio, di caricare in SI l'offset di Variabile32; in tal caso, il segment override è necessario e dobbiamo quindi scrivere l'istruzione:
mov eax, ss:[si]
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m; nel nostro caso, l'istruzione è to register, per cui d=1. Grazie alla presenza di EAX, l'assembler capisce che gli operandi sono a 32 bit, per cui w=1 (full size); otteniamo quindi:
Opcode = 10001011b = 8Bh
Il registro destinazione è EAX, per cui reg=000b; la sorgente è un effective address del tipo [SI], per cui mod=00b, r/m=100b. Otteniamo allora:
mod_reg_r/m = 00000100b = 04h
Il registro SI non viene associato automaticamente a SS, per cui l'assembler, genera un codice macchina preceduto dal segment override 36h relativo allo stesso SS; in caso contrario, SI verrebbe associato automaticamente a DS. Gli operandi sono a 32 bit, per cui è necessario anche il prefisso 66h; l'assembler genera quindi il codice macchina:
01100110b 00110110b 10001011b 00000100b = 66h 36h 8Bh 04h
Quando la CPU incontra questa istruzione, legge da SI l'offset 0008h, accede in memoria all'indirizzo SS:0008h (cioè a 0CA5h:0008h), legge i 32 bit 00111010101111110010100011010110b e li trasferisce nei 32 bit del registro EAX; in assenza del codice 36h, la CPU avrebbe associato l'offset 0008h al registro DS.

Nell'esempio appena illustrato, il dato Variabile32 è stato gestito con un indirizzo FAR contenuto in SS:SI; ciò è una diretta conseguenza del fatto che SI non viene associato automaticamente a SS.

Supponiamo infine di voler accedere a Variabile32 attraverso [BP+SI+Disp], con BP=0004h, SI=0002h e Disp=0002h; in questo caso, il segment override viene evitato, in quanto la base BP viene associata automaticamente a SS. Possiamo quindi scrivere l'istruzione:
mov eax, [bp+si+0002h]
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m e da un Disp8=02h; nel nostro caso, l'istruzione è to register, per cui d=1. Grazie alla presenza di EAX, l'assembler capisce che gli operandi sono a 32 bit, per cui w=1 (full size); otteniamo quindi:
Opcode = 10001011b = 8Bh
Il registro destinazione è EAX, per cui reg=000b; la sorgente è un effective address del tipo [BP+SI+Disp8], per cui mod=01b, r/m=010b. Otteniamo allora:
mod_reg_r/m = 01000010b = 42h
Il Disp8 è:
Disp8 = 00000010b = 02h
La base BP viene associata automaticamente a SS, per cui il segment override non è necessario; gli operandi sono a 32 bit, per cui è necessario il prefisso 66h. L'assembler genera quindi il codice macchina:
01100110b 10001011b 01000010b 00000010b = 66h 8Bh 42h 02h
Quando la CPU incontra questa istruzione, legge da (BP+SI+02h) l'offset 0008h, accede in memoria all'indirizzo SS:0008h (cioè a 0CA5h:0008h), legge i 32 bit 00111010101111110010100011010110b e li trasferisce nei 32 bit del registro EAX.

Nell'esempio appena illustrato, il dato Variabile32 è stato gestito con un indirizzo NEAR contenuto in (BP+SI+02h); ciò è una diretta conseguenza del fatto che la base BP, viene associata automaticamente a SS. Se vogliamo servirci della libreria EXELIB per verificare in pratica gli esempi appena esposti, possiamo ricorrere ad un semplice espediente che ci permette di posizionare i dati all'offset desiderato; in riferimento alla Figura 16.1, all'inizio del blocco DATASEGM, dotato di attributo di allineamento PARA, se vogliamo creare il dato Variabile8 all'offset 0008h possiamo scrivere: In riferimento alla Figura 16.2, all'inizio del blocco CODESEGM, dotato di attributo di allineamento PARA, se vogliamo creare il dato Variabile16 all'offset 0008h, possiamo scrivere prima dell'entry point: In riferimento alla Figura 16.3, all'inizio del blocco STACKSEGM, dotato di attributo di allineamento PARA, se vogliamo creare il dato Variabile32 all'offset 0008h, possiamo scrivere:

16.1.4 Trasferimento dati da Imm a Reg/Mem

Un aspetto molto interessante da analizzare, riguarda il caso di una istruzione MOV (o di una qualsiasi altra istruzione), che comprende un operando sorgente di tipo Imm; in particolare, analizziamo il procedimento seguito dall'assembler per adattare l'operando sorgente Imm all'ampiezza in bit dell'operando destinazione.
Ricordiamo che, se Imm è un numero positivo, l'estensione della sua ampiezza, da m bit a n bit (con n > m), consiste nell'aggiunta di n-m cifre 0 alla sua sinistra; se Imm è un numero negativo, l'estensione della sua ampiezza, da m bit a n bit, consiste nell'aggiunta di n-m cifre 1 alla sua sinistra.

Prima di tutto, inseriamo nel nostro programma la seguente dichiarazione:
TEMPERATURA = +25
Con questa dichiarazione, stiamo dicendo all'assembler che l'identificatore TEMPERATURA rappresenta un generico valore numerico pari a +25; l'importante compito di stabilire l'ampiezza in bit di questo valore, spetta all'assembler.

Nel caso di un operando destinazione di tipo Reg, la situazione è molto semplice; consideriamo, ad esempio, la seguente istruzione:
mov dl, TEMPERATURA
Dalle tabelle, si ricava che il codice macchina di questa istruzione è formato dall'Opcode 1011_w_reg, seguito dal campo Imm; grazie alla presenza di DL, l'assembler capisce che gli operandi sono a 8 bit, per cui w=0. Il registro destinazione è DL, per cui reg=010b; otteniamo quindi:
Opcode = 10110010b = B2h
Siccome gli operandi sono a 8 bit, l'assembler deve esprimere il valore +25 sotto forma di Imm8; otteniamo quindi:
Imm8 = 00011001b = 19h
L'assembler genera quindi il codice macchina:
10110010b 00011001b = B2h 19h
Quando la CPU incontra questa istruzione, trasferisce nel registro DL gli 8 bit del valore immediato 00011001b (incorporato nel codice macchina dell'istruzione stessa).

Consideriamo l'istruzione:
mov dx, TEMPERATURA
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 1011_w_reg, seguito dal campo Imm; grazie alla presenza di DX, l'assembler capisce che gli operandi sono a 16 bit, per cui w=1. Il registro destinazione è DX, per cui reg=010b; otteniamo quindi:
Opcode = 10111010b = BAh
Siccome gli operandi sono a 16 bit, l'assembler deve esprimere il valore +25 sotto forma di Imm16; otteniamo quindi:
Imm16 = 0000000000011001b = 0019h
L'assembler genera quindi il codice macchina:
10111010b 0000000000011001b = BAh 0019h
Quando la CPU incontra questa istruzione, trasferisce nel registro DX i 16 bit del valore immediato 0000000000011001b (incorporato nel codice macchina dell'istruzione stessa).

Consideriamo l'istruzione:
mov edx, TEMPERATURA
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 1011_w_reg, seguito dal campo Imm; grazie alla presenza di EDX, l'assembler capisce che gli operandi sono a 32 bit, per cui w=1 (full size). Il registro destinazione è EDX, per cui reg=010b; otteniamo quindi:
Opcode = 10111010b = BAh
Siccome gli operandi sono a 32 bit, l'assembler deve esprimere il valore +25 sotto forma di Imm32; otteniamo quindi:
Imm32 = 00000000000000000000000000011001b = 00000019h
Per indicare alla CPU che gli operandi sono a 32 bit, è necessario il prefisso 66h; l'assembler genera quindi il codice macchina:
01100110b 10111010b 00000000000000000000000000011001b = 66h BAh 00000019h
Quando la CPU incontra questa istruzione, trasferisce nel registro EDX i 32 bit del valore immediato 00000000000000000000000000011001b (incorporato nel codice macchina dell'istruzione stessa).

Cosa succede se dichiariamo TEMPERATURA come:
TEMPERATURA = -25
Con questa dichiarazione, stiamo dicendo all'assembler che l'identificatore TEMPERATURA rappresenta un generico valore numerico con segno, pari a -25; questa volta, l'assembler deve stabilire l'ampiezza in bit di TEMPERATURA, rispettandone anche il segno negativo.

Ripetendo gli esempi precedenti, possiamo subito intuire che nel codice macchina cambia solamente il valore assunto dall'operando Imm; partiamo allora dall'istruzione:
mov dl, TEMPERATURA
Siccome gli operandi sono a 8 bit, l'assembler deve esprimere il valore -25 sotto forma di Imm8 in complemento a 2; otteniamo quindi:
Imm8 = 256 - 25 = 231 = 11100111b = E7h
L'assembler genera quindi il codice macchina:
10110010b 11100111b = B2h E7h
Quando la CPU incontra questa istruzione, trasferisce nel registro DL gli 8 bit del valore immediato 11100111b (incorporato nel codice macchina dell'istruzione stessa).

Consideriamo l'istruzione:
mov dx, TEMPERATURA
Siccome gli operandi sono a 16 bit, l'assembler deve esprimere il valore -25 sotto forma di Imm16 in complemento a 2; otteniamo quindi:
Imm16 = 65536 - 25 = 65511 = 1111111111100111b = FFE7h
L'assembler genera quindi il codice macchina:
10111010b 1111111111100111b = BAh FFE7h
Quando la CPU incontra questa istruzione, trasferisce nel registro DX i 16 bit del valore immediato 1111111111100111b (incorporato nel codice macchina dell'istruzione stessa).

Consideriamo l'istruzione:
mov edx, TEMPERATURA
Siccome gli operandi sono a 32 bit, l'assembler deve esprimere il valore -25 sotto forma di Imm32 in complemento a 2; otteniamo quindi:
Imm32 = 4294967296 - 25 = 4294967271 = 11111111111111111111111111100111b = FFFFFFE7h
Per indicare alla CPU che gli operandi sono a 32 bit, è necessario il prefisso 66h; l'assembler genera quindi il codice macchina:
01100110b 10111010b 11111111111111111111111111100111b = 66h BAh FFFFFFE7h
Quando la CPU incontra questa istruzione, trasferisce nel registro EDX i 32 bit del valore immediato 11111111111111111111111111100111b (incorporato nel codice macchina dell'istruzione stessa).

Se l'operando destinazione è di tipo Mem, l'aspetto più importante da tenere in considerazione è dato dal fatto che l'assembler deve essere in grado di determinare l'ampiezza in bit degli operandi; ovviamente, questa informazione deve essere fornita dal programmatore.
Il procedimento più semplice da seguire, consiste nel gestire l'operando di tipo Mem attraverso un nome simbolico; in riferimento alla Figura 16.1, consideriamo l'istruzione:
mov Variabile8, TEMPERATURA
In questo caso, l'assembler è in grado di capire facilmente che il trasferimento dati coinvolge due operandi a 8 bit; infatti, il nome Variabile8 è stato utilizzato per definire un dato di tipo BYTE (con la direttiva DB).

La situazione diventa, invece, ambigua nel caso dell'istruzione:
mov [si], TEMPERATURA
In questo caso, l'assembler genera un messaggio di errore per indicare che non è in grado di determinare l'ampiezza in bit degli operandi; infatti, TEMPERATURA è un generico valore numerico di ampiezza imprecisata, mentre [SI] indica il contenuto, di ampiezza imprecisata, di una locazione di memoria che si trova all'indirizzo logico DS:SI.
Come già sappiamo, per risolvere questo problema possiamo ricorrere agli address size operators; se, ad esempio, vogliamo effettuare un trasferimento dati a 16 bit, possiamo scrivere:
mov word ptr [si], TEMPERATURA
In questo modo, stiamo dicendo all'assembler che vogliamo copiare il valore numerico TEMPERATURA, a 16 bit, nella locazione di memoria da 16 bit che si trova all'indirizzo logico DS:SI. Nei precedenti capitoli, abbiamo visto che le componenti Seg e Offset di un indirizzo logico, vengono trattate come dati di tipo Imm16, per cui possono comparire solo nell'operando sorgente di una istruzione; in sostanza, istruzioni del tipo:
mov ax, seg Variabile8 ; ax = Seg(Variabile8)
oppure:
mov Variabile16, offset start ; Variabile16 = Offset(start)
non sono altro che trasferimenti di dati da Imm16 a Reg16/Mem16.

16.1.5 Indirizzamenti a 32 bit

Come è stato spiegato in precedenza, quando si programma in modalità reale è opportuno che si evitino gli indirizzamenti con componente Offset a 32 bit; nel seguito, ci limitiamo a presentare un semplice esempio che serve anche ad evidenziare un bug del MASM (già illustrato in un precedente capitolo).

Consideriamo un programma in formato EXE, dotato di un segmento di dati chiamato DATASEGM, un segmento di codice chiamato CODESEGM e un segmento di stack chiamato STACKSEGM; all'offset 0000h di DATASEGM, inseriamo la seguente definizione:
VarData32 dd 3C2AB1C8h
All'offset 0000h di STACKSEGM, inseriamo la seguente definizione:
VarStack32 dd 1CF8442Dh
Servendoci della libreria EXELIB, nell'ipotesi che DS=DATASEGM e SS=STACKSEGM, possiamo scrivere: Utilizzando NASM, questo esempio funziona perfettamente.

Analizziamo ora il codice macchina generato da NASM, per l'istruzione:
mov eax, [ecx+ebp]
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m e dal S.I.B. byte; nel nostro caso, l'istruzione è to register, per cui d=1. Grazie a EAX, l'assembler capisce che gli operandi sono a 32 bit, per cui w=1 (full size); otteniamo quindi:
Opcode = 10001011b = 8Bh
Il registro destinazione è EAX, per cui reg=000b; la sorgente è un effective address del tipo:
[ECX+(indice scalato)]
per cui mod=00b, r/m=100b. Otteniamo allora:
mod_reg_r/m = 00000100b = 04h
Il fattore di scala è 1 (nessuna moltiplicazione), il registro base è ECX, mentre il registro indice è EBP; otteniamo allora, scale=00b, index=101b, base=001b. Per il campo S.I.B. (scale_index_base) si ha quindi:
S.I.B. = 00101001b = 29h
Gli operandi sono a 32 bit, per cui l'assembler inserisce il prefisso 66h; gli offset sono a 32 bit, per cui l'assembler inserisce il prefisso 67h.
In assenza di diverse indicazioni da parte del programmatore, la base ECX viene associata automaticamente a DS, per cui non è necessario alcun segment override; l'assembler genera quindi il codice macchina:
01100110b 01100111b 10001011b 00000100b 00101001b = 66h 67h 8Bh 04h 29h
Quando la CPU incontra questa istruzione, accede in memoria all'indirizzo DS:(ECX+EBP), legge i 32 bit 3C2AB1C8h e li trasferisce nei 32 bit del registro EAX.

Analizziamo il codice macchina generato da NASM, per l'istruzione:
mov eax, [ebp+ecx]
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dall'Opcode 100010dw, seguito dal campo mod_reg_r/m e dal S.I.B. byte; nel nostro caso, l'istruzione è to register, per cui d=1. Grazie a EAX, l'assembler capisce che gli operandi sono a 32 bit, per cui w=1 (full size); otteniamo quindi:
Opcode = 10001011b = 8Bh
Il registro destinazione è EAX, per cui reg=000b; la sorgente è un effective address del tipo:
[EBP+(indice scalato)+Disp8]
per cui mod=01b, r/m=100b. Infatti (Capitolo 11), in presenza del registro base EBP, viene aggiunto un Disp8=00h; otteniamo allora:
mod_reg_r/m = 01000100b = 44h
Il fattore di scala è 1 (nessuna moltiplicazione), il registro base è EBP, mentre il registro indice è ECX; otteniamo allora, scale=00b, index=001b, base=101b. Per il campo S.I.B. (scale_index_base) si ha quindi:
S.I.B. = 00001101b = 0Dh
Il Disp8 è:
Disp8 = 00000000b = 00h
Gli operandi sono a 32 bit, per cui l'assembler inserisce il prefisso 66h; gli offset sono a 32 bit, per cui l'assembler inserisce il prefisso 67h.
In assenza di diverse indicazioni da parte del programmatore, la base EBP viene associata automaticamente a SS, per cui non è necessario alcun segment override; l'assembler genera quindi il codice macchina:
01100110b 01100111b 10001011b 01000100b 00001101b 00000000b = 66h 67h 8Bh 44h 0Dh 00h
Quando la CPU incontra questa istruzione, accede in memoria all'indirizzo SS:(EBP+ECX+00h), legge i 32 bit 1CF8442Dh e li trasferisce nei 32 bit del registro EAX.

Se proviamo ad utilizzare MASM, ci accorgiamo che l'istruzione:
mov eax, [ecx+ebp]
visualizza VarStack32; analogamente, l'istruzione:
mov eax, [ebp+ecx]
visualizza VarData32!

Consultando il listing file, scopriamo che MASM, per la prima istruzione ha generato il codice macchina:
66h 67h 8Bh 44h 0Dh 00h
mentre per la seconda istruzione ha generato il codice macchina:
66h 67h 8Bh 04h 29h
Come si può notare, questi due codici macchina appaiono invertiti rispetto a quelli generati dal NASM!

È stato già spiegato che questo bug di MASM, si verifica solo in modalità reale e in assenza di un fattore di scala esplicito; abbiamo anche visto che, per evitare questo bug, possiamo servirci del segment override, scrivendo:
mov eax, ds:[ecx+ebp]
e:
mov eax, ss:[ebp+ecx]
Le considerazioni appena esposte, inducono a maggior ragione, ad evitare l'uso degli indirizzamenti a 32 bit in modalità reale.

16.1.6 Effetti provocati da MOV sugli operandi e sui flags

L'esecuzione dell'istruzione MOV tra SRC e DEST, modifica il contenuto del solo operando DEST; infatti, il vecchio contenuto di DEST viene sovrascritto dal contenuto di SRC. Il contenuto dell'operando SRC rimane inalterato; bisogna però prestare particolare attenzione ad istruzioni del tipo:
mov si, [si]
oppure:
mov bh, ss:[bx+di+09h]
Per capire bene questa situazione, consideriamo l'esempio di Figura 16.2; carichiamo l'offset (0008h) di Variabile16 in SI e scriviamo:
mov si, cs:[si]
In questo esempio, l'operando DEST è SI, mentre l'operando SRC è la locazione di memoria che si trova all'indirizzo logico 0CA5h:0008h e che contiene il valore 28D6h; in seguito al trasferimento dati, otteniamo SI=28D6h. Il valore 28D6h che si trova in memoria all'indirizzo 0CA5h:0008h, viene ovviamente preservato; in relazione, invece, al registro SI, succede che: In sostanza, dopo il trasferimento dati, la coppia CS:SI contiene l'indirizzo logico 0CA5h:28D6h; questa coppia quindi, non punta più alla locazione di memoria in cui si trova Variabile16.

Il fatto che sia permesso un trasferimento dati da Reg a Reg, ci permette di scrivere anche istruzioni del tipo:
mov dx, dx
Come si può facilmente constatare, l'esecuzione di una tale istruzione non provoca alcuna modifica del contenuto di DX; proprio per questo motivo, alcuni assembler utilizzano spesso questo tipo di istruzioni (in alternativa a NOP), per inserire eventuali buchi di memoria all'interno di un segmento di codice. In tal caso, lo scopo della precedente istruzione è solo quello di occupare 2 byte di memoria (cioè, i due byte 8Bh, D2h del relativo codice macchina).

L'esecuzione dell'istruzione MOV, non modifica alcuno dei campi presenti nel Flags Register; per chi programma in Assembly, è fondamentale tenere sempre conto di questo aspetto. Ricordiamo, infatti, che in Assembly (e in qualunque altro linguaggio di programmazione), il comportamento dei programmi dipende proprio dal contenuto del Flags Register; pertanto, si consiglia vivamente di consultare sempre le tabelle per tenere conto degli effetti prodotti su questo registro dall'esecuzione di una qualsiasi istruzione.

16.2 Le istruzioni MOVZX e MOVSX

Molto spesso, quando si scrive un programma, si presenta la necessità di modificare l'ampiezza in bit di un numero intero; come si può facilmente immaginare, si tratta di un aspetto molto delicato, che deve essere gestito con la massima cautela.
La Figura 16.4 mostra un programma di esempio scritto in linguaggio C. Come si può notare, il C permette di effettuare assegnamenti tra variabili di tipo differente; possiamo scrivere, ad esempio:
var16 = var8
In questo caso, il contenuto (-128) di var8 viene copiato in var16; la conversione non presenta alcun problema, in quanto stiamo passando dagli interi con segno a 8 bit agli interi con segno a 16 bit. Il contenuto di var8 si trova memorizzato nella forma:
var8 = 256 - 128 = 128 = 10000000b
Il compilatore C effettua l'estensione del bit di segno e ottiene:
var16 = 1111111110000000b
In questo caso, il passaggio da 8 a 16 bit è stato ottenuto aggiungendo otto 1 alla sinistra di 10000000b; il risultato che ne scaturisce, è ancora la rappresentazione di -128 a 16 bit in complemento a 2. Infatti:
-128 = 65536 - 128 = 65408 = 1111111110000000b
Il discorso però cambia se scriviamo:
var8 = var16
In questo caso, il contenuto (-129) di var16 viene copiato in var8; la conversione comporta un troncamento di cifre significative, in quanto stiamo passando dagli interi con segno a 16 bit agli interi con segno a 8 bit. Il contenuto di var16 si trova memorizzato nella forma:
var16 = 65536 - 129 = 65407 = 1111111101111111b
Il compilatore C effettua il troncamento degli 8 bit più significativi di questo valore e ottiene:
var8 = 01111111b
Nell'insieme dei numeri interi con segno a 8 bit in complemento a due, il valore binario 01111111b rappresenta il numero positivo +127; a causa quindi del troncamento di cifre significative, siamo passati da -129 a +127!

Proviamo ora a scrivere:
num1 = num2
In questo caso, stiamo assegnando il valore intero senza segno 40000, al dato num2 che è stato dichiarato come intero con segno; l'ampiezza di entrambe le variabili è di 16 bit, ma il risultato che si ottiene è sbagliato. Infatti, se proviamo a visualizzare il contenuto di num1, otteniamo il numero negativo -25536!
Il perché di questo errore è abbastanza evidente; per i numeri interi con segno a 16 bit in complemento a 2, il valore 40000 non è altro che la rappresentazione di -25536. Infatti:
-25536 = 65536 - 25536 = 40000 = 1001110001000000b
Gli esempi appena presentati, dimostrano la pericolosità delle conversioni tra dati di tipo differente; il C è un linguaggio destinato agli esperti, per cui assume che il programmatore sappia ciò che sta facendo. Anche il linguaggio Assembly si basa su questa stessa filosofia; i linguaggi destinati, invece, ai principianti, proibiscono le conversioni tra dati di tipo differente.

Dalle considerazioni appena esposte, risulta evidente che le uniche conversioni sicure sono quelle che coinvolgono dati dello stesso tipo e che comportano un aumento della ampiezza in bit del valore da convertire; in caso contrario, è necessario accertarsi che la conversione non provochi un cambiamento di segno o una perdita di cifre significative.
Tutte le CPU della famiglia 80x86, forniscono una serie di istruzioni che permettono di estendere, in modo sicuro, l'ampiezza in bit di un numero intero con segno; queste istruzioni vengono esaminate nel prossimo capitolo, in quanto fanno parte della categoria delle istruzioni aritmetiche.
In questo capitolo, invece, vengono prese in considerazione due nuove istruzioni, disponibili solo per le CPU 80386 e superiori, che permettono di eseguire trasferimenti di dati verso un operando DEST avente una ampiezza in bit maggiore dell'operando SRC; queste due istruzioni, sono rappresentate dai mnemonici MOVZX (per gli interi senza segno) e MOVSX (per gli interi con segno).

16.2.1 L'istruzione MOVZX

Con il mnemonico MOVZX si indica l'istruzione MOVe data with Zero eXtend (trasferimento dati con estensione di zeri); quando la CPU incontra questa istruzione, legge (bit per bit) il contenuto dell'operando SRC e lo copia nell'operando DEST, di ampiezza maggiore, aggiungendo un opportuno numero di zeri alla sinistra del dato copiato.
Il contenuto di SRC quindi, viene trattato sempre come un numero intero senza segno, anche se il suo bit più significativo vale 1; il risultato salvato in DEST è la rappresentazione ad ampiezza maggiore, del numero intero senza segno contenuto in SRC.
L'operando DEST deve avere, necessariamente, una ampiezza in bit maggiore di quella dell'operando SRC; le uniche combinazioni lecite tra SRC e DEST, sono le seguenti: Come si può notare, l'operando DEST deve essere, esclusivamente, di tipo Reg; inoltre, è proibito l'uso di operandi di tipo SegReg o Imm.

In generale, le nuove istruzioni fornite dalle CPU 80386 e superiori, hanno un campo Opcode formato da 2 byte; infatti, il campo Opcode da 1 byte utilizzato con le precedenti CPU non è sufficiente per contenere l'intero set di istruzioni delle CPU 80386 e superiori.
Nel caso dell'istruzione MOVZX, l'Opcode è formato dai 2 byte 00001111 e 1011011w; w=0 indica che l'operando SRC è a 8 bit, mentre w=1 indica che l'operando SRC è a 16 bit.

In riferimento all'esempio di Figura 16.1, con Variabile8=76h (118d), possiamo scrivere l'istruzione:
movzx bx, Variabile8
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dal campo Opcode, seguito dal campo mod_reg_r/m e da un Disp16; nel nostro caso, l'operando SRC è a 8 bit, per cui w=0; otteniamo quindi:
Opcode = 00001111b 10110110b = 0Fh B6h
Il registro destinazione è BX, per cui reg=011b; la sorgente è un Disp16, per cui mod=00b, r/m=110b. Otteniamo allora:
mod_reg_r/m = 00011110b = 1Eh
Il Disp16 è:
Disp16 = 0000000000001000b = 0008h
L'operando DEST è a 16 bit, per cui non è necessario il prefisso 66h; l'assembler genera quindi il codice macchina:
00001111b 10110110b 00011110b 0000000000001000b = 0Fh B6h 1Eh 0008h
Quando la CPU incontra questa istruzione: Alla fine, si ottiene in BX la rappresentazione a 16 bit del numero intero senza segno 118d; infatti:
118d = 0000000001110110b
Se DS:(BP+SI+0002h) punta a Variabile8=D6h (214d), possiamo scrivere l'istruzione:
movzx ebx, byte ptr ds:[bp+si+0002h]
Il segment override è necessario, in modo che la base BP venga associata a DS; l'operatore BYTE PTR è necessario, per poter specificare che l'operando SRC è a 8 bit.
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dal campo Opcode, seguito dal campo mod_reg_r/m e da un Disp8; nel nostro caso, l'operando SRC è a 8 bit, per cui w=0; otteniamo quindi:
Opcode = 00001111b 10110110b = 0Fh B6h
Il registro destinazione è EBX, per cui reg=011b; la sorgente è un effective address del tipo [BP+SI+Disp8], per cui mod=01b, r/m=010b. Otteniamo allora:
mod_reg_r/m = 01011010b = 5Ah
Il Disp8 è:
Disp8 = 00000010b = 02h
La base BP non viene associata automaticamente a DS, per cui è necessario il segment override 3Eh relativo allo stesso DS; l'operando DEST è a 32 bit, per cui è necessario il prefisso 66h. L'assembler genera quindi il codice macchina:
01100110b 00111110b 00001111b 10110110b 01011010b 00000010b = 66h 3Eh 0Fh B6h 5Ah 02h
Quando la CPU incontra questa istruzione: Alla fine, si ottiene in EBX la rappresentazione a 32 bit del numero intero senza segno 214d; infatti:
214d = 00000000000000000000000011010110b

16.2.2 L'istruzione MOVSX

Con il mnemonico MOVSX si indica l'istruzione MOVe data with Sign eXtension (trasferimento dati con estensione del bit di segno); quando la CPU incontra questa istruzione, legge (bit per bit) il contenuto dell'operando SRC e lo copia nell'operando DEST, di ampiezza maggiore, estendendo il bit di segno del dato copiato.
Il contenuto di SRC quindi, viene trattato sempre come un numero intero con segno; il risultato salvato in DEST, è la rappresentazione ad ampiezza maggiore del numero intero con segno contenuto in SRC.
L'operando DEST deve avere, necessariamente, una ampiezza in bit maggiore di quella dell'operando SRC; le uniche combinazioni lecite tra SRC e DEST, sono le seguenti: In relazione alle caratteristiche degli operandi SRC e DEST, valgono le stesse considerazioni già svolte per MOVZX.

Per l'istruzione MOVSX, l'Opcode è formato dai 2 byte 00001111 e 1011111w; w=0 indica che l'operando SRC è a 8 bit, mentre w=1 indica che l'operando SRC è a 16 bit.

In riferimento all'esempio di Figura 16.2, con Variabile16=28D6h (+10454d), definita in CODESEGM, e con una direttiva ASSUME che associa CODESEGM a CS, possiamo scrivere l'istruzione:
movsx ebx, Variabile16
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dal campo Opcode, seguito dal campo mod_reg_r/m e da un Disp16; nel nostro caso, l'operando SRC è a 16 bit, per cui w=1; otteniamo quindi:
Opcode = 00001111b 10111111b = 0Fh BFh
Il registro destinazione è EBX, per cui reg=011b; la sorgente è un Disp16, per cui mod=00b, r/m=110b. Otteniamo allora:
mod_reg_r/m = 00011110b = 1Eh
Il Disp16 è:
Disp16 = 0000000000001000b = 0008h
Il Disp16 non viene associato automaticamente a CS, per cui è necessario il segment override 2Eh relativo allo stesso CS; l'operando DEST è a 32 bit, per cui è necessario il prefisso 66h. L'assembler genera quindi il codice macchina:
01100110b 00101110b 00001111b 10111111b 00011110b 0000000000001000b = 66h 2Eh 0Fh BFh 1Eh 0008h
Quando la CPU incontra questa istruzione: Alla fine, si ottiene in EBX la rappresentazione a 32 bit in complemento a 2 del numero intero con segno +10454d; infatti:
+10454d = 00000000000000000010100011010110b
Se CS:(BX+DI+0002h) punta a Variabile16=88D6h (-30506d), possiamo scrivere l'istruzione:
movsx ebx, word ptr cs:[bx+di+0002h]
Il segment override è necessario, in modo che la base BX venga associata a CS; l'operatore WORD PTR è necessario per poter specificare che l'operando SRC è a 16 bit.
Dalle tabelle si ricava che il codice macchina di questa istruzione è formato dal campo Opcode, seguito dal campo mod_reg_r/m e da un Disp8; nel nostro caso, l'operando SRC è a 16 bit, per cui w=1; otteniamo quindi:
Opcode = 00001111b 10111111b = 0Fh BFh
Il registro destinazione è EBX, per cui reg=011b; la sorgente è un effective address del tipo [BX+DI+Disp8], per cui mod=01b, r/m=001b. Otteniamo allora:
mod_reg_r/m = 01011001b = 59h
Il Disp8 è:
Disp8 = 00000010b = 02h
La base BX non viene associata automaticamente a CS, per cui è necessario il segment override 2Eh relativo allo stesso CS; l'operando DEST è a 32 bit, per cui è necessario il prefisso 66h. L'assembler genera quindi il codice macchina:
01100110b 00101110b 00001111b 10111111b 01011001b 00000010b = 66h 2Eh 0Fh BFh 59h 02h
Quando la CPU incontra questa istruzione: Alla fine, si ottiene in EBX la rappresentazione a 32 bit in complemento a 2 del numero intero con segno -30506d; infatti:
-30506d = 4294967296 - 30506 = 4294936790 = 11111111111111111000100011010110b

16.2.3 Inserimento diretto di codici macchina in un programma Assembly

Ogni volta che compare sul mercato una nuova CPU della famiglia 80x86, viene ulteriormente arricchito il set di istruzioni disponibile con le CPU precedenti; abbiamo appena visto che con la 80386, vengono rese disponibili anche le due nuove istruzioni MOVZX e MOVSX.
Può capitare allora di avere a disposizione un assembler che non supporta le nuove istruzioni appartenenti al set della CPU installata sul proprio computer; in un caso del genere, è possibile risolvere il problema senza la necessità di procurarsi un assembler più aggiornato.
La tecnica da utilizzare consiste nell'inserire, direttamente nei propri programmi, il codice macchina delle istruzioni non supportate dall'assembler; come esempio pratico, supponiamo di voler scrivere le seguenti istruzioni: L'assembler in nostro possesso non supporta però l'istruzione MOVSX; servendoci allora della documentazione ufficiale della nostra CPU, possiamo rilevare che l'istruzione dell'esempio ha un codice macchina formato dal campo Opcode e dal campo mod_reg_r/m. L'operando SRC è a 16 bit, per cui w=1; otteniamo allora:
Opcode = 00001111b 10111111b = 0Fh BFh
Il registro destinazione è ECX, per cui reg=001b; il registro sorgente è BX, per cui mod=11b, r/m=011b. Otteniamo allora:
mod_reg_r/m = 11001011b = CBh
L'operando DEST è a 32 bit, per cui è necessario il prefisso 66h; in definitiva, il codice macchina che si ottiene è:
01100110b 00001111b 10111111b 11001011b = 66h 0Fh BFh CBh
Dopo aver ricavato queste informazioni, possiamo riscrivere le istruzioni del precedente esempio, in questo modo: Quando l'assembler incontra la direttiva DB, crea in quel preciso punto del blocco codice, una sequenza di 4 locazioni consecutive e contigue, ciascuna delle quali occupa 1 byte; in queste 4 locazioni l'assembler inserisce i valori 66h, 0Fh, BFh, CBh.
Quando la CPU incontra questo codice macchina, copia in ECX il numero intero con segno contenuto in BX; naturalmente, affinché questo esempio possa funzionare, è necessario disporre almeno di una CPU 80386.

Quando si applica questa tecnica, bisogna prestare particolare attenzione ad eventuali operandi di tipo Disp; infatti, abbiamo visto che l'assembler marca come relocatable questo tipo di operandi, in modo che il linker, se necessario, possa procedere alla loro rilocazione.
Consideriamo un precedente esempio, relativo all'istruzione:
movsx ebx, Variabile16
Abbiamo visto che il codice macchina che l'assembler genera per questa istruzione, è:
66h 2Eh 0Fh BFh 1Eh 0008h
Anche se sappiamo che Variabile16 si trova all'offset 0008h del blocco CODESEGM, non possiamo utilizzare questo valore esplicito (che verrebbe trattato dall'assembler come un semplice Imm16); dobbiamo fare in modo che sia l'assembler ad inserire l'offset di Variabile16, marcandolo anche come relocatable. Per ottenere questo risultato, all'interno del blocco CODESEGM possiamo inserire il seguente codice macchina: In questo modo, si ottiene il codice macchina completo, che comprende anche l'offset 0008h, marcato come relocatable; infatti, in presenza della direttiva:
dw offset Variabile16
l'assembler crea una locazione di memoria da 2 byte e carica in tale locazione l'offset di Variabile16, marcato come relocatable.

16.2.4 Effetti provocati da MOVZX e MOVSX sugli operandi e sui flags

L'esecuzione delle istruzioni MOVZX e MOVSX, tra SRC e DEST, modifica il contenuto del solo operando DEST; infatti, il vecchio contenuto di DEST viene sovrascritto dal contenuto di SRC. Il contenuto dell'operando SRC rimane inalterato; come al solito, bisogna prestare particolare attenzione a quelle istruzioni che prevedono lo stesso registro, sia come SRC, sia come DEST.

Supponendo di avere CX=-12000 (1101000100100000b), consideriamo l'istruzione:
movsx ecx, cx
In questo caso, l'operando DEST è ECX, mentre l'operando SRC è CX; subito dopo l'esecuzione di questa istruzione, otteniamo:
ECX = 11111111111111111101000100100000b
e, di conseguenza:
CX = 1101000100100000b
Il contenuto di CX rimane chiaramente inalterato; infatti, l'istruzione dell'esempio modifica solo i 16 bit più significativi di ECX.

Supponendo di avere BX=0008h e [CS:BX]=18CBh, consideriamo la seguente istruzione:
movzx ebx, word ptr cs:[bx]
In questo caso, l'operando DEST è EBX, mentre l'operando SRC è la locazione di memoria puntata da CS:BX e contenente il valore 18CBh.
L'esecuzione di questa istruzione, preserva il contenuto 18CBh della locazione di memoria che si trova all'indirizzo CS:0008h; per quanto riguarda, invece, l'operando DEST, otteniamo EBX=000018CBh. Ne consegue che la coppia CS:BX, non punta più a CS:0008h, bensì a CS:18CBh!

L'esecuzione delle istruzioni MOVZX e MOVSX, non modifica alcuno dei campi presenti nel Flags Register.

16.3 Le istruzioni PUSH e POP

Con le CPU della famiglia 80x86, il metodo predefinito per la gestione dello stack consiste nell'uso delle istruzioni PUSH e POP; queste due istruzioni, presuppongono che nel momento in cui inizia la fase di esecuzione di un programma, la coppia SS:SP stia puntando alla cima dello stack (TOS).
Abbiamo visto che se definiamo un segmento di programma con attributo di combinazione STACK, allora la coppia SS:SP viene inizializzata automaticamente dal SO; in caso contrario, il compito di inizializzare la coppia SS:SP spetta a noi.
È importante ricordare che quando si lavora in modalità reale con le CPU a 32 bit, le componenti Offset degli indirizzi logici non devono superare il valore massimo 0000FFFFh; quando una CPU 80386 o superiore viene inizializzata in modalità reale, i 16 bit più significativi del registro ESP vengono posti automaticamente a 0000h.

In generale, l'ampiezza in bit degli operandi di PUSH e POP rispecchia fedelmente l'architettura della CPU; in questo modo, si semplifica notevolmente il lavoro del programmatore, che deve cercare di allineare correttamente i dati nello stack.
Con le CPU a 16 bit, PUSH e POP lavorano esclusivamente con operandi di tipo WORD; è proibito l'uso di operandi di tipo BYTE.
Con le CPU a 32 bit, PUSH e POP lavorano esclusivamente con operandi di tipo WORD e DWORD; anche in questo caso, è proibito l'uso di operandi di tipo BYTE.

16.3.1 L'istruzione PUSH

Con il mnemonico PUSH si indica l'istruzione PUSH Word or Doubleword onto the stack (inserimento di un operando di tipo WORD o DWORD sulla cima dello stack). Questa istruzione richiede, esplicitamente, il solo operando SRC; infatti, l'operando DEST è rappresentato, implicitamente, dalla locazione di memoria puntata da SS:SP.
Quando la CPU incontra una istruzione PUSH con operando di tipo WORD, esegue le seguenti operazioni: Se l'operando è di tipo DWORD, il registro SP viene decrementato di 4 byte.

Come si può notare, l'istruzione PUSH esegue un vero e proprio trasferimento dati; nel caso, ad esempio, dell'operando sorgente AX, l'istruzione PUSH (dopo il decremento di SP) equivale a:
mov ss:[sp], ax
Naturalmente, questa istruzione ha un significato puramente simbolico; infatti, sappiamo che in modalità reale è proibito dereferenziare il registro SP.

Per l'istruzione PUSH, sono permesse solo le seguenti forme: L'uso di un operando di tipo Imm è permesso solo sulle CPU 80186 e superiori; questo aspetto verrà trattato più avanti.

Vediamo una serie di esempi, nei quali supponiamo di partire con SP=0400h; questa situazione è illustrata in Figura 16.5, dove il valore iniziale di SP è rappresentato dal simbolo SP(0). In riferimento all'esempio di Figura 16.2, con SP=0400h, Variabile16=28D6h (definita in CODESEGM) e con una direttiva ASSUME che associa CODESEGM a CS, possiamo scrivere l'istruzione:
push word Variabile16
Quando la CPU incontra questa istruzione, esegue le seguenti operazioni: In Figura 16.5, la nuova posizione di SP è rappresentata dal simbolo SP(1).

In riferimento all'esempio di Figura 16.3, con SP=03FEh e con SS:BX che punta a Variabile32=3ABF28D6h (definita in STACKSEGM), possiamo scrivere l'istruzione:
push dword ss:[bx]
Il segment override è necessario, in modo che BX venga associato a SS; l'operatore DWORD è necessario, per specificare che l'operando SRC è a 32 bit. Quando la CPU incontra questa istruzione, esegue le seguenti operazioni: In Figura 16.5, la nuova posizione di SP è rappresentata dal simbolo SP(2).

In riferimento all'esempio di Figura 16.1, con SP=03FAh, Variabile8=D6h (definita in DATASEGM) e con una direttiva ASSUME che associa DATASEGM a DS, possiamo scrivere l'istruzione:
push Variabile8
In questo caso, l'assembler genera un messaggio di errore, per indicare che è proibito l'uso di un operando a 8 bit; come dobbiamo comportarci in un caso del genere?
La soluzione consiste nel salvare Variabile8 negli 8 bit meno significativi di un registro generale a 16 bit (preferibilmente AX); possiamo scrivere allora: In questo modo, ci riconduciamo al caso di un operando SRC a 16 bit; più avanti vedremo come si estrae dallo stack un operando a 8 bit. In presenza di una istruzione PUSH con operando AX, la CPU compie le seguenti operazioni: In Figura 16.5, la nuova posizione di SP è rappresentata dal simbolo SP(3); osserviamo, inoltre, che all'indirizzo SS:03F9h sono stati salvati gli 8 bit più significativi di AX (in questo caso, il valore assunto da questi 8 bit non ha alcuna importanza).

16.3.2 Istruzione PUSH con operando di tipo Imm

Avendo a disposizione una CPU 80186 o superiore, possiamo utilizzare PUSH anche con operandi di tipo Imm; consultando i manuali della CPU, ci accorgiamo che in questo caso il codice macchina è formato dall'Opcode 011010s0, seguito da un Imm. Il significato del bit indicato con s (sign bit) è molto importante; infatti, questo bit indica alla CPU come deve essere trattato il valore Imm da inserire nello stack. Se s=0, la CPU non apporta alcuna modifica all'Imm; se, invece, s=1, la CPU effettua l'estensione del bit di segno dell'Imm.

Consideriamo la seguente dichiarazione:
IMMEDIATO = +25 ; = 19h = 00011001b
A questo punto, possiamo scrivere:
push IMMEDIATO
Quando l'assembler incontra questa istruzione, genera il codice macchina:
01101010b 00011001b = 6Ah 19h
Quando la CPU incontra questo codice macchina, vede che s=1, per cui deve estendere a 16 bit il valore 19h, attraverso il bit di segno; il valore che viene inserito nello stack è quindi:
0000000000011001b = 0019h
Consideriamo la seguente dichiarazione:
IMMEDIATO = -25 ; = E7h = 11100111b
A questo punto, possiamo scrivere:
push IMMEDIATO
Quando l'assembler incontra questa istruzione, genera il codice macchina:
01101010b 11100111b = 6Ah E7h
Quando la CPU incontra questo codice macchina, vede che s=1, per cui deve estendere a 16 bit il valore E7h, attraverso il bit di segno; il valore che viene inserito nello stack è quindi:
1111111111100111b = FFE7h
Consideriamo la seguente dichiarazione:
IMMEDIATO = +40950 ; = 9FF6h = 1001111111110110b
A questo punto, possiamo scrivere:
push IMMEDIATO
Quando l'assembler incontra questa istruzione, genera il codice macchina:
01101000b 1001111111110110b = 68h 9FF6h
Quando la CPU incontra questo codice macchina, vede che s=0, per cui non deve modificare il valore 9FF6h; il valore che viene inserito nello stack è quindi:
1001111111110110b = 9FF6h
Consideriamo la seguente dichiarazione:
IMMEDIATO = -12850 ; = CDCEh = 1100110111001110b
A questo punto, possiamo scrivere:
push IMMEDIATO
Quando l'assembler incontra questa istruzione, genera il codice macchina:
01101000b 1100110111001110b = 68h CDCEh
Quando la CPU incontra questo codice macchina, vede che s=0, per cui non deve modificare il valore CDCEh; il valore che viene inserito nello stack è quindi:
1100110111001110b = CDCEh
Consideriamo la seguente dichiarazione:
IMMEDIATO = -25 ; = E7h = 11100111b
A questo punto, possiamo scrivere:
push dword ptr IMMEDIATO
Quando l'assembler incontra questa istruzione, genera il codice macchina:
01100110b 01101010b 11100111b = 66h 6Ah E7h
Quando la CPU incontra questo codice macchina, vede che s=1, per cui deve estendere a 32 bit (prefisso 66h) il valore E7h, attraverso il bit di segno; il valore che viene inserito nello stack è quindi:
11111111111111111111111111100111b = FFFFFFE7h
Consideriamo, infine, la seguente dichiarazione:
IMMEDIATO = -300000 ; = FFFB6C20h = 11111111111110110110110000100000b
A questo punto, possiamo scrivere:
push IMMEDIATO
Quando l'assembler incontra questa istruzione, genera il codice macchina:
01100110b 01101000b 11111111111110110110110000100000b = 66h 68h FFFB6C20h
Quando la CPU incontra questo codice macchina, vede che s=0, per cui non deve modificare il valore FFFB6C20h; il valore che viene inserito nello stack è quindi:
11111111111110110110110000100000b = FFFB6C20h

16.3.3 L'istruzione POP

Con il mnemonico POP si indica l'istruzione POP a value from the stack (estrazione di un valore dalla cima dello stack). Questa istruzione richiede, esplicitamente, il solo operando DEST; infatti, l'operando SRC è rappresentato, implicitamente, dalla locazione di memoria puntata da SS:SP.
Quando la CPU incontra una istruzione POP con operando di tipo WORD, esegue le seguenti operazioni: Se l'operando è di tipo DWORD, il registro SP viene incrementato di 4 byte.

Come si può notare, anche l'istruzione POP esegue un vero e proprio trasferimento dati; nel caso, ad esempio, dell'operando destinazione AX, l'istruzione POP (prima dell'incremento di SP) equivale a:
mov ax, ss:[sp]
Come al solito, questa istruzione ha un significato puramente simbolico; infatti, sappiamo che in modalità reale è proibito dereferenziare il registro SP.

Per l'istruzione POP, sono permesse solo le seguenti forme: L'istruzione POP con operando di tipo Imm non ha alcun senso; infatti, non è possibile estrarre un valore dallo stack e metterlo in un Imm.
Nel caso dell'istruzione POP con operando di tipo SegReg, è proibito scrivere:
POP CS
Abbiamo già visto, infatti, che solo la CPU può modificare il contenuto di CS; in un prossimo capitolo, vedremo che il caricamento in CS di una WORD estratta dallo stack viene svolto dalla istruzione RET (ritorno da una procedura).

Prima di eseguire l'istruzione:
POP SS
la CPU disabilita automaticamente, sia le interruzioni mascherabili, sia le NMI; tutte le interruzioni vengono automaticamente ripristinate subito dopo l'esecuzione dell'istruzione successiva (che spesso è un'altra POP con operando SP o ESP).

Ripetiamo ora in senso inverso, i primi tre esempi presentati per l'istruzione PUSH; partiamo quindi con il registro SP che contiene il valore 03F8h. Questa situazione è illustrata in Figura 16.5, dove il valore iniziale di SP è rappresentato dal simbolo SP(3); possiamo dire quindi che in questo momento, la cima dello stack si trova all'indirizzo SS:03F8h.

È fondamentale ricordare che tutte le estrazioni dallo stack, devono essere effettuate in senso inverso rispetto agli inserimenti; alla fine, lo stack deve risultare perfettamente bilanciato (SP=0400h).
Prima di tutto, procediamo con l'estrazione del valore D6h che si trova all'indirizzo SS:03F8h; in riferimento alla Figura 16.5, con SP=03F8h, possiamo scrivere l'istruzione:
pop ax
Quando la CPU incontra questa istruzione, esegue le seguenti operazioni: In Figura 16.5, la nuova posizione di SP è rappresentata dal simbolo SP(2); a questo punto, se vogliamo rimettere D6h in Variabile8, non dobbiamo fare altro che scrivere:
mov Variabile8, al
Solamente gli 8 bit meno significativi di AX contengono un valore valido (in questo caso, D6h); gli 8 bit più significativi di AX, contengono un valore casuale.

In riferimento alla Figura 16.5, con SP=03FAh e con una direttiva ASSUME che associa STACKSEGM a SS, possiamo scrivere l'istruzione:
pop Variabile32
Quando la CPU incontra questa istruzione, esegue le seguenti operazioni: In Figura 16.5, la nuova posizione di SP è rappresentata dal simbolo SP(1).

In riferimento alla Figura 16.5, con SP=03FEh e con CS:(BX+DI+0002h) che punta a Variabile16, possiamo scrivere l'istruzione:
pop word ptr cs:[bx+di+0002h]
Il segment override è necessario, in modo che la base BX venga associata a CS; l'operatore WORD PTR è necessario, per specificare che l'operando DEST è a 16 bit. Quando la CPU incontra questa istruzione, esegue le seguenti operazioni: In Figura 16.5, la nuova posizione di SP è rappresentata dal simbolo SP(0).

È necessario ribadire che l'aspetto più importante nella gestione dello stack riguarda il perfetto bilanciamento tra PUSH e POP; al termine di un programma, il numero di byte estratti con le istruzioni POP, deve essere pari al numero di byte inseriti con le istruzioni PUSH (questo discorso vale anche per una qualsiasi procedura, che può essere vista come un mini-programma).
Ricordiamo, inoltre, che se abbiamo scritto, ad esempio:
push Variabile32
non siamo obbligati poi a scrivere:
pop Variabile32
Possiamo anche scrivere:
pop ebx
Oppure: L'importante è che l'inserimento dei 32 bit di Variabile32, venga poi bilanciato dalla estrazione degli stessi 32 bit (che possiamo mettere dove ci pare).
Tutto ciò ci permette di utilizzare le istruzioni PUSH e POP, per eseguire rapidamente diverse operazioni; se, ad esempio, vogliamo copiare DS in ES senza utilizzare un registro generale, possiamo scrivere: Questo tipo di istruzioni compaiono molto frequentemente nei programmi Assembly; evidentemente, chi programma in questo modo non sa che (soprattutto con le vecchie CPU) si ottiene un notevole aumento della velocità di esecuzione con le istruzioni: Un'altra operazione che possiamo eseguire velocemente con PUSH e POP, consiste nel copiare due WORD in una DWORD; se, ad esempio, vogliamo copiare il contenuto di BX nei 16 bit più significativi di EAX e il contenuto di CX nei 16 bit meno significativi di EAX, possiamo scrivere: È importante capire bene come vanno a disporsi in EAX i contenuti di BX e CX; a tale proposito, conviene sempre disegnare uno schema dello stack, come quello mostrato in Figura 16.5.

16.3.4 Effetti provocati da PUSH e POP sugli operandi e sui flags

L'esecuzione delle istruzioni PUSH e POP, modifica il contenuto del solo operando DEST; infatti, il vecchio contenuto di DEST viene sovrascritto dal contenuto di SRC. Il contenuto dell'operando SRC rimane inalterato; in relazione, però, alla istruzione PUSH, si presenta un caso molto delicato, rappresentato da:
push sp
o:
push esp
Il contenuto di SP/ESP che viene salvato nello stack, è quello precedente o quello successivo al decremento dello stesso SP/ESP?
La risposta a questa domanda, varia da CPU a CPU; per analizzare i diversi casi, supponiamo di avere SP=0400h.
La CPU 8086, in presenza della precedente istruzione, esegue le seguenti operazioni: I progettisti delle CPU, si sono resi conto che questa situazione non è del tutto corretta; infatti, appare più logico il salvataggio nello stack del valore di SP che precede il decremento. A partire quindi dalla 80286, in presenza della precedente istruzione la CPU esegue le seguenti operazioni: In sostanza, indicando con Temp16 un registro interno a 16 bit della CPU, possiamo simulare il procedimento appena descritto, con le seguenti pseudo-istruzioni: Il problema appena descritto in relazione alla istruzione PUSH, si presenta anche per l'istruzione:
pop sp
o:
pop esp
Il valore caricato in SP/ESP, è quello precedente o quello successivo all'incremento dello stesso SP/ESP?
Fortunatamente, in questo caso il problema è stato subito risolto per tutti i modelli di CPU della famiglia 80x86; per analizzare questo caso, supponiamo di avere SP=03FEh, e SS:[SP]=13B8h. In presenza della precedente istruzione, la CPU esegue le seguenti operazioni: In sostanza, indicando con Temp16 un registro interno a 16 bit della CPU, possiamo simulare il procedimento appena descritto, con le seguenti pseudo-istruzioni: Come si può notare, quando l'operando DEST di POP è SP (o ESP), le fasi 2 (trasferimento dati) e 3 (incremento di SP/ESP) vengono scambiate tra loro; naturalmente, si dà per scontato che nell'usare l'istruzione POP con operandi del tipo SP/ESP, DS, ES, SS, etc, il programmatore sappia esattamente ciò che sta facendo.

L'esecuzione delle istruzioni PUSH e POP, non modifica alcuno dei campi presenti nel Flags Register.

16.4 Le istruzioni PUSHA, PUSHAD, POPA, POPAD

Una situazione che si presenta molto spesso in Assembly, consiste nella chiamata di una procedura, la quale modifica uno o più registri senza preservarne il contenuto originale; quando la procedura termina, il "chiamante" riottiene il controllo e si ritrova con diversi registri, il cui contenuto non è più quello precedente alla chiamata della procedura stessa.
Supponiamo, ad esempio, che il programma principale stia utilizzando i registri AX e BX per contenere informazioni importanti; ad un certo punto, lo stesso programma principale deve chiamare una procedura di nome Proc1, la quale modifica AX e BX senza preservarne il contenuto originale. Se non vogliamo perdere il contenuto originale di AX e BX, possiamo scrivere le seguenti istruzioni: Naturalmente, questo procedimento funziona solo se, all'interno di Proc1, non vengono commessi errori nella gestione dello stack.
Questo modo di programmare appare piuttosto discutibile in quanto, per ogni chiamata di Proc1, dobbiamo inserire due istruzioni PUSH e due istruzioni POP; come è facile intuire, si tratta di una situazione che, oltre ad accrescere le dimensioni del programma, ci espone anche ad una elevata probabilità di commettere qualche errore.
La strada più logica e più sicura da seguire, consiste nel fare in modo che sia la procedura stessa a preservare il contenuto di tutti i registri che utilizza; in sostanza, tutte le necessarie istruzioni PUSH e POP, devono essere inserite all'interno della procedura.

In certi casi, è fondamentale che una procedura preservi il contenuto di tutti i registri che utilizza; questa situazione si presenta, ad esempio, per le ISR.
Come sappiamo, quando arriva una richiesta di interruzione hardware, la CPU sospende temporaneamente il programma in esecuzione e chiama l'opportuna ISR; le richieste di interruzione hardware, arrivano quindi in modo asincrono (cioè non sincronizzato) rispetto al programma in esecuzione. Al termine della ISR, la CPU riavvia il programma precedentemente interrotto, il quale si aspetta di trovare intatti tutti i registri che stava utilizzando; se la ISR non ha preservato il contenuto dei registri che ha utilizzato, si verifica quindi un sicuro crash.

L'utilizzo di un numero eccessivo (e ingiustificato) di istruzioni PUSH e POP, influisce negativamente sulle prestazioni di un programma; proprio per questo motivo, è necessario limitare al massimo il loro impiego.
Nel caso in cui sia assolutamente necessario preservare il contenuto di numerosi registri generali, può rivelarsi vantaggioso (in termini di cicli macchina) l'impiego delle istruzioni PUSHA, PUSHAD, POPA e POPAD, disponibili a partire dalle CPU 80186; ciascuna di queste istruzioni è in grado di gestire ben 8 registri generali, sia a 16, sia a 32 bit.
Le uniche forme lecite per queste istruzioni, sono le seguenti: Come si può notare, tutte queste istruzioni devono essere utilizzate senza alcun operando esplicito; infatti, sia l'operando SRC, sia l'operando DEST, vengono stabiliti, implicitamente, dalla CPU.
Proprio per questo motivo, diventa determinante l'attributo Dimensione assegnato al segmento di programma nel quale sono presenti queste istruzioni; è necessario quindi distinguere tra modalità reale (attributo USE16), e modalità protetta (attributo USE32).
In presenza dell'attributo USE16, le istruzioni PUSHA e POPA operano sui registri generali a 16 bit, mentre le istruzioni PUSHAD e POPAD, operano sui registri generali a 32 bit; in presenza, invece, dell'attributo USE32, tutte queste istruzioni operano, in ogni caso, sui registri generali a 32 bit.
Nel seguito, facciamo riferimento ad un programma Assembly destinato alla modalità reale; tutti i segmenti di questo programma sono quindi dotati di attributo USE16.

16.4.1 Le istruzioni PUSHA e PUSHAD

Con il mnemonico PUSHA, si indica l'istruzione Push All General-Purpose Registers (inserimento sulla cima dello stack, del contenuto di tutti i registri generali); in presenza dell'istruzione:
pusha
la CPU compie le seguenti operazioni: In sostanza, l'istruzione PUSHA equivale a: Con il mnemonico PUSHAD, si indica l'istruzione Push All General-Purpose Registers (Dword size) (inserimento sulla cima dello stack, del contenuto di tutti i registri generali a 32 bit); in presenza dell'istruzione:
pushad
la CPU compie le seguenti operazioni: In sostanza, l'istruzione PUSHAD equivale a: Le istruzioni PUSHA e PUSHAD hanno l'identico codice macchina 01100000b (60h); in modalità reale, per permettere alla CPU di distinguere PUSHAD da PUSHA, l'assembler inserisce il prefisso 66h.

16.4.2 Le istruzioni POPA e POPAD

Con il mnemonico POPA, si indica l'istruzione Pop All General-Purpose Registers (estrazione dalla cima dello stack, di valori da trasferire in tutti i registri generali); in presenza dell'istruzione:
popa
la CPU compie le seguenti operazioni: In sostanza, l'istruzione PUSHA equivale a: Come si può notare, la sequenza di estrazione con POPA è ovviamente invertita rispetto alla sequenza di inserimento con PUSHA; la quarta WORD, destinata a SP, viene ignorata (cioè, SP viene direttamente incrementato di 2 byte senza che venga effettuata alcuna estrazione).

Con il mnemonico POPAD, si indica l'istruzione Pop All General-Purpose Registers (Dword size) (estrazione dalla cima dello stack, di valori da trasferire in tutti i registri generali a 32 bit); in presenza dell'istruzione:
popad
la CPU compie le seguenti operazioni: In sostanza, l'istruzione PUSHAD equivale a: Anche in questo caso, si nota che la sequenza di estrazione con POPAD è ovviamente invertita rispetto alla sequenza di inserimento con PUSHAD; la quarta DWORD, destinata a ESP, viene ignorata (cioè, ESP viene direttamente incrementato di 4 byte senza che venga effettuata alcuna estrazione).

Le istruzioni POPA e POPAD hanno l'identico codice macchina 01100001b (61h); in modalità reale, per permettere alla CPU di distinguere PUSHAD da PUSHA, l'assembler inserisce il prefisso 66h.

16.4.3 Effetti provocati da PUSHA, PUSHAD, POPA e POPAD, sugli operandi e sui flags

Le istruzioni PUSHA, PUSHAD, POPA e POPAD, modificano il solo operando DEST, che viene sovrascritto dal contenuto dell'operando SRC; il contenuto dell'operando SRC rimane inalterato.
Come accade per PUSH, anche per PUSHA e PUSHAD si pone il problema legato alla presenza del registro SP/ESP; questo problema è stato risolto, salvando nello stack il valore originale di SP/ESP, cioè il valore che precede il decremento di SP/ESP.
In relazione, invece, alle istruzioni POPA e POPAD, il problema non si pone; infatti, abbiamo appena visto che il valore estratto dallo stack e destinato a SP/ESP, viene scartato.

L'esecuzione delle istruzioni PUSHA, PUSHAD, POPA e POPAD, non modifica alcuno dei campi presenti nel Flags Register.

16.5 Le istruzioni PUSHF, PUSHFD, POPF, POPFD

Se vogliamo gestire velocemente l'intero contenuto del registro dei flags, possiamo servirci delle istruzioni PUSHF, PUSHFD, POPF e POPFD; le uniche forme lecite per queste istruzioni, sono le seguenti: Come si può notare, tutte queste istruzioni devono essere utilizzate senza alcun operando esplicito; infatti, sia l'operando SRC, sia l'operando DEST, vengono stabiliti, implicitamente, dalla CPU.
Anche in questo caso quindi, diventa determinante l'attributo Dimensione assegnato al segmento di programma nel quale sono presenti queste istruzioni; è necessario cioè, distinguere tra modalità reale (attributo USE16), e modalità protetta (attributo USE32).
In presenza dell'attributo USE16, le istruzioni PUSHF e POPF operano sul registro FLAGS, mentre le istruzioni PUSHFD e POPFD, operano sul registro EFLAGS; in presenza, invece, dell'attributo USE32, tutte queste istruzioni operano, in ogni caso, sul registro EFLAGS.
Nel seguito, facciamo riferimento ad un programma Assembly destinato alla modalità reale.

Ricordiamo che il registro FLAGS occupa 16 bit e assume la struttura mostrata in Figura 16.6; il registro EFLAGS occupa 32 bit e rappresenta l'estensione del registro FLAGS.

16.5.1 Le istruzioni PUSHF e PUSHFD

Con il mnemonico PUSHF, si indica l'istruzione Push FLAGS Register onto the Stack (inserimento sulla cima dello stack, del contenuto a 16 bit del registro FLAGS); in presenza dell'istruzione:
pushf
la CPU compie le seguenti operazioni: Con il mnemonico PUSHFD, si indica l'istruzione Push EFLAGS Register onto the Stack (inserimento sulla cima dello stack, del contenuto a 32 bit del registro EFLAGS); in presenza dell'istruzione:
pushfd
la CPU compie le seguenti operazioni: Le istruzioni PUSHF e PUSHFD hanno l'identico codice macchina 10011100b (9Ch); in modalità reale, per permettere alla CPU di distinguere PUSHFD da PUSHF, l'assembler inserisce il prefisso 66h.

Supponiamo, ad esempio, di voler effettuare una addizione, per verificarne gli effetti prodotti sul registro FLAGS; possiamo scrivere allora le seguenti istruzioni: Per il corretto funzionamento di questo esempio è fondamentale il fatto che, come sappiamo, le istruzioni MOV, POP e CALL non modificano alcun campo del registro FLAGS; inoltre, le procedure writeBin16 e writeSdec16, preservano il contenuto del registro FLAGS.

16.5.2 Le istruzioni POPF e POPFD

Con il mnemonico POPF, si indica l'istruzione Pop Stack into FLAGS Register (estrazione dalla cima dello stack, di una WORD da trasferire nel registro FLAGS); in presenza dell'istruzione:
popf
la CPU compie le seguenti operazioni: Con il mnemonico POPFD, si indica l'istruzione Pop Stack into EFLAGS Register (estrazione dalla cima dello stack, di una DWORD da trasferire nel registro EFLAGS); in presenza dell'istruzione:
popfd
la CPU compie le seguenti operazioni: Le istruzioni POPF e POPFD hanno l'identico codice macchina 10011101b (9Dh); in modalità reale, per permettere alla CPU di distinguere POPFD da POPF, l'assembler inserisce il prefisso 66h.

16.5.3 Effetti provocati da PUSHF, PUSHFD, POPF e POPFD, sugli operandi e sui flags

Le istruzioni PUSHF, PUSHFD, POPF e POPFD, modificano il solo operando DEST, che viene sovrascritto dal contenuto dell'operando SRC; il contenuto dell'operando SRC rimane inalterato.

L'esecuzione delle istruzioni PUSHF e PUSHFD non modifica alcuno dei campi presenti nel registro FLAGS/EFLAGS.
In modalità reale, l'esecuzione delle istruzioni POPF e POPFD modifica tutti i campi presenti nel solo registro FLAGS (cioè, i campi visibili in Figura 16.6); i campi presenti nei 16 bit più significativi di EFLAGS, rimangono inalterati.

16.6 Le istruzioni LAHF e SAHF

Sempre in relazione alla gestione del Flags Register, sono disponibili anche le due istruzioni LAHF e SAHF; le uniche forme permesse per queste istruzioni sono: Come si può notare, queste istruzioni devono essere utilizzate senza alcun operando esplicito; infatti, sia l'operando SRC, sia l'operando DEST, vengono stabiliti, implicitamente, dalla CPU.
Come al solito, diventa determinante l'attributo Dimensione assegnato al segmento di programma nel quale sono presenti queste istruzioni; se l'attributo è USE16, viene coinvolto il registro FLAGS, mentre se l'attributo è USE32, viene coinvolto il registro EFLAGS.

16.6.1 L'istruzione LAHF

Con il mnemonico LAHF, si indica l'istruzione Load Status Flags into AH Register (trasferimento nel registro AH, degli 8 bit meno significativi del registro FLAGS/EFLAGS); in presenza dell'istruzione:
lahf
la CPU legge gli 8 bit meno significativi del registro FLAGS e li trasferisce nel registro AH; analizzando la Figura 16.6, possiamo dire che dopo l'esecuzione di questa istruzione, il registro AH assume la struttura mostrata in Figura 16.7.

16.6.2 L'istruzione SAHF

Con il mnemonico SAHF, si indica l'istruzione Store AH into Flags (trasferimento del contenuto del registro AH, negli 8 bit meno significativi del registro FLAGS/EFLAGS); in presenza dell'istruzione:
sahf
la CPU legge il contenuto del registro AH e lo trasferisce negli 8 bit meno significativi del registro FLAGS/EFLAGS; si dà per scontato il fatto che il contenuto di AH abbia un senso logico.

In genere, quando si ha la necessità di accedere in lettura/scrittura al Flags Register, si utilizzano istruzioni come PUSHF, POPF, etc; proprio per questo motivo, le due istruzioni LAHF e SAHF vengono utilizzate con minore frequenza. Un caso particolare è rappresentato dalla comparazione tra due numeri reali effettuata dal coprocessore matematico; come viene spiegato in un apposito capitolo della sezione Assembly Avanzato, in questo caso LAHF e SAHF ricoprono un ruolo importante nel determinare il risultato della comparazione stessa.

16.6.3 Effetti provocati da LAHF e SAHF sugli operandi e sui flags

Le istruzioni LAHF e SAHF, modificano il solo operando DEST, che viene sovrascritto dal contenuto dell'operando SRC; il contenuto dell'operando SRC rimane inalterato.

L'esecuzione dell'istruzione LAHF, non modifica alcuno dei campi presenti nel registro FLAGS/EFLAGS.
In modalità reale, l'esecuzione dell'istruzione SAHF modifica tutti i campi presenti negli 8 bit meno significativi del registro FLAGS; come si nota dalla Figura 16.7, i flags interessati sono, SF, ZF, AF, PF e CF.

16.7 L'istruzione LEA

Consideriamo la seguente istruzione:
mov ax, bx + offset Variabile8
Incontrando questa istruzione, l'assembler genera un messaggio di errore, per indicare che non è in grado di calcolare la somma tra il contenuto del registro BX e l'offset di Variabile8; infatti, solo nella fase di esecuzione di un programma, è possibile conoscere il contenuto del registro BX o di qualsiasi altro registro della CPU.

In sostanza, il calcolo della precedente somma può essere svolto solo dalla CPU durante la fase di esecuzione del programma; a tale proposito, la CPU stessa ci mette a disposizione una potente istruzione, chiamata LEA.
Con il mnemonico LEA, si indica l'istruzione Load Effective Address (trasferimento nel registro DEST, dell'effective address dell'operando SRC); le uniche forme lecite per questa istruzione, sono le seguenti (la sigla EA sta per effective address): Come abbiamo visto nel Capitolo 11, tutte le forme di indirizzamento permesse dalle CPU della famiglia 80x86, vengono definite effective address; l'operando SRC dell'istruzione LEA deve essere, appunto, uno tra gli effective address mostrati nelle Figure 11.8, 11.15 e 11.18 del Capitolo 11.
Il compito di LEA è quello di calcolare l'effective address specificato dall'operando SRC; il risultato di questo calcolo viene trasferito nell'operando DEST, che deve essere un registro a 16 o a 32 bit.
Come sappiamo, in modalità reale l'effective address rappresenta la componente Offset di un indirizzo logico Seg:Offset; l'istruzione LEA calcola il solo effective address di un indirizzo logico e non tiene quindi conto della eventuale presenza della componente Seg dell'indirizzo stesso. In sostanza, nel caso di una istruzione del tipo:
lea ax, ss:[bx+di+0004h]
la presenza del registro SS è del tutto superflua; nel registro AX viene trasferito il risultato della somma:
BX + DI + 0004h
Appare evidente il fatto che, il risultato di questa somma, è del tutto indipendente dalla presenza o meno di un registro di segmento. Il codice macchina dell'istruzione LEA è formato dal campo Opcode 10001101b (8Dh), dal campo mod_reg_r/m e da un eventuale Disp; analizziamo i 4 possibili casi che si possono presentare.

16.7.1 Effective address a 16 bit e registro DEST a 16 bit

Poniamo BX=0006h, SI=0004h e scriviamo l'istruzione:
lea cx, [bx+si+0002h]
Il registro destinazione è CX, per cui reg=001b; l'operando sorgente è un effective address del tipo [BX+SI+Disp8], per cui mod=01b, r/m=000b. Otteniamo quindi:
mod_reg_r/m = 01001000b = 48h
Il Disp8 è:
Disp8 = 00000010b = 02h
L'assembler genera quindi il codice macchina:
10001101b 01001000b 00000010b = 8Dh 48h 02h
In presenza di questo codice macchina, la CPU calcola:
BX + SI + 02h = 0006h + 0004h + 0002h = 000Ch
Il risultato 000Ch viene quindi trasferito nei 16 bit del registro CX.

Nel caso particolare di una istruzione del tipo:
lea bx, Variabile8
il codice macchina è formato dal campo Opcode, dal campo mod_reg_r/m e da un Disp16; il risultato che si ottiene è del tutto equivalente a quello dell'istruzione:
mov bx, offset Variabile8
Si tenga presente, però, che con l'istruzione MOV, il codice macchina richiede 1 byte in meno; infatti, il trasferimento dati da Imm16 a Reg16 ha un codice macchina formato dal campo Opcode e da un Imm16.

16.7.2 Effective address a 16 bit e registro DEST a 32 bit

In riferimento all'esempio di Figura 16.1, osserviamo che Variabile8 si trova all'offset 0008h del segmento DATASEGM; posto allora BX=0020h, scriviamo l'istruzione:
lea edx, Variabile8[bx]
Il registro destinazione è EDX, per cui reg=010b; l'operando sorgente, cioè lo "strano" simbolo [Variabile8+BX], non è altro che un banalissimo effective address del tipo [BX+Disp16], per cui mod=10b, r/m=111b. Otteniamo quindi:
mod_reg_r/m = 10010111b = 97h
Il Disp16 è:
Disp16 = 0000000000001000b = 0008h
L'operando DEST è a 32 bit, per cui si rende necessario il prefisso 66h; l'assembler genera quindi il codice macchina:
01100110b 10001101b 10010111b 0000000000001000b = 66h 8Dh 97h 0008h
In presenza di questo codice macchina, la CPU calcola:
BX + 0008h = 0020h + 0008h = 0028h
Il valore 0028h viene esteso a 32 bit con degli zeri e si ottiene 00000028h; il risultato 00000028h viene quindi trasferito nei 32 bit del registro EDX.

In assenza della direttiva ASSUME che associa DATASEGM a DS, l'assembler non genera alcun messaggio di errore; è ovvio, infatti, che il calcolo dell'offset di Variabile8 non ha niente a che vedere con il SegReg a cui viene associato DATASEGM.

16.7.3 Effective address a 32 bit e registro DEST a 16 bit

Poniamo EBP=00008BFAh e scriviamo l'istruzione:
lea ax, [ebp+00001FB8h]
Il registro destinazione è AX, per cui reg=000b; l'operando sorgente è un effective address del tipo [EBP+Disp32], per cui mod=10b, r/m=101b. Otteniamo quindi:
mod_reg_r/m = 10000101b = 85h
Il Disp32 è:
Disp32 = 00000000000000000001111110111000b = 00001FB8h
L'indirizzo SRC è a 32 bit, per cui si rende necessario il prefisso 67h; l'assembler genera quindi il codice macchina:
01100111b 10001101b 10000101b 00000000000000000001111110111000b = 67h 8Dh 85h 00001FB8h
In presenza di questo codice macchina, la CPU calcola:
EBP + 00001FB8h = 00008BFAh + 00001FB8h = 0000ABB2h
I 16 bit meno significativi del risultato, cioè ABB2h, vengono quindi trasferiti nei 16 bit del registro AX.

16.7.4 Effective address a 32 bit e registro DEST a 32 bit

Poniamo ECX=0000A1B8h, EDI=00000020h e scriviamo l'istruzione:
lea ebx, [ecx+(edi*4)+00002C8Eh]
Il registro destinazione è EBX, per cui reg=011b; l'operando sorgente è un effective address del tipo [ECX+(indice scalato)+Disp32], per cui mod=10b, r/m=100b. Otteniamo quindi:
mod_reg_r/m = 10011100b = 9Ch
Il fattore di scala è 4, il registro base è ECX e il registro indice è EDI, per cui scale=10b, index=111b, base=001b; otteniamo quindi:
S.I.B. = 10111001b = B9h
Il Disp32 è:
Disp32 = 00000000000000000010110010001110b = 00002C8Eh
L'operando DEST è a 32 bit, per cui si rende necessario il prefisso 66h; l'indirizzo SRC è a 32 bit, per cui si rende necessario il prefisso 67h. L'assembler genera quindi il codice macchina: In presenza di questo codice macchina, la CPU calcola:
ECX + (EDI * 4) + 00002C8Eh = 0000A1B8h + (00000020h * 4) + 00002C8Eh = 0000CEC6h
Il risultato 0000CEC6h viene quindi trasferito nei 32 bit del registro EBX.

16.7.5 Effetti provocati da LEA sugli operandi e sui flags

L'esecuzione dell'istruzione LEA, modifica il contenuto del solo operando DEST, che viene sovrascritto dal contenuto di SRC; il contenuto dell'operando SRC rimane inalterato.
Anche per LEA, bisogna prestare attenzione ai casi del tipo:
lea bx, [bx+0008h]
Supponiamo di avere BX=0020h e quindi, DS:(BX+0008h)=DS:0028h; in relazione al registro BX, accade che: Di conseguenza, DS:(BX+0008h) non rappresenta più l'indirizzo DS:0028h, bensì DS:(0028h+0008h)=DS:0030h.

L'esecuzione dell'istruzione LEA, non modifica alcuno dei campi presenti nel Flags Register.

16.8 Le istruzioni LDS, LES, LFS, LGS, LSS

Tutte le istruzioni della CPU che operano sugli indirizzi FAR, presuppongono che la struttura di questi indirizzi segua la convenzione appena enunciata; tra queste particolari istruzioni, troviamo quelle rappresentate dai mnemonici LDS, LES, LFS, LGS e LSS.
Questi mnemonici, indicano la generica istruzione Load Far Pointer (caricamento di un indirizzo FAR Seg:Offset, in una coppia SegReg:Reg); le uniche forme permesse per queste istruzioni sono le seguenti: Possiamo dire quindi che, se l'operando DEST è un Reg16, allora l'operando SRC deve essere una locazione di memoria da 32 bit contenente una coppia Seg:Offset da 16+16 bit; se l'operando DEST è un Reg32, allora l'operando SRC deve essere una locazione di memoria da 48 bit contenente una coppia Seg:Offset da 16+32 bit.
La componente Offset di questa coppia viene caricata nel Reg che rappresenta l'operando DEST; la componente Seg di questa coppia viene caricata nel SegReg specificato dal mnemonico dell'istruzione stessa (DS, ES, FS, GS o SS). In relazione alle istruzioni LDS, LES, LFS, LGS e LSS, si possono presentare due casi fondamentali, che vengono analizzati nel seguito.

16.8.1 Operando DEST di tipo Reg16

Nel segmento dati (ad esempio, DATASEGM) del nostro programma, definiamo i seguenti dati statici: Supponiamo ora di voler stampare la stringa VarString, con la procedura writeString della libreria EXELIB; come sappiamo, questa procedura richiede che ES:DI punti alla stringa C da stampare e che DX contenga le coordinate di output.
Nell'ipotesi che DS=DATASEGM e in presenza di una direttiva ASSUME che associa DATASEGM a DS, possiamo scrivere allora le seguenti istruzioni: Dai manuali della CPU, si ricava che il codice macchina dell'istruzione:
les di, FarPointer
è formato dal campo Opcode 11000100b (C4h), seguito dal campo mod_reg_r/m e da un Disp16 (cioè, dall'offset di FarPointer); abbiamo quindi:
Opcode = 11000100b = C4h
Il registro DEST è DI, per cui reg=111b; l'operando SRC è un Disp16, per cui mod=00b, r/m=110b. Otteniamo quindi:
mod_reg_r/m = 00111110b = 3Eh
Supponendo che FarPointer si trovi all'offset 000Ah di DATASEGM, otteniamo:
Disp16 = 0000000000001010b = 000Ah
L'assembler genera quindi il codice macchina:
11000100b 00111110b 0000000000001010b = C4h 3Eh 000Ah
Come possiamo notare, nel codice macchina non è presente alcun prefisso 66h (operandi a 32 bit) o 67h (indirizzi a 32 bit); infatti, siccome l'operando DEST è di tipo SegReg:Reg16, la CPU tratta l'operando SRC come Mem16:Mem16.
Quando la CPU incontra questo codice macchina, trasferisce in DI i 16 bit contenuti nella locazione di memoria DS:000Ah e in ES i 16 bit contenuti nella locazione di memoria DS:000Ch.

Affinché questo esempio funzioni in modo corretto, è fondamentale che l'indirizzo Seg:Offset di VarString venga disposto in FarPointer nel rispetto della convenzione enunciata in precedenza; la componente Offset di VarString deve essere posta nei 16 bit meno significativi di FarPointer, mentre la componente Seg di VarString deve essere posta nei 16 bit più significativi di FarPointer.
Se vogliamo verificare in pratica la corretta disposizione in FarPointer, dell'indirizzo Seg:Offset di VarString, possiamo scrivere il seguente codice (che deve essere aggiunto a quello del precedente esempio):

16.8.2 Operando DEST di tipo Reg32

Nel segmento dati (ad esempio, DATASEGM) del nostro programma, definiamo i seguenti dati statici: Supponiamo ora di voler caricare la coppia Seg16:Off32, sia in FarPointer48, sia in FS:ESI; nell'ipotesi che DS=DATASEGM e in presenza di una direttiva ASSUME che associa DATASEGM a DS, possiamo scrivere le seguenti istruzioni: Dai manuali della CPU, si ricava che il codice macchina dell'istruzione:
lfs esi, [bx]
è formato dal campo Opcode 00001111b 10110100b (0Fh B4h), seguito dal campo mod_reg_r/m; abbiamo quindi:
Opcode = 00001111b 10110100b = 0Fh B4h
Il registro DEST è ESI, per cui reg=110b; l'operando SRC è un effective address del tipo [BX], per cui mod=00b, r/m=111b. Otteniamo quindi:
mod_reg_r/m = 00110111b = 37h
L'operando DEST è a 32 bit, per cui si rende necessario il prefisso 66h; l'assembler genera quindi il codice macchina:
01100110b 00001111b 10110100b 00110111b = 66h 0Fh B4h 37h
Osserviamo che, in relazione all'operando [BX], non è necessario l'operatore DWORD PTR; infatti, siccome l'operando DEST è di tipo SegReg:Reg32, la CPU tratta l'operando SRC come Mem16:Mem32.
Quando la CPU incontra questo codice macchina, trasferisce in ESI i 32 bit contenuti nella locazione di memoria DS:(BX+0) e in FS i 16 bit contenuti nella locazione di memoria DS:(BX+4).

16.8.3 Effetti provocati da LDS, LES, LFS, LGS e LSS, sugli operandi e sui flags

L'esecuzione delle istruzioni LDS, LES, LFS, LGS e LSS, modifica il contenuto del solo operando DEST, che viene sovrascritto dal contenuto di SRC; il contenuto dell'operando SRC rimane inalterato.
Anche per queste istruzioni, bisogna prestare attenzione ai casi del tipo:
les bx, [bx+si+0004h]
Dopo l'esecuzione di questa istruzione, il contenuto della locazione di memoria puntata da DS:(BX+SI+0004h) rimane inalterato; invece, il contenuto del registro puntatore BX viene modificato.

L'esecuzione delle istruzioni LDS, LES, LFS, LGS e LSS, non modifica alcuno dei campi presenti nel Flags Register.

16.9 Le istruzioni IN e OUT

Nei precedenti capitoli è stato detto che ciascuna periferica collegata al computer, comunica con la CPU attraverso proprie aree di memoria che vengono chiamate porte hardware; si usa il termine "porta" proprio perché, grazie ad essa, i dati scambiati con la CPU possono entrare nella periferica o uscire dalla periferica.
Ogni periferica può essere dotata di una sola porta hardware, come nel caso dei vecchi joystick a due assi e due pulsanti, o di numerosissime porte hardware, come nel caso delle schede video; considerando proprio il caso della scheda video, si può dire che questa periferica, oltre ad essere dotata di numerose porte hardware propriamente dette, presenta anche un'area riservata alla memoria video (VRAM), formata spesso da milioni di byte, che formalmente possiamo equiparare a milioni di porte hardware.
Quando si deve comunicare con una memoria periferica molto grande (come nel caso della VRAM), si ricorre ad un metodo chiamato I/O Memory Mapped (input/output mappato in RAM); in pratica, la memoria periferica viene "mappata" in un'area della memoria centrale del computer, chiamata finestra, in modo che tutte le operazioni di I/O compiute su questa finestra, si ripercuotano sulla memoria periferica stessa.
Tutte le porte hardware che fanno capo, invece, ad aree di memoria molto piccole, vengono individuate assegnando a ciascuna di esse un indirizzo fisico, proprio come accade per i vari byte che formano la RAM; quando la CPU vuole comunicare con una di queste porte hardware, deve caricare il relativo indirizzo sull'Address Bus. Attraverso la logica di controllo, la CPU indica che quell'indirizzo si riferisce proprio ad una porta hardware e non ad una locazione della RAM (vedere, ad esempio, la Figura 7.9 del Capitolo 7); dopo aver svolto queste operazioni, la CPU è pronta per comunicare con la porta hardware attraverso il Data Bus.

Tutte le CPU precedenti l'80386, utilizzano gli 8 bit meno significativi dell'Address Bus per specificare gli indirizzi delle varie porte hardware; queste CPU quindi, sono in grado di gestire sino a 28=256 porte hardware differenti.
Le CPU 80386 e superiori, invece, utilizzano i 16 bit meno significativi dell'Address Bus; in questo modo, possono gestire sino a 216=65536 porte hardware differenti. Le istruzioni che la CPU mette a disposizione per le comunicazioni con le porte hardware, sono rappresentate dai due mnemonici IN e OUT; l'istruzione IN serve per leggere dati da una porta hardware, mentre l'istruzione OUT serve per scrivere dati in una porta hardware.
L'utilizzo pratico di queste istruzioni, viene trattato in dettaglio nella sezione Assembly Avanzato; si raccomanda vivamente di non utilizzare IN e OUT per effettuare letture/scritture a caso nelle porte hardware, in quanto si potrebbero verificare malfunzionamenti del PC.

16.9.1 L'istruzione IN

Con il mnemonico IN si indica l'istruzione INput from port; (lettura dati da una porta hardware); le uniche forme lecite per questa istruzione, sono le seguenti: L'indirizzo della porta hardware a cui accedere in lettura (operando SRC), può essere espresso attraverso un Imm8, oppure attraverso il registro DX; con un Imm8 possiamo specificare una tra 256 possibili porte, mentre con DX possiamo specificare una tra 65536 possibili porte (se si utilizza DX per specificare un indirizzo a 8 bit, è importante che gli 8 bit più significativi dello stesso DX vengano posti a zero).
L'operando DEST deve essere obbligatoriamente l'accumulatore; l'ampiezza in bit (8/16/32) dell'accumulatore (AL/AX/EAX), determina il numero di byte (1/2/4) da leggere dalla porta.

Nel Capitolo 14 sono stati illustrati alcuni semplici esempi relativi all'accesso alle porte hardware; tali esempi funzionano solo con i SO che permettono l'accesso diretto alle porte hardware.

16.9.2 L'istruzione OUT

Con il mnemonico OUT si indica l'istruzione OUTput to port (scrittura dati in una porta hardware); le uniche forme lecite per questa istruzione, sono le seguenti: L'operando SRC, deve essere obbligatoriamente l'accumulatore; l'ampiezza in bit (8/16/32) dell'accumulatore (AL/AX/EAX), determina il numero di byte (1/2/4) da scrivere nella porta.
L'indirizzo della porta hardware a cui accedere in scrittura (operando DEST), può essere espresso attraverso un Imm8, oppure attraverso il registro DX; con un Imm8 possiamo specificare una tra 256 possibili porte, mentre con DX possiamo specificare una tra 65536 possibili porte (se si utilizza DX per specificare un indirizzo a 8 bit, è importante che gli 8 bit più significativi dello stesso DX, vengano posti a zero).

Osserviamo che per questa particolare istruzione, l'operando DEST può essere di tipo Imm; ovviamente, in questo caso l'Imm rappresenta l'indirizzo di una porta hardware verso cui inviare i dati.

16.9.3 Effetti provocati da IN e OUT sugli operandi e sui flags

L'esecuzione dell'istruzione IN, modifica il contenuto del solo accumulatore; il contenuto della porta da cui si effettua la lettura, rimane inalterato.
L'esecuzione dell'istruzione OUT, modifica il contenuto della porta in cui si effettua la scrittura; il contenuto dell'accumulatore, rimane inalterato.

L'esecuzione delle istruzioni IN e OUT, non modifica alcuno dei campi presenti nel Flags Register.

16.10 L'istruzione XCHG

Un caso classico che si presenta nella programmazione, consiste nella eventualità di dover scambiare il contenuto di due variabili; vediamo un esempio pratico riferito al linguaggio Pascal. Supponiamo di aver definito una variabile IntVar1=12850 e una variabile IntVar2=27500, entrambe di tipo Integer (intero con segno a 16 bit); per effettuare lo scambio del contenuto di IntVar1 e IntVar2, si definisce una terza variabile, ad esempio, Temp, sempre di tipo Integer, e si scrivono le tre classiche istruzioni: Dopo l'esecuzione di queste tre istruzioni, si ottiene IntVar1=27500 e IntVar2=12850; si è verificato quindi lo scambio del contenuto delle due variabili.
Il codice utilizzato appare molto compatto grazie al fatto che, i linguaggi di alto livello, permettono di simulare, via software, un trasferimento dati tra due locazioni di memoria; è chiaro però che al momento di convertire il programma in codice macchina, il compilatore Pascal deve tenere conto del fatto che la CPU non supporta questo tipo di operazione. Di conseguenza, il compilatore stesso deve trovare le istruzioni Assembly che permettano di scambiare il contenuto di IntVar1 e IntVar2, nel modo più rapido e più compatto possibile; una soluzione potrebbe essere la seguente: Questo primo metodo genera un codice macchina da 16 byte e con una CPU 80486 richiede circa 4+4+6+6=20 cicli macchina.
Un'altra soluzione potrebbe essere la seguente: Questo secondo metodo genera un codice macchina da 12 byte e con una CPU 80486 richiede circa 1+1+1+1=4 cicli macchina; come si può notare, questo metodo è più compatto e circa 5 volte più veloce di quello precedente!

Se siamo interessati, non solo alla velocità del codice, ma anche alla sua compattezza, possiamo servirci di una apposita istruzione rappresentata dal mnemonico XCHG; questo mnemonico indica l'istruzione Exchange Register/Memory with Register (scambia il contenuto di un Reg/Mem e di un Reg).
Le uniche forme lecite per questa istruzione, sono le seguenti: Trattandosi di un trasferimento dati "incrociato", i due operandi devono avere la stessa ampiezza in bit (che può essere 8, 16 o 32); è proibito l'impiego di operandi di tipo SegReg o Imm.

Ripetiamo l'esempio precedente, servendoci dell'istruzione XCHG; in questo caso, possiamo scrivere: Questo terzo metodo genera un codice macchina da 10 byte e con una CPU 80486 richiede circa 1+5+1=7 cicli macchina; come si può notare, questo metodo presenta una estrema compattezza ed è anche piuttosto veloce. Come si può facilmente intuire, nel caso di un programma che contiene numerosissimi scambi tra variabili, l'uso di XCHG porta ad una notevole riduzione delle dimensioni del codice.

16.10.1 Uso di XCHG con operandi di ampiezza arbitraria

Con le conoscenze che abbiamo acquisito, possiamo facilmente utilizzare le istruzioni illustrate in questo capitolo, con operandi di ampiezza arbitraria; in sostanza, possiamo effettuare trasferimenti di dati, anche tra operandi di ampiezza superiore a 32 bit.
Come esempio generale, vediamo una applicazione dell'istruzione XCHG per lo scambio di due QWORD; a tale proposito, nel blocco dati (ad esempio, DATASEGM) del nostro programma, inseriamo le seguenti definizioni: Se abbiamo a disposizione una CPU 80386 o superiore, non dobbiamo fare altro che operare via hardware sulle singole DWORD dei dati di tipo QWORD; in questo modo, sfruttiamo al massimo le prestazioni della CPU.
In presenza di una direttiva ASSUME che associa DATASEGM a DS, possiamo applicare il seguente metodo: L'operatore DWORD PTR è necessario in quanto abbiamo definito VarXchg1 e VarXchg2 come variabili di tipo QWORD; osserviamo, inoltre, che per visualizzare in modo corretto un dato di tipo QWORD con writeHex32, dobbiamo iniziare dalla DWORD più significativa; in caso contrario, writeHex32 stampa delle H sul numero da visualizzare.

Nel caso in cui le variabili abbiano una ampiezza pari a decine e decine di byte, il metodo da applicare è del tutto simile a quello appena illustrato; a tale proposito, supponiamo di voler scambiare il contenuto delle seguenti due variabili:
BigVar1 = 99DD47B194B611F316F942EC3AC8F2BBh
e:
BigVar2 = 3B00C8C2668AB6A93CE1941D8F66B147h
Se vogliamo disporre in memoria queste due variabili, nel rispetto della convenzione little-endian, dobbiamo scrivere le seguenti definizioni: Come si può notare, le varie DWORD di ogni variabile vengono disposte una di seguito all'altra, a partire da quella meno significativa; in questo modo, l'assembler legge le DWORD a partire da quella più a sinistra e le dispone in memoria ad indirizzi via via crescenti.

Alternativamente, si può anche scrivere: Anche in questo caso, le varie QWORD devono essere disposte, una di seguito all'altra, a partire da quella meno significativa.

A questo punto, tutto ciò che dobbiamo fare consiste nel gestire BigVar1 e BigVar2, suddividendole in tante DWORD; servendoci di due registri puntatori e nell'ipotesi che DS=DATASEGM, possiamo scrivere: In questo modo, le quattro DWORD di BigVar1 sono rappresentate da [SI+0], [SI+4], [SI+8] e [SI+12], mentre le quattro DWORD di BigVar2 sono rappresentate da [DI+0], [DI+4], [DI+8] e [DI+12]; per scambiare, ad esempio, le DWORD meno significative di BigVar1 e BigVar2, possiamo scrivere: Grazie alla presenza del registro EAX, non è necessario l'uso dell'operatore DWORD PTR.
Al momento di visualizzare, con writeHex32, le due variabili BigVar1 e BigVar2, dobbiamo ricordarci di partire con le rispettive DWORD più significative e cioè, [SI+12] e [DI+12].

In un prossimo capitolo verranno illustrate le istruzioni di salto; con tali istruzioni, potremo realizzare delle iterazioni (loop) che ci permetteranno di rendere estremamente compatto il codice presentato nei precedenti esempi.

16.10.2 Effetti provocati da XCHG sugli operandi e sui flags

L'esecuzione dell'istruzione XCHG, modifica il contenuto di entrambi gli operandi; inoltre, bisogna prestare attenzione ai soliti casi del tipo:
xchg bx, [bx+di+002Ah]
Dopo l'esecuzione di questa istruzione, il contenuto del registro puntatore BX viene modificato.

Analogamente a quanto abbiamo visto per le istruzioni del tipo:
mov cx, cx
anche le istruzioni del tipo:
xchg dx, dx
non provocano alcuna modifica del contenuto dell'operando registro impiegato, contemporaneamente, come sorgente e come destinazione; proprio per questo motivo, gli assembler possono utilizzare queste particolari istruzioni, in sostituzione di NOP, per inserire dei buchi di memoria all'interno dei segmenti di codice.
In particolare, l'istruzione:
xchg ax, ax
viene considerata equivalente ad una istruzione NOP; infatti, in entrambi i casi viene generato il codice macchina 90h.

L'esecuzione dell'istruzione XCHG, non modifica alcuno dei campi presenti nel Flags Register.