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:
- il bit in posizione 0 di SRC viene copiato nel bit in
posizione 0 di DEST
- il bit in posizione 1 di SRC viene copiato nel bit in
posizione 1 di DEST
- il bit in posizione 2 di SRC viene copiato nel bit in
posizione 2 di DEST
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:
- BX, SI e DI vengono associati automaticamente a DS
- BP e SP vengono associati automaticamente a SS
- IP viene associato automaticamente a CS
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:
- prima del trasferimento dati, SI=0008h
- dopo il trasferimento dati, SI=28D6h
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:
- accede in memoria all'indirizzo logico DS:0008h
- legge il valore a 8 bit 01110110b
- estende questo valore a 16 bit con degli 0 a sinistra,
ottenendo 0000000001110110b
- copia 0000000001110110b nei 16 bit di BX
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:
- accede in memoria all'indirizzo logico DS:(BP+SI+02h)
- legge il valore a 8 bit 11010110b
- estende questo valore a 32 bit con degli 0 a sinistra,
ottenendo 00000000000000000000000011010110b
- copia 00000000000000000000000011010110b nei 32 bit di EBX
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:
- accede in memoria all'indirizzo logico CS:0008h
- legge il valore a 16 bit 0010100011010110b
- estende questo valore a 32 bit con degli 0 a sinistra (bit
di segno di SRC), ottenendo 00000000000000000010100011010110b
- copia 00000000000000000010100011010110b nei 32 bit di
EBX
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:
- accede in memoria all'indirizzo logico CS:(BX+DI+02h)
- legge il valore a 16 bit 1000100011010110b
- estende questo valore a 32 bit con degli 1 a sinistra
(bit di segno di SRC), ottenendo 11111111111111111000100011010110b
- copia 11111111111111111000100011010110b nei 32 bit di
EBX
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:
- sottrae 2 byte al registro SP per fare posto ad una
nuova WORD nello stack
- copia la WORD contenuta in SRC, nella locazione di
memoria all'indirizzo SS:SP
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:
- sottrae 2 byte a SP e ottiene SP=03FEh
- legge il valore 28D6h dall'indirizzo CS:Variabile16
- trasferisce 28D6h nella locazione puntata da SS:SP (cioè,
SS:03FEh)
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:
- sottrae 4 byte a SP e ottiene SP=03FAh
- legge il valore 3ABF28D6h dall'indirizzo SS:BX
- trasferisce 3ABF28D6h nella locazione puntata da SS:SP (cioè,
SS:03FAh)
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:
- sottrae 2 byte a SP e ottiene SP=03F8h
- legge il valore ??D6h dal registro AX
- trasferisce ??D6h nella locazione puntata da SS:SP (cioè,
SS:03F8h)
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:
- copia in DEST la WORD che si trova nella locazione di
memoria all'indirizzo SS:SP
- somma 2 byte al registro SP per recuperare lo spazio che
si è liberato nello stack
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:
- legge il valore ??D6h dall'indirizzo SS:SP (cioè,
SS:03F8h)
- trasferisce ??D6h nel registro AX
- somma 2 byte a SP e ottiene SP=03FAh
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:
- legge il valore 3ABF28D6h dall'indirizzo SS:SP (cioè,
SS:03FAh)
- trasferisce 3ABF28D6h nella locazione di memoria all'indirizzo
SS:Variabile32
- somma 4 byte a SP e ottiene SP=03FEh
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:
- legge il valore 28D6h dall'indirizzo SS:SP (cioè,
SS:03FEh)
- trasferisce 28D6h nella locazione di memoria all'indirizzo
CS:(BX+DI+0002h)
- somma 2 byte a SP e ottiene SP=0400h
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:
- sottrae 2 byte a SP e ottiene SP=03FEh
- legge il valore 03FEh presente nel registro SP
- trasferisce 03FEh nella locazione puntata da SS:SP (cioè,
SS:03FEh)
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:
- memorizza il valore 0400h contenuto in SP
- sottrae 2 byte a SP e ottiene SP=03FEh
- trasferisce 0400h nella locazione puntata da SS:SP (cioè,
SS:03FEh)
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:
- memorizza il valore 13B8h presente all'indirizzo SS:SP
(cioè, SS:03FEh)
- somma 2 byte a SP e ottiene SP=0400h
- trasferisce 13B8h nel registro SP
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:
- sottrae 8*2=16 byte al registro SP per fare posto a
8 nuove WORD nello stack
- salva nello stack il contenuto della seguente sequenza ordinata di
registri generali:
AX, CX, DX, BX, SP originale, BP, SI, DI
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:
- sottrae 8*4=32 byte al registro SP per fare posto a
8 nuove DWORD nello stack
- salva nello stack il contenuto della seguente sequenza ordinata di
registri generali:
EAX, ECX, EDX, EBX, ESP originale, EBP, ESI, EDI
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:
- estrae 8 WORD dalla cima dello stack e le trasferisce
nella seguente sequenza ordinata di registri generali:
DI, SI, BP, --, BX, DX, CX, AX
- somma 8*2=16 byte al registro SP, per recuperare
lo spazio che si è liberato nello stack
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:
- estrae 8 DWORD dalla cima dello stack e le trasferisce
nella seguente sequenza ordinata di registri generali:
EDI, ESI, EBP, ---, EBX, EDX, ECX, EAX
- somma 8*4=32 byte al registro SP, per recuperare
lo spazio che si è liberato nello stack
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:
- sottrae 2 byte al registro SP per fare posto a
una nuova WORD nello stack
- salva il contenuto del registro FLAGS, nella locazione di
memoria all'indirizzo SS:SP
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:
- sottrae 4 byte al registro SP per fare posto a
una nuova DWORD nello stack
- salva il contenuto del registro EFLAGS, nella locazione di
memoria all'indirizzo SS:SP
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:
- estrae 1 WORD dalla cima dello stack e la trasferisce nel
registro FLAGS
- somma 2 byte al registro SP, per recuperare
lo spazio che si è liberato nello stack
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:
- estrae 1 DWORD dalla cima dello stack e la trasferisce nel
registro EFLAGS
- somma 4 byte al registro SP, per recuperare
lo spazio che si è liberato nello stack
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:
- prima dell'esecuzione di LEA, BX=0020h
- dopo l'esecuzione di LEA, BX=0020h+0008h=0028h
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.