Assembly Base con MASM

Capitolo 11: Il codice macchina e il codice Assembly


Nei precedenti capitoli è stato detto che le prime macchine di calcolo automatico, venivano progettate incorporando al loro interno l'unico programma che potevano eseguire; se si aveva la necessità di eseguire un programma differente, bisognava procedere alla progettazione di una nuova macchina. Per ovviare a questa situazione piuttosto scomoda, si pensò di realizzare delle macchine che incorporavano al loro interno un certo numero di programmi, selezionabili attraverso meccanismi più o meno complicati; in questo modo si migliorava sicuramente la situazione, ma non si risolveva certo il problema di fondo rappresentato dalla scarsissima flessibilità di questo sistema. In particolare, si rendeva necessario limitare al massimo il numero di programmi che queste macchine potevano eseguire, per evitare di andare incontro ad enormi complicazioni costruttive; in questo tipo di macchine si può dire che la logica di calcolo viene "cablata" all'interno della macchina stessa e per questo motivo si parla anche di logica cablata.

Negli attuali computer si utilizza, invece, un altro metodo che permette di eliminare radicalmente i problemi appena citati; invece di adattare il computer al programma da eseguire, si fa esattamente il contrario, adattando cioè il programma da eseguire al computer. Questo obiettivo viene raggiunto grazie al fatto che il cuore del computer e cioè, la CPU, presenta come sappiamo la caratteristica di poter riconoscere ed interpretare un certo numero di istruzioni a ciascuna delle quali corrisponde una ben precisa azione che la CPU stessa può svolgere; il programmatore dialoga con la CPU attraverso un programma che può essere visto come un insieme di dati e istruzioni. L'elaborazione di un programma da parte del computer consiste nell'inviare in sequenza queste istruzioni alla CPU, la quale può così procedere alla fase di decodifica e di esecuzione; grazie a questa tecnica, utilizzando poche decine di istruzioni è possibile scrivere una quantità di programmi limitata solo dalla fantasia del programmatore. Attraverso questo sistema la CPU può essere programmata e riprogrammata a piacere e per questo motivo si parla anche di logica programmabile.

Ogni famiglia di CPU quindi è in grado di decodificare ed eseguire un ben determinato insieme (set) di istruzioni; la totalità di queste istruzioni forma il cosiddetto set di istruzioni della CPU.
Nei precedenti capitoli abbiamo visto che la compatibilità tra diverse famiglie di CPU viene ottenuta rendendo compatibili le loro architetture interne; chiaramente però questo non basta, in quanto è necessario che le diverse famiglie di CPU siano compatibili anche a livello di set di istruzioni. Da questo punto di vista, due CPU sono compatibili tra loro quando hanno lo stesso set di istruzioni, oppure quando il set di istruzioni di una CPU è un sottoinsieme del set di istruzioni dell'altra CPU; questa è proprio la strada seguita per garantire la compatibilità verso il basso tra i diversi modelli delle CPU 80x86. Possiamo dire quindi che l'80286 estende il set di istruzioni dell'8086, l'80386 estende il set di istruzioni dell'80286, l'80486 estende il set di istruzioni dell'80386 e così via.

Come già sappiamo, l'unico linguaggio che la CPU è in grado di capire è il linguaggio binario; ciò implica che tutte le informazioni (codice, dati e stack) presenti in un programma, prima di poter essere elaborate, devono essere convertite in codice binario. Questo lavoro viene svolto da appositi strumenti software chiamati traduttori; una volta che il traduttore ha svolto il proprio compito, il nostro programma si presenta sotto forma di sequenza di numeri binari che nel loro insieme formano il cosiddetto machine code (codice macchina).
I programmi traduttori si suddividono in: assemblatori, compilatori e interpreti; l'assemblatore o assembler, ha il compito di tradurre in codice macchina un programma scritto in Assembly. Il compilatore o compiler, ha il compito di tradurre in codice macchina un programma scritto con un linguaggio di alto livello che per questo motivo viene chiamato linguaggio compilato; come vedremo in seguito, esiste una differenza abissale tra il lavoro svolto da un assemblatore e il lavoro svolto da un compilatore. L'interprete o interpreter, è analogo al compilatore, con la differenza che le istruzioni del programma vengono convertite in codice macchina ed eseguite una alla volta; i linguaggi di alto livello che permettono di scrivere programmi destinati agli interpreti, si chiamano linguaggi interpretati.
Mentre gli assemblatori e i compilatori generano un programma direttamente eseguibile dalla CPU, gli interpreti generano programmi che per essere eseguiti richiedono la presenza dell'interprete stesso in memoria; questo accade, ad esempio, con i programmi scritti nel vecchio BASIC interpretato che possono essere eseguiti solo se si ha a disposizione un apposito interprete BASIC. Sia i compilatori, sia gli interpreti, presentano vantaggi e svantaggi che verranno analizzati in seguito.

Il problema che si pone quindi, consiste nel trovare il modo per convertire in codice binario le informazioni (codice, dati e stack) che formano un programma; vediamo allora quale metodo viene seguito per raggiungere questo obiettivo.

11.1 Caratteristiche generali delle istruzioni per le CPU 80x86

Una qualunque istruzione, per poter essere eseguita, deve fornire alla CPU una serie di informazioni fondamentali che comprendono, in particolare, il tipo di operazione da svolgere; in sostanza, attraverso una istruzione dobbiamo dire alla CPU se vogliamo che venga eseguita una addizione, una divisione, una moltiplicazione, un complemento a 1, un trasferimento dati, etc.
Come si può facilmente intuire, molte di queste operazioni che la CPU deve eseguire, richiedono la presenza implicita o esplicita di appositi dati che le operazioni stesse devono elaborare; l'addizione, ad esempio, richiede la presenza di due addendi, così come il trasferimento dati richiede una sorgente e una destinazione per i dati stessi.
Le entità come: gli addendi di una addizione, i fattori di una moltiplicazione, la sorgente e la destinazione di un trasferimento dati, etc, vengono chiamati, come già sappiamo, operandi; un qualsiasi operando può essere rappresentato da: Nel seguito del capitolo e nei capitoli successivi, per indicare il tipo di operandi coinvolti da una istruzione faremo uso, per comodità, di apposite abbreviazioni.

Un generico operando di tipo registro generale o registro speciale come AX, BL, SI, BP, etc, viene indicato con l'abbreviazione Reg; nel caso in cui sia necessario specificare la dimensione in bit del registro, si usano le abbreviazioni Reg8, Reg16, Reg32, etc. Come già sappiamo, il programmatore può accedere direttamente in lettura o in scrittura ai registri interni della CPU; tutto ciò significa in sostanza che il contenuto di un registro interno della CPU può essere modificato in qualsiasi momento attraverso le istruzioni di un programma.

Un generico operando di tipo registro di segmento come CS, SS, etc, viene indicato con l'abbreviazione SegReg; come già sappiamo, i registri di segmento delle CPU 80x86 sono tutti a 16 bit, per cui il simbolo SegReg equivale a SegReg16. Anche nel caso dei registri di segmento della CPU, il programmatore può accedere ad essi in lettura o in scrittura; in questo caso però la situazione è chiaramente più delicata e verrà analizzata in dettaglio nel seguito del capitolo e nei capitoli successivi.

Un generico operando rappresentato da una locazione di memoria come DS:[BX], SS:[BP], Variabile1, etc, viene indicato con l'abbreviazione Mem; nel caso in cui sia necessario specificare la dimensione in bit della locazione di memoria, si usano le abbreviazioni Mem8, Mem16, Mem32, etc. Un operando di tipo Mem è associato ad una locazione di memoria individuata da un ben preciso indirizzo Seg:Offset (come DS:BX); il programmatore può quindi accedere in lettura o in scrittura a questo indirizzo modificandone il contenuto.

Un generico operando rappresentato da una costante numerica come -121, 12d, 3F2Ch, etc, cioè da un valore numerico immediato, viene indicato con l'abbreviazione Imm; nel caso in cui sia necessario specificare la dimensione in bit del valore immediato, si usano le abbreviazioni Imm8, Imm16, Imm32, etc. Un operando di tipo Imm, essendo un semplice valore numerico, non è associato ad alcuna locazione di memoria e quindi non può essere modificato dal programmatore; come verrà spiegato in dettaglio nel seguito del capitolo, i valori numerici immediati vengono "incorporati" direttamente nel codice macchina dei programmi.

11.1.1 Combinazioni permesse tra gli operandi di una istruzione

Nel caso più generale possibile, una istruzione destinata ad una CPU 80x86 richiede la presenza di due operandi che vengono sempre chiamati sorgente e destinazione; questa terminologia viene quindi usata anche per indicare i due addendi di una addizione, i due fattori di una moltiplicazione, il dividendo e il divisore di una divisione, etc.
Alcune istruzioni vengono elaborate dalla CPU attraverso l'uso di operandi impliciti che non devono essere quindi specificati dal programmatore; nella gran parte dei casi però, il programmatore ha il compito di specificare in modo esplicito l'operando o gli operandi da impiegare con una istruzione.

A seconda del tipo di istruzione da elaborare, le CPU della famiglia 80x86 permettono solamente determinate combinazioni tra operando sorgente e operando destinazione; nel caso, ad esempio, dell'addizione, la CPU somma il contenuto dell'operando sorgente con il contenuto dell'operando destinazione e mette il risultato nello stesso operando destinazione (il cui vecchio contenuto viene quindi sovrascritto). Per l'addizione le CPU 80x86 permettono solamente le seguenti combinazioni tra sorgente e destinazione: Osserviamo subito che l'addizione non può coinvolgere come operandi i registri di segmento; osserviamo inoltre che non è permessa la somma tra Mem e Mem. Questa è una regola generale legata al fatto che le CPU 80x86 richiedono che tutte le operazioni si svolgano sotto la supervisione della CPU stessa; l'unica eccezione è rappresentata dalla operazione di trasferimento dati che può essere effettuata da Mem a Mem attraverso la tecnica del DMA o Direct Memory Access.

Un altro aspetto abbastanza intuitivo, è legato al fatto che in generale, sono proibite tutte le combinazioni prive di senso come, ad esempio, l'uso di un operando di tipo Imm come destinazione; infatti, come è stato detto in precedenza, un operando di tipo Imm non è, né un registro, né una locazione di memoria, per cui la CPU non può accedere ad esso per modificarne il contenuto.

Un'altra importantissima regola generale riguarda il fatto che gli operandi di una istruzione devono essere "compatibili" tra loro in termini di ampiezza in bit; nel caso della addizione, non avrebbe alcun senso pensare di sommare, ad esempio, il contenuto a 16 bit di AX con il contenuto a 8 bit di DL. Una CPU a 16 bit può sommare tra loro due operandi che devono essere, o entrambi a 8 bit, o entrambi a 16 bit; analogamente, una CPU a 32 bit può sommare tra loro due operandi che devono essere, o entrambi a 8 bit, o entrambi a 16 bit, o entrambi a 32 bit.

Per quanto riguarda, invece, il trasferimento dati, questa operazione consiste nel trasferire il contenuto dell'operando sorgente nel contenuto dell'operando destinazione; per questa istruzione (che è quella usata in modo più massiccio nei programmi Assembly), le CPU 80x86 permettono solamente le seguenti combinazioni tra sorgente e destinazione: Come è stato già detto, se vogliamo effettuare un trasferimento da Mem a Mem che coinvolge un blocco dati di grosse dimensioni, possiamo ricorrere alla tecnica del DMA; questo argomento verrà trattato nella sezione Assembly Avanzato.

Nei precedenti capitoli abbiamo già incontrato le due istruzioni PUSH e POP che fanno chiaramente parte della categoria delle istruzioni per il trasferimento dati; abbiamo anche visto che per ciascuna di queste due istruzioni il programmatore deve indicare un solo operando esplicito, in quanto l'altro operando è rappresentato implicitamente da una locazione di memoria che si trova nello stack.
L'istruzione PUSH richiede esplicitamente il solo operando sorgente; l'operando destinazione, infatti, è implicitamente rappresentato da una locazione di memoria dello stack puntata da SS:SP.
L'istruzione POP richiede esplicitamente il solo operando destinazione; l'operando sorgente, infatti, è implicitamente rappresentato da una locazione di memoria dello stack puntata da SS:SP.

Nel caso del trasferimento dati si nota in modo ancora più evidente che non avrebbe alcun senso utilizzare un operando di tipo Imm come destinazione; analogamente, non avrebbe senso pensare di trasferire, ad esempio, il contenuto a 16 bit di CX negli 8 bit di AL.
Nei successivi capitoli verranno analizzate in dettaglio tutte le istruzioni delle CPU 80x86 con le relative combinazioni valide tra operando sorgente e operando destinazione.

11.1.2 Operando di riferimento

Abbiamo appena visto che un operando di una istruzione può essere di tipo Reg, SegReg, Mem o Imm; tralasciando per il momento gli operandi di tipo SegReg che rappresentano casi particolari, bisogna dire che le CPU 80x86, nelle istruzioni privilegiano un eventuale operando di tipo Reg trattandolo come operando di riferimento. Osserviamo subito che ad eccezione delle istruzioni con sorgente Imm e destinazione Mem, in tutti gli altri casi è sicuramente presente almeno un operando di tipo Reg; si possono presentare allora due possibilità: Tutte le istruzioni per le quali l'operando di riferimento è un registro destinazione, vengono definite to register (operazioni verso registro destinazione); tutte le istruzioni per le quali l'operando di riferimento è un registro sorgente, vengono definite from register (operazioni da registro sorgente). Ad esempio, un trasferimento dati da Mem a Reg è una operazione to register; l'operando di riferimento, infatti, è il registro destinazione. Analogamente, una addizione tra sorgente Reg e destinazione Mem è una operazione from register; l'operando di riferimento, infatti, è il registro sorgente.

11.2 Codice macchina delle istruzioni 8086

Come al solito, partiamo dal caso della 8086 che è la CPU di riferimento per le architetture a 16 bit della famiglia 80x86; vediamo quindi come viene effettuata dall'assembler la codifica binaria delle istruzioni destinate a questa CPU.

Abbiamo già visto che i computer equipaggiati con le CPU 80x86 fanno parte della categoria denominata CISC o Complex Instruction Set Computer; su questo tipo di CPU viene utilizzata una complessa tecnica di codifica delle istruzioni, che permette di ottenere un codice macchina estremamente compatto. Ogni istruzione viene convertita in un codice che può occupare uno o più byte; la CPU legge il primo byte e dopo averlo decodificato è in grado di sapere come procedere per acquisire tutte le informazioni necessarie per l'elaborazione dell'istruzione stessa.
In questo capitolo verranno illustrati gli aspetti generali, relativi alla codifica delle istruzioni; chi volesse approfondire l'argomento, può consultare la documentazione tecnica messa a disposizione dalla Intel nel suo sito WEB (vedere a tale proposito le sezioni Siti con argomenti correlati e la sezione Documentazione tecnica di supporto al corso assembly dell’ Area Downloads di questo sito, in particolare, dalla sezione Documentazione tecnica di supporto al corso assembly dell’ Area Downloads di questo sito, si consiglia di scaricare il documento 231455.PDF che reca il titolo 8086 16 BIT HMOS MICROPROCESSOR e il documento 24319101.PDF che reca il titolo Intel Architecture Software Developer's Manual - Volume 2 - Instruction Set Reference; la quasi totalità della documentazione è disponibile in formato PDF (e in lingua inglese).

11.2.1 Struttura generale di una istruzione in codice macchina

La prima cosa da dire riguarda il fatto che ogni istruzione di un programma contiene una serie di elementi (o campi) che al momento dell'esecuzione dovranno essere riconosciuti dalla CPU; lo scopo di questi campi è quello di codificare svariate informazioni che comprendono tra l'altro: il tipo (categoria) di istruzione da eseguire, gli eventuali registri coinvolti, gli indirizzi di memoria degli eventuali dati coinvolti, la modalità di accesso in memoria ai dati stessi e così via. Per fare in modo che la CPU sia in grado di riconoscere questi campi, dobbiamo assegnare a ciascuno di essi un apposito codice binario; combinando opportunamente tra loro questi codici binari, si ottiene il codice macchina dell'istruzione da eseguire.
Prima di tutto bisogna quindi definire la struttura generale che assume una istruzione appartenente al set di istruzioni della CPU 8086; come è stato già detto, ogni istruzione è formata da una serie di campi, ciascuno dei quali deve trovarsi in una posizione ben determinata. La CPU decodifica in sequenza questi campi ricavando da ciascuno di essi le informazioni necessarie per poter procedere con la fase di elaborazione; attraverso la Figura 11.1 possiamo analizzare la struttura generale che assume una istruzione, valida per le CPU 8086. I vari campi di Figura 11.1 sono ordinati dall'alto verso il basso e vengono disposti in sequenza in memoria in modo da formare il codice macchina di una istruzione; analizziamo in dettaglio il significato di ciascun campo.

11.2.2 Campo Prefisso (Instruction Prefix e Segment Override Prefix)

Come si può notare, i primi 2 campi sono dei prefissi opzionali; ciascun prefisso, se è presente, occupa 1 byte. Lo scopo dei prefissi è quello di fornire alla CPU una serie di informazioni relative alle caratteristiche dei successivi campi che formano l'istruzione; come vedremo nel seguito del capitolo, i prefissi svolgono un ruolo fondamentale per ottenere la compatibilità verso il basso tra le CPU della famiglia 80x86. La Figura 11.2 illustra l'elenco completo dei codici macchina (in binario e in esadecimale) di tutti i prefissi disponibili; questi prefissi possono essere disposti in un ordine qualsiasi, ma devono trovarsi rigorosamente all'inizio del codice macchina di una istruzione. L'Instruction Prefix, se è presente, indica alla CPU che l'istruzione da elaborare è preceduta da uno tra i possibili prefissi LOCK, REPNE/REPNZ, REP, REPE/REPZ; questi prefissi vengono usati insieme a particolari istruzioni che saranno analizzate in dettaglio in un capitolo successivo.

Il Segment Override Prefix, se è presente, indica alla CPU che l'istruzione da elaborare contiene un indirizzo di memoria la cui componente Offset deve essere riferita ad un registro di segmento specificato dal prefisso stesso; come già sappiamo, il programmatore deve ricorrere al segment override per aggirare le associazioni predefinite che la CPU effettua tra registri puntatori e registri di segmento. Ogni volta che in una istruzione è presente un segment override, l'assembler inserisce uno dei codici da 1 byte visibili in Figura 11.2; ciò significa che ogni segment override fa crescere di 1 byte le dimensioni del codice macchina del programma!

11.2.3 Campo Opcode

Subito dopo gli eventuali prefissi, troviamo un campo che occupa 1 byte ed è quindi sempre presente nel codice macchina di una istruzione; questo campo viene chiamato Opcode e assume una importanza enorme. Infatti, il campo Opcode contiene al suo interno un valore binario che codifica il tipo di operazione che la CPU deve eseguire; attraverso l'Opcode quindi, possiamo indicare alla CPU se vogliamo che venga eseguita una addizione, una sottrazione, un prodotto, un trasferimento di dati, una chiamata di un sottoprogramma, etc. La Control Logic, in base al codice contenuto nell'Opcode, predispone la ALU o gli altri dispositivi della CPU affinché eseguano il compito richiesto. La Figura 11.3 illustra la struttura interna più frequente del campo Opcode; come vedremo nel seguito del capitolo, in certi casi questo campo può assumere anche altre configurazioni. Nel caso più semplice, una istruzione può essere interamente codificata con i soli 8 bit del campo Opcode; il codice 10011100, ad esempio, è una istruzione completa che indica alla CPU di salvare nello stack il contenuto del registro dei flags (FLAGS). In un caso del genere, la CPU non ha bisogno di ulteriori informazioni in quanto sa già come si deve comportare; in sostanza, quando la CPU incontra l'Opcode 10011100, legge il contenuto a 16 bit del registro FLAGS e lo copia nella locazione di memoria a 16 bit che si trova in cima allo stack ad un indirizzo logico che viene gestito, come sappiamo, attraverso la coppia SS:SP. Possiamo dire allora che in questa istruzione, il registro FLAGS ricopre il ruolo di operando sorgente "implicito", mentre la locazione di memoria puntata da SS:SP ricopre il ruolo di operando destinazione "implicito"; una osservazione molto importante riguarda il fatto che il trasferimento dati appena illustrato si svolge da una sorgente a 16 bit a una destinazione che, come è stato già detto, deve essere ugualmente a 16 bit.

Abbiamo visto però che nel caso generale la situazione è più complessa in quanto la quasi totalità delle istruzioni della CPU richiede uno o due operandi che devono essere esplicitamente indicati dal programmatore; per gestire questa situazione, l'Opcode di Figura 11.3 viene suddiviso in due parti principali.
Nel caso più frequente, i 6 bit più a sinistra (posizioni dalla 2 alla 7) indicati con x, contengono il codice binario vero e proprio del comando da inviare alla CPU; ad esempio, la sequenza 100010b codifica una richiesta di trasferimento dati, la sequenza 000000b codifica l'operazione di somma, la sequenza 111101b codifica l'operazione di complemento a 1 (NOT).
Attraverso questi tre esempi si può osservare appunto che molto spesso le istruzioni prevedono la presenza di operandi espliciti; il trasferimento dati, ad esempio, avviene da una sorgente ad una destinazione. L'addizione prevede la presenza di due addendi; il complemento a 1 si applica ovviamente ad un numero binario.
In base a queste osservazioni, risulta evidente che nel caso in cui sia prevista la presenza di operandi espliciti, bisognerà fornire alla CPU ulteriori informazioni; queste informazioni devono permettere alla CPU di poter ricavare una serie di dettagli relativi, ad esempio, al tipo di operandi (Reg, Mem o Imm), alla loro dimensione in bit, etc.

A proposito della dimensione in bit degli eventuali operandi di una istruzione, bisogna ribadire ancora una volta che non avrebbe senso, ad esempio, sommare un operando a 8 bit con un operando a 16 bit; analogamente, non avrebbe senso trasferire il contenuto del registro AX (16 bit) nel registro BL (8 bit).
Una CPU 8086 con architettura a 16 bit può gestire via hardware operandi a 8 bit o a 16 bit; per indicare alla CPU la dimensione in bit degli operandi, si utilizza il bit w o word bit che nell'Opcode di Figura 11.3 si trova in posizione 0. Se w=0, gli operandi sono a 8 bit; se, invece, w=1, gli operandi sono a 16 bit.

Il bit che nell'Opcode di Figura 11.3 occupa la posizione 1 ed è indicato con la lettera d, rappresenta il cosiddetto direction bit; questo bit viene utilizzato per indicare l'operando di riferimento di cui si è già parlato nella sezione 11.1.
Abbiamo già visto che se una istruzione prevede la presenza di due operandi, entrambi di tipo Reg, allora l'operando di riferimento è per convenzione il registro destinazione; se una istruzione prevede la presenza di due operandi e uno solo di essi è di tipo Reg, allora quel registro viene trattato ovviamente come operando di riferimento. Per indicare alla CPU quale ruolo (sorgente o destinazione) svolge l'operando registro di riferimento, viene appunto utilizzato il bit d; se d=0, l'operando (registro) di riferimento svolge il ruolo di operando sorgente (operazione from register), mentre se d=1, l'operando (registro) di riferimento svolge il ruolo di operando destinazione (operazione to register).
Tutti questi aspetti verranno chiariti meglio attraverso gli esempi pratici presentati più avanti; si tenga anche presente che, come vedremo nel seguito del capitolo, i due bit d e w possono assumere in alcune circostanze un significato differente da quello appena descritto.

11.2.4 Campo mod_reg_r/m

Abbiamo visto che attraverso il campo Opcode, la CPU ricava una serie di informazioni generali sugli operandi di una istruzione; l'Opcode 100010dw, ad esempio, indica un trasferimento dati che coinvolge operandi di tipo Reg o Mem. L'Opcode 1100011w indica un trasferimento dati da sorgente Imm a destinazione Reg o Mem; l'Opcode 10001110 indica un trasferimento dati da sorgente Reg o Mem a destinazione SegReg.
Nel caso di istruzioni che prevedono la presenza di operandi esplicitamente specificati dal programmatore, il codice da 1 byte che rappresenta l'Opcode non è sufficiente per fornire alla CPU tutte le informazioni necessarie per l'elaborazione dell'istruzione stessa; la CPU, infatti, deve anche conoscere il nome di eventuali operandi di tipo Reg, la modalità di accesso ad eventuali operandi di tipo Mem, etc.
In una situazione di questo genere, l'Opcode è seguito da un secondo codice binario da 1 byte che ha proprio lo scopo di fornire alla CPU tutte le informazioni supplementari; in Figura 11.1 questo ulteriore campo viene indicato con mod_reg_r/m.
Riprendendo un esempio precedente, abbiamo visto che l'Opcode per l'addizione tra operandi Reg o Mem vale 000000dw; ponendo d=1 e w=1 otteniamo l'Opcode 00000011 che indica alla CPU che bisogna eseguire una addizione tra due operandi a 16 bit (w=1), con un registro che ricopre il ruolo di operando destinazione e verrà quindi trattato come operando di riferimento (d=1). In un caso del genere, la CPU ha bisogno di sapere qual è il registro da usare come destinazione e se l'operando sorgente è un altro registro oppure una locazione di memoria. Inoltre, se l'operando sorgente è un altro registro, bisogna dire alla CPU di quale registro si tratta; se, invece, l'operando sorgente è una locazione di memoria, bisogna dire alla CPU come avviene l'accesso a questa locazione.
Come è stato già detto, tutte queste informazioni vengono passate alla CPU attraverso il campo mod_reg_r/m; la struttura interna di questo campo viene illustrata dalla Figura 11.4. Il sottocampo reg occupa 3 bit (posizioni 3, 4, 5) e rappresenta il codice binario del registro a cui fa riferimento il direction bit dell'Opcode; con tre bit possiamo rappresentare in totale 23=8 registri differenti. In certi casi, i 3 bit di reg vengono combinati con gli 8 bit del campo Opcode per formare particolari Opcode a 11 bit.
Il sottocampo mod occupa 2 bit (posizioni 6, 7) e può assumere quindi 22=4 valori differenti e cioè: 00b, 01b, 10b, 11b; se mod=11b, allora anche il secondo operando è un registro e il suo codice è rappresentato dai 3 bit del sottocampo r/m (posizioni 0, 1, 2). Se, invece, il sottocampo mod è diverso da 11b, allora il secondo operando è una locazione di memoria o un valore immediato (cioè una costante numerica); se il secondo operando è una locazione di memoria, allora i due bit di mod si combinano con i tre bit di r/m per codificare la modalità di indirizzamento a questo secondo operando. Con 2+3=5 bit possiamo formare 25=32 valori diversi destinati alla codifica del secondo operando; attraverso questi 32 valori possiamo codificare 8 registri (per il caso di mod=11b) più 24 modalità di indirizzamento differenti.

Le considerazioni appena svolte ci indicano chiaramente che il passo successivo che dobbiamo compiere, consiste nel procedere alla codifica binaria dei registri della CPU e delle varie modalità di accesso alla memoria; analizziamo prima di tutto la codifica binaria dei registri generali e speciali che viene illustrata dalla Figura 11.5. Tenendo conto del fatto che gli operandi di una istruzione possono essere, o tutti a 8 bit, o tutti a 16 bit, vengono utilizzati gli stessi 8 codici per rappresentare, sia gli 8 possibili registri a 16 bit, sia gli 8 possibili half registers a 8 bit; come già sappiamo, grazie al word bit, la CPU è in grado di sapere se i codici di Figura 11.5 si riferiscono ad un registro a 8 bit (w=0), oppure ad un registro a 16 bit (w=1).

In presenza di un registro di segmento tra gli operandi di una istruzione, vengono impiegati particolari Opcode; abbiamo visto in precedenza che nel caso, ad esempio, del trasferimento dati, l'Opcode 10001110 indica alla CPU che l'operando destinazione è di tipo SegReg. La codifica dei registri di segmento viene effettuata con soli 2 bit come viene illustrato dalla Figura 11.6; il codice a 2 bit di un SegReg viene sistemato sempre nei bit in posizione 3 e 4 del sottocampo reg di Figura 114, con il bit in posizione 5 che deve valere 0. Grazie alla Figura 11.6, possiamo anche capire come vengono costruiti i codici relativi ai segment override prefix di Figura 11.2; la struttura di questi prefissi, infatti, è rappresentata dagli 8 bit 001_SegReg_110, dove SegReg è uno dei possibili codici a 2 bit visibili in Figura 11.6.

Riassumendo quindi, se una istruzione prevede un operando di riferimento di tipo Reg, allora il codice macchina a 3 bit di questo registro (ricavato dalla Figura 11.5) viene inserito nel sottocampo reg di Figura 11.4; inoltre, se mod=11b, anche il secondo operando è un registro e il suo codice macchina a 3 bit (ricavato dalla Figura 11.5) viene inserito nel sottocampo r/m di Figura 11.4. In questo caso, per il secondo operando si presentano le 8 possibilità mostrate in Figura 11.7. Se, invece, il sottocampo mod è diverso da 11b, allora il secondo operando è di tipo Mem o Imm; la presenza di un secondo operando di tipo Imm viene indicata come al solito dall'Opcode. Abbiamo visto, infatti, che ad esempio, l'Opcode 1100011w indica un trasferimento dati da sorgente Imm a destinazione Reg o Mem; se, invece, il secondo operando è di tipo Mem, bisogna indicare alla CPU il metodo utilizzato per accedere alla corrispondente locazione di memoria. La CPU 8086 fornisce, come è stato già detto, ben 24 modalità di indirizzamento differenti per accedere ad un operando di tipo Mem; tutte queste modalità indicano naturalmente il metodo di accesso al contenuto di una locazione di memoria individuata da un indirizzo logico del tipo Seg:Offset.
Nel precedente capitolo abbiamo già incontrato alcuni semplicissimi esempi relativi al modo con cui si accede ad una locazione di memoria; abbiamo visto esempi del tipo DS:[BX], SS:[BP], Variabile1, etc. La CPU 8086 permette però di rappresentare la componente Offset di un indirizzo logico, in modo molto più articolato; la Figura 11.8 illustra tutte le 24 modalità di indirizzamento disponibili. In Figura 11.8, la struttura che può assumere la componente Offset di un indirizzo logico viene chiamata Effective Address (indirizzo effettivo); i termini Disp8 e Disp16 indicano un offset rappresentato da un valore esplicito rispettivamente a 8 e a 16 bit. Per ridurre al minimo le dimensioni del codice macchina, l'assembler utilizza, quando è possibile, solo 8 bit per rappresentare una componente Disp; questa situazione si verifica quando Disp è compreso tra 0 e +127 (cioè tra 0000h e 007Fh).

Osserviamo subito che nel caso più semplice, la componente Offset dell'indirizzo di memoria a cui vogliamo accedere viene espressa attraverso un singolo registro puntatore come BX, SI, etc; possiamo notare che quando mod=00b, non è presente alcun Disp da sommare ad un registro puntatore.

Se vogliamo esprimere in modo più avanzato una componente Offset, possiamo servirci di due registri puntatori; in questo caso la componente Offset è data dalla somma degli offset contenuti nei due registri puntatori.
In una situazione di questo genere, il primo registro (quello più a sinistra) viene chiamato registro base, mentre il secondo registro viene chiamato registro indice; nel caso, ad esempio, di [BX+SI], il registro BX è la base, mentre il registro SI è l'indice.
In modalità reale 8086 solamente BX e BP possono svolgere il ruolo di registro base (da cui il nome base register per BX e base pointer register per BP); solamente SI e DI possono svolgere il ruolo di registri indice (da cui il nome di index registers per SI e DI).
È proibito usare BX e BP in coppia; non possiamo scrivere quindi [BX+BP] o [BP+BX]. Il programmatore può accedere in lettura o in scrittura al registro SP, ma non può dereferenziarlo (non si può scrivere cioè [SP]); non è possibile quindi utilizzare SP nelle combinazioni visibili in Figura 11.8 per esprimere una componente Offset.

Come si può notare dalla Figura 11.8, nel caso più complesso la componente Offset di un indirizzo logico può essere espressa con due registri puntatori più un ulteriore spiazzamento rappresentato da un offset esplicito a 8 bit (Disp8) o 16 bit (Disp16); in questo caso la componente Offset è data dalla somma dei contenuti dei due registri puntatori e dello spiazzamento. Queste tre componenti dell'offset vengono chiamate base, indice e spiazzamento; nel caso, ad esempio, di una componente Offset espressa dalla terna [BX+SI+03F2h], il registro BX è la base, il registro SI è l'indice, mentre 03F2h è lo spiazzamento.

Nella colonna SegReg predefinito di Figura 11.8, vengono riportati i registri di segmento che la CPU, in assenza di segment override, associa automaticamente alla componente Offset della colonna Effective Address; come si può notare, in assenza di BP il SegReg predefinito è sempre DS. In presenza, invece, di BP, il SegReg predefinito è sempre SS; per aggirare le associazioni predefinite visibili in Figura 11.8, il programmatore deve ricorrere come al solito al segment override esplicito scrivendo, ad esempio, SS:[BX+DI+002Eh].

11.2.5 Campo Displacement

Ogni volta che in un Effective Address è presente una componente Disp, questa componente viene sistemata nel campo Displacement visibile in Figura 11.1; come si può notare dalla stessa Figura 11.1, il campo Displacement può essere formato da 0 byte (nessuna componente Disp), 1 byte (Disp8) o 2 byte (Disp16).

11.2.6 Campo Immediate

Se l'operando sorgente di una istruzione è di tipo Imm, allora il valore immediato viene sistemato nel campo Immediate visibile in Figura 11.1; questo campo, se è presente, è sempre l'ultimo nel codice macchina di una istruzione. Nel caso di operandi a 8 bit (w=0), il campo Immediate occupa 1 byte; nel caso, invece, di operandi a 16 bit (w=1), il campo Immediate occupa 2 byte.

11.3 Esempi pratici di codici macchina per la CPU 8086

Attraverso le tabelle esposte nella sezione 11.2, siamo in grado di determinare il codice macchina di qualsiasi istruzione 8086, dalla più semplice alla più complessa; a tale proposito, dobbiamo munirci di un documento, come il già citato 231455.PDF (8086 16 BIT HMOS MICROPROCESSOR), che illustra la struttura dei campi Opcode e mod_reg_r/m di tutte le istruzioni appartenenti al set della CPU 8086.
In questo capitolo analizzeremo solo alcune semplici istruzioni che vengono largamente utilizzate nei programmi Assembly; nei successivi capitoli verranno analizzati i codici macchina che si riferiscono a numerose altre istruzioni delle CPU 80x86.

11.3.1 Addizione tra operandi di tipo Reg/Mem

L'Opcode 000000dw indica alla CPU che deve essere eseguita una addizione tra operandi di tipo Reg/Mem; come già sappiamo, sono permesse tutte le combinazioni tra Reg e Mem, ad eccezione della somma tra Mem e Mem.
Se la somma si svolge tra Reg e Reg, è previsto anche l'Opcode 00000000 per i Reg8 e 00000001 per i Reg16 e Reg32; in tal caso, nel campo mod_reg_r/m si pone: mod=11b, reg=sorgente e r/m=destinazione.
L'assembler NASM utilizza proprio gli Opcode 00000000 e 00000001; viceversa, MASM utilizza l'Opcode 000000dw.

Il caso più semplice si presenta quando entrambi gli operandi sono di tipo Reg; supponiamo a tale proposito di voler effettuare una somma tra sorgente AL e destinazione BL.
Osserviamo subito che gli operandi sono a 8 bit, per cui w=0; per convenzione, l'operando di riferimento è il registro destinazione, per cui d=1. Ricaviamo quindi:
Opcode = 00000010b = 02h
Dalla Figura 11.5 ricaviamo il codice macchina dell'operando di riferimento BL=011b; questo codice va inserito nel sottocampo reg di Figura 11.4.
Anche il secondo operando (sorgente) è un registro (AL), per cui mod=11b; dalla Figura 11.5 ricaviamo il codice macchina del secondo operando AL=000b, che deve essere inserito nel sottocampo r/m di Figura 11.4. Ricaviamo quindi:
mod_reg_r/m = 11011000b = D8h
L'istruzione non ha bisogno di altre informazioni, per cui il suo codice macchina generato dall'assembler è:
00000010b 11011000b = 02h D8h
Quando la CPU incontra questo codice macchina, somma il contenuto di AL con il contenuto di BL e mette il risultato in BL.

Passiamo alla somma tra sorgente AX e destinazione BX; nell'Opcode l'unico cambiamento riguarda w=1 per indicare che gli operandi sono a 16 bit. Ricaviamo quindi:
Opcode = 00000011b = 03h
In Figura 11.5 notiamo che i codici macchina di AX e BX sono gli stessi di AL e BL, per cui il campo mod_reg_r/m rimane invariato; ricaviamo quindi:
mod_reg_r/m = 11011000b = D8h
L'istruzione non ha bisogno di altre informazioni, per cui il suo codice macchina generato dall'assembler è:
00000011b 11011000b = 03h D8h
Quando la CPU incontra questo codice macchina, somma il contenuto di AX con il contenuto di BX e mette il risultato in BX.

Scambiando di posto AX e BX, l'Opcode rimane invariato in quanto si tratta sempre di una somma to register con operandi a 16 bit; ricaviamo quindi:
Opcode = 00000011b = 03h
Nel campo mod_reg_r/m bisogna naturalmente scambiare di posto i due codici presenti in reg e r/m; ricaviamo quindi:
mod_reg_r/m = 11000011b = C3h
L'istruzione non ha bisogno di altre informazioni, per cui il suo codice macchina generato dall'assembler è:
00000011b 11000011b = 03h C3h
Quando la CPU incontra questo codice macchina, somma il contenuto di AX con il contenuto di BX e mette il risultato in AX.

Vediamo ora quello che succede quando uno degli operandi è di tipo Mem; a tale proposito, facciamo riferimento ad un dato a 16 bit che si trova all'offset 002Ah del Data Segment di un programma.
Supponiamo di voler effettuare una somma tra sorgente Mem e destinazione SI; grazie alla presenza del registro SI, l'assembler rileva che gli operandi sono a 16 bit, per cui w=1. L'operando di riferimento è il registro destinazione, per cui d=1; ricaviamo quindi:
Opcode = 00000011b = 03h
Dalla Figura 11.5 ricaviamo il codice macchina di SI=110b; questo codice viene inserito nel sottocampo reg di Figura 11.4.
Per quanto riguarda l'operando sorgente, supponiamo di volerlo gestire come Disp16=002Ah; in questo caso, dalla Figura 11.8 ricaviamo mod=00b e r/m=110b. Se utilizziamo DS per gestire il Data Segment, dobbiamo ricordarci di esprimere l'operando sorgente come DS:002Ah, oppure DS:[002Ah], oppure [DS:002Ah]; in caso contrario l'assembler crede che vogliamo sommare SI con 002Ah!
Per il campo mod_reg_r/m otteniamo quindi:
mod_reg_r/m = 00110110b = 36h
L'istruzione necessita di una ulteriore informazione che riguarda lo spiazzamento 002Ah da inserire nel campo Displacement di Figura 11.1; questo valore, pur non superando 007Fh, non viene convertito dall'assembler in 2Ah in quanto, come si vede in Figura 11.8, un semplice Disp deve essere obbligatoriamente a 16 bit.
Siccome DS è il SegReg naturale per il blocco dati di un programma, l'assembler non inserisce alcun segment override prefix; l'istruzione non ha bisogno di altre informazioni, per cui il suo codice macchina generato dall'assembler è:
00000011b 00110110b 0000000000101010b = 03h 36h 002Ah
Quando la CPU incontra questo codice macchina, somma il contenuto di SI con il contenuto della locazione che si trova all'indirizzo DS:002Ah e mette il risultato in SI; è chiaro che prima di far eseguire questa istruzione, il programmatore deve ricordarsi di inizializzare DS con la componente Seg dell'indirizzo iniziale del Data Segment!

Anziché utilizzare l'offset esplicito 002Ah, con il rischio di commettere qualche svista, possiamo etichettare questo stesso offset con un nome simbolico come Variabile1; in questo caso dobbiamo esprimere l'operando di tipo Mem con il simbolo DS:Variabile1, oppure DS:[Variabile1], oppure [DS:Variabile1]. Il codice macchina che si ottiene è ovviamente identico; il vantaggio dell'uso di un nome simbolico sta nel fatto che il calcolo del relativo offset viene delegato all'assembler il quale è infallibile nello svolgere questo compito.

Cosa succede se il nostro operando sorgente di tipo Mem si trova in un segmento di programma gestito con CS?
In questo caso l'operando sorgente deve essere espresso come CS:[002Ah] (o CS:[Variabile1]); siccome CS non è il SegReg naturale per il Data Segment di un programma, l'assembler inserisce all'inizio del codice macchina il segment override prefix 00101110b=2Eh relativo allo stesso CS. Il restante codice macchina rimane invariato, per cui si ottiene:
00101110b 00000011b 00110110b 0000000000101010b = 2Eh 03h 36h 002Ah
Osserviamo che a causa del segment override prefix, il codice macchina è cresciuto di un byte; in assenza di questo prefisso, la CPU accede a DS:002Ah anziché a CS:002Ah!

Passiamo ora a qualcosa di più impegnativo; vogliamo accedere alla locazione di memoria dei precedenti esempi attraverso il registro base BX, il registro indice DI e uno spiazzamento Disp8. Ponendo allora BX=0010h, DI=0010h e Disp8=0Ah, otteniamo:
BX + DI + Disp8 = 0010h + 0010h + 0Ah = 002Ah
che è l'offset della locazione di memoria dei precedenti esempi.
Supponiamo ora di voler effettuare una somma tra sorgente CX e destinazione [BX+DI+0Ah]; come al solito, grazie alla presenza del registro CX, l'assembler rileva che gli operandi sono a 16 bit (w=1). L'operando di riferimento è il registro CX sorgente, per cui d=0; ricaviamo quindi:
Opcode = 00000001b = 01h
Dalla Figura 11.5 ricaviamo il codice macchina di CX=001b; questo codice viene inserito nel sottocampo reg di Figura 11.4.
Per l'operando destinazione [BX+DI+Disp8], dalla Figura 11.8 ricaviamo mod=01b, r/m=001b; il campo mod_reg_r/m è quindi:
mod_reg_r/m = 01001001b = 49h
L'istruzione necessita inoltre del contenuto di Disp8=0Ah; questo valore viene inserito nel campo Displacement di Figura 11.1.
In assenza di segment override e in presenza di BX come registro base, il SegReg predefinito è DS, per cui l'assembler non inserisce alcun segment override prefix; è importante osservare che in questo caso, il segment override prefix non viene inserito dall'assembler nemmeno se scriviamo in modo esplicito DS:[BX+DI+0Ah] (infatti, DS è il SegReg naturale per il blocco dati, per cui la sua presenza è superflua).
L'istruzione non ha bisogno di altre informazioni, per cui il suo codice macchina generato dall'assembler è:
00000001b 01001001b 00001010b = 01h 49h 0Ah
Quando la CPU incontra questo codice macchina, somma il contenuto di CX con il contenuto della locazione che si trova all'indirizzo DS:002Ah e mette il risultato nella stessa locazione DS:002Ah.

Se al posto di BX usiamo BP, allora mod=01b e r/m=011b; il campo mod_reg_r/m diventa:
mod_reg_r/m = 01001011b = 4Bh
In assenza di segment override e in presenza di BP come registro base, il SegReg predefinito è SS per cui l'assembler non inserisce alcun segment override prefix; come al solito, il segment override prefix non viene inserito dall'assembler nemmeno se scriviamo in modo esplicito SS:[BP+DI+0Ah] (infatti, SS è il SegReg naturale per il blocco stack, per cui la sua presenza è superflua).
Tutto il resto rimane invariato, per cui il codice macchina generato dall'assembler è:
00000001b 01001011b 00001010b = 01h 4Bh 0Ah
Quando la CPU incontra questo codice macchina, somma il contenuto di CX con il contenuto della locazione che si trova all'indirizzo SS:002Ah e mette il risultato nella stessa locazione SS:002Ah.

Se, invece, l'operando destinazione è DS:[BP+DI+0Ah], allora l'assembler inserisce il segment override prefix 00111110b=3Eh relativo a DS; in caso contrario, l'offset [BP+DI+0Ah] verrebbe riferito a SS a causa della associazione predefinita tra BP e SS. Tutto il resto rimane invariato, per cui il codice macchina generato dall'assembler è:
00111110b 00000001b 01001011b 00001010b = 3Eh 01h 4Bh 0Ah
Quando la CPU incontra questo codice macchina, somma il contenuto di CX con il contenuto della locazione che si trova all'indirizzo DS:002Ah e mette il risultato nella stessa locazione DS:002Ah.

11.3.2 Addizione tra sorgente Imm e destinazione Reg/Mem

L'Opcode 100000sw indica alla CPU che deve essere eseguita una addizione tra sorgente Imm e destinazione Reg/Mem; in questo caso, il campo mod_reg_r/m assume la forma mod_000_r/m. I 3 bit del sottocampo reg valgono quindi 000b e si combinano con gli 8 bit del campo Opcode per formare il codice a 11 bit 100000sw000; questo codice dice appunto alla CPU che l'operando sorgente è di tipo Imm e l'operando destinazione è di tipo Reg/Mem ed è codificato dai due sottocampi mod e r/m.
Notiamo subito che nell'Opcode, il direction bit viene sostituito dal cosiddetto sign bit (bit di segno) indicato con s; per capire il perché di questa situazione, basta osservare innanzi tutto che in un caso di questo genere il bit d è del tutto superfluo in quanto l'operando di tipo Imm non può che essere la sorgente.
Il bit d viene allora sostituito con il bit s che in combinazione con il bit w indica alla CPU se l'operando Imm deve essere esteso da 8 a 16 bit con conseguente estensione del bit di segno; naturalmente, l'estensione del bit di segno viene effettuata attraverso le regole già esposte in un precedente capitolo. Si possono presentare i seguenti casi: Questo accorgimento apparentemente contorto, permette all'assembler di ridurre le dimensioni del codice macchina; vediamo subito alcuni esempi che chiariscono questo aspetto.

Supponiamo di voler effettuare una somma tra sorgente Imm=+3 e destinazione DH; grazie alla presenza di DH l'assembler rileva che gli operandi sono a 8 bit, per cui w=0. L'operando di tipo Imm può essere espresso con soli 8 bit (00000011b), per cui non deve subire alcuna estensione del bit di segno (s=0); otteniamo quindi:
Opcode = 10000000b = 80h
Come è stato già detto, l'operando destinazione viene codificato attraverso i sottocampi mod e r/m; in questo caso, la destinazione è il registro DH=110b, per cui mod=11b e r/m=110b. Otteniamo quindi:
mod_reg_r/m = 11000110b = C6h
L'istruzione necessita di una ulteriore informazione che riguarda il valore immediato +3 (cioè 00000011b=03h) da inserire nel campo Immediate di Figura 11.1; in definitiva, il codice macchina generato dall'assembler è:
10000000b 11000110b 00000011b = 80h C6h 03h
Quando la CPU incontra questo codice macchina, somma 00000011b con il contenuto di DH e mette il risultato in DH; l'esempio appena illustrato ci permette di constatare che i valori immediati vengono "incorporati" direttamente nel codice macchina del programma.

Rispetto al precedente esempio sostituiamo ora DH con DX; grazie alla presenza di DX l'assembler rileva che gli operandi sono a 16 bit, per cui w=1. L'operando di tipo Imm può essere espresso con soli 8 bit (00000011b), per cui deve subire l'estensione del bit di segno (s=1); otteniamo quindi:
Opcode = 10000011b = 83h
Dalla Figura 11.5 si ricava DX=010b, per cui mod=11b e r/m=010b; otteniamo quindi:
mod_reg_r/m = 11000010b = C2h
L'istruzione necessita di una ulteriore informazione che riguarda il valore immediato +3 (cioè 00000011b=03h) da inserire nel campo Immediate di Figura 11.1; in definitiva, il codice macchina generato dall'assembler è:
10000011b 11000010b 00000011b = 83h C2h 03h
Quando la CPU incontra questo codice macchina, estende a 16 bit 00000011b ottenendo 0000000000000011b che è appunto la rappresentazione a 16 bit in complemento a 2 di +3; successivamente somma questo valore immediato a DX e mette il risultato in DX.
Possiamo constatare quindi che con questo accorgimento, l'assembler ha risparmiato 1 byte di codice macchina delegando alla CPU il compito di estendere l'operando Imm da 8 a 16 bit.

Tenendo sempre DX come destinazione, poniamo ora Imm=-3; grazie alla presenza di DX l'assembler rileva che gli operandi sono a 16 bit, per cui w=1. L'operando di tipo Imm può essere espresso con soli 8 bit (11111101b), per cui deve subire l'estensione del bit di segno (s=1); il campo Opcode e il campo mod_reg_r/m restano inalterati rispetto all'esempio precedente.
L'istruzione necessita di una ulteriore informazione che riguarda il valore immediato -3 (cioè 11111101b=FDh) da inserire nel campo Immediate di Figura 11.1; in definitiva, il codice macchina generato dall'assembler è:
10000011b 11000010b 11111101b = 83h C2h FDh
Quando la CPU incontra questo codice macchina, estende a 16 bit 11111101b ottenendo 1111111111111101b che è appunto la rappresentazione a 16 bit in complemento a 2 di -3; successivamente somma questo valore immediato a DX e mette il risultato in DX.

Tenendo sempre DX come destinazione, poniamo ora Imm=-1500; grazie alla presenza di DX l'assembler rileva che gli operandi sono a 16 bit, per cui w=1. L'operando di tipo Imm richiede almeno 16 bit (1111101000100100b), per cui non deve subire alcuna estensione del bit di segno (s=0); otteniamo quindi:
Opcode = 10000001b = 81h
Il campo mod_reg_r/m rimane inalterato.
L'istruzione necessita di una ulteriore informazione che riguarda il valore immediato -1500 (cioè 1111101000100100b=FA24h) da inserire nel campo Immediate di Figura 11.1; in definitiva, il codice macchina generato dall'assembler è:
10000001b 11000010b 1111101000100100b = 81h C2h FA24h
Quando la CPU incontra questo codice macchina, somma il valore immediato FA24h a DX e mette il risultato in DX.

Particolare interesse riveste il caso in cui la sorgente è di tipo Imm e la destinazione è di tipo Mem; supponiamo allora di voler sommare la sorgente Imm=+3800, con il contenuto di una locazione di memoria che si trova all'offset 00F8h del Data Segment di un programma.
Se esprimiamo la destinazione come DS:[00F8h], possiamo subito constatare che in questo caso l'assembler non è in grado di ricavare l'ampiezza in bit degli operandi; infatti, +3800 è un semplice numero, mentre DS:[00F8h] è un semplice indirizzo di memoria. In una situazione del genere, l'assembler produce un messaggio di errore del tipo:
Instruction operand must have size
oppure:
Argument needs type override
Con questo messaggio l'assembler ci sta dicendo che almeno uno degli operandi deve specificare la sua ampiezza in bit!
Per fornire all'assembler questa informazione, possiamo ricorrere a diversi metodi; il metodo più diretto consiste nell'utilizzare gli operatori mostrati in Figura 11.9. Questi operatori, applicati ad operandi di tipo Mem, permettono al programmatore di indicare in modo esplicito l'ampiezza in bit della locazione di memoria a cui si vuole accedere; applicati, invece, ad operandi di tipo Imm, permettono al programmatore di indicare in modo esplicito l'ampiezza in bit del valore immediato.
Tornando al nostro esempio, se vogliamo eseguire una somma tra operandi a 16 bit, dobbiamo esprimere la destinazione come:
WORD PTR DS:[00F8h]
Grazie a questa informazione l'assembler pone w=1; siccome +3800 richiede almeno 16 bit (0000111011011000b), l'assembler pone anche s=0, per cui si ottiene:
Opcode = 10000001b = 81h
L'operando destinazione è di tipo Disp16, per cui dalla Figura 11.8 ricaviamo mod=00b e r/m=110b; si ottiene quindi:
mod_reg_r/m = 00000110b = 06h
Subito dopo il campo mod_reg_r/m è presente il campo Displacement nel quale viene inserito l'offset 00F8h della locazione di memoria; l'istruzione necessita di una ulteriore informazione che riguarda il valore immediato +3800 (cioè 0000111011011000b=0ED8h) da inserire nel campo Immediate di Figura 11.1. Non essendo presente alcun segment override, il codice macchina generato dall'assembler è:
10000001b 00000110b 0000000011111000b 0000111011011000b = 81h 06h 00F8h 0ED8h
Quando la CPU incontra questo codice macchina, somma il valore immediato 0ED8h al contenuto a 16 bit della locazione di memoria DS:00F8h e mette il risultato nella stessa locazione DS:00F8h.

Supponiamo ora di esprimere l'operando destinazione come:
WORD PTR ES:[00F8h]
Rispetto all'esempio precedente, l'unico cambiamento è dato dalla presenza del segment override ES; l'assembler inserisce quindi il prefisso 00100110b=26h relativo allo stesso ES e ottiene il seguente gigantesco codice macchina:
00100110b 10000001b 00000110b 0000000011111000b 0000111011011000b = 26h 81h 06h 00F8h 0ED8h
Quando la CPU incontra questo codice macchina, somma il valore immediato 0ED8h al contenuto a 16 bit della locazione di memoria ES:00F8h e mette il risultato nella stessa locazione ES:00F8h.
Naturalmente, le considerazioni appena esposte si possono estendere con estrema facilità al caso di operandi di tipo Mem espressi, ad esempio, come:
WORD PTR CS:[BX+SI+0CF2h]
Confrontiamo ora l'enorme codice macchina dell'ultimo esempio, con il codice macchina:
00000011b 11011000b = 03h D8h
relativo all'esempio della somma tra sorgente AX e destinazione BX.
Nel caso della somma tra registri, il codice macchina è formato da 2 soli byte, che vengono decodificati molto rapidamente dalla CPU; a tutto ciò bisogna aggiungere il fatto che la velocità di accesso agli operandi di tipo Reg è elevatissima.
Nel caso, invece, dell'ultimo esempio, relativo alla somma tra Imm e Mem con tanto di segment override, otteniamo un codice macchina da ben 7 byte che richiede un tempo di decodifica più alto; in più bisogna anche tenere conto del tempo necessario alla CPU per accedere in memoria all'operando di tipo Mem.
Proprio per questo motivo, nei limiti del possibile il programmatore deve sempre cercare di privilegiare l'utilizzo nelle istruzioni di operandi di tipo Reg; in ogni caso, con le moderne CPU gli accessi in memoria avvengono sempre più velocemente grazie all'uso di accorgimenti come la cache memory, la prefetch queue, la pipeline, etc.
Con le vecchie CPU come la 8086, invece, gli accessi in memoria per l'elaborazione dei dati comportavano una grave perdita di tempo; tanto è vero che nei manuali di questa CPU è sempre presente una tabella come quella mostrata in Figura 11.10: La colonna EA Clocks contiene i cicli di clock che bisogna sommare a quelli necessari alla 8086 per eseguire una istruzione che contiene uno degli effective address della Figura 11.10; come si può notare, nel caso della presenza di registro base, registro indice e spiazzamento, si può arrivare ad un aggravio pari a 12 cicli di clock!

11.3.3 Operandi di tipo SegReg

Gli esempi illustrati in precedenza, coprono la quasi totalità dei casi che si possono presentare in relazione al tipo di operandi, alle modalità di indirizzamento delle locazioni di memoria, etc; per analizzare altre situazioni interessanti, vediamo anche qualche esempio relativo al trasferimento dati che è sicuramente l'istruzione utilizzata in modo più massiccio nei programmi Assembly.
Cominciamo con un trasferimento dati da Reg/Mem a SegReg, che ci permette di analizzare il sistema di codifica degli operandi di tipo SegReg; come già sappiamo, gli operandi di tipo SegReg non possono essere utilizzati con l'addizione o con qualsiasi altra operazione logico aritmetica. Nel caso del trasferimento dati, solo uno dei due operandi può essere eventualmente di tipo SegReg; è anche proibito il trasferimento dati da Imm a SegReg.

Nel caso del trasferimento dati da Reg/Mem a SegReg, l'Opcode è 10001110b=8Eh; il campo mod_reg_r/m, inoltre, assume la forma mod_0_SegReg_r/m.
Per quanto riguarda il campo Opcode notiamo che in questo caso d=1 per indicare che l'operando di riferimento è il SegReg di destinazione; il word bit non è necessario (w=0) in quanto gli operandi devono essere necessariamente a 16 bit.
Per quanto riguarda il campo mod_reg_r/m notiamo che il sottocampo reg diventa 0_SegReg; ovviamente SegReg è uno dei codici da 2 bit visibili in Figura 11.6.

Supponiamo di voler trasferire in ES il contenuto del registro DX; in questo caso, dalla Figura 11.6 ricaviamo SegReg=00b. La sorgente è il registro DX=010b, per cui mod=11b e r/m=010b; otteniamo quindi:
mod_reg_r/m = 11000010b = C2h
L'istruzione non necessita di altre informazioni, per cui il codice macchina generato dall'assembler è:
10001110b 11000010b = 8Eh C2h
Quando la CPU incontra questo codice macchina, copia in ES il contenuto di DX.

Supponiamo di voler trasferire in SS il contenuto della locazione di memoria che si trova all'indirizzo CS:(BX+SI+03F8h); in questo caso, dalla Figura 11.6 ricaviamo SegReg=10b. La sorgente è di tipo [BX+SI+Disp16], per cui dalla Figura 11.8 ricaviamo mod=10b e r/m=000b; otteniamo quindi:
mod_reg_r/m = 10010000b = 90h
L'istruzione richiede anche il valore Disp16 pari a 03F8h da inserire nel campo Displacement di Figura 1; l'assembler aggiunge inoltre il prefisso 00101110b=2Eh per CS e genera il codice macchina:
00101110b 10001110b 10010000b 0000001111111000b = 2Eh 8Eh 90h 03F8h
Quando la CPU incontra questo codice macchina, copia in SS il contenuto a 16 bit della locazione di memoria che si trova all'indirizzo CS:(BX+SI+03F8h).

11.3.4 Espressioni costanti

L'Assembly fornisce una serie di operatori che permettono di definire valori numerici di tipo Displacement o Immediate attraverso complesse espressioni matematiche; per definire, ad esempio, il valore numerico +350, possiamo scrivere:
2 + 80 + (100 * 2) + (60 / 3) + (60 - 12)
Questa sequenza (volutamente contorta) di operazioni matematiche, ci permette di analizzare le caratteristiche che devono avere queste espressioni; come si può notare, gli operandi devono essere tutti valori numerici immediati. La divisione (come 60/3) è intesa come divisione intera con eventuale troncamento della parte frazionaria del quoziente; il quoziente quindi sarà esatto solo quando il dividendo è un multiplo intero del divisore.
Queste espressioni richiedono tali caratteristiche in quanto devono essere valutate e risolte dall'assembler in fase di generazione del codice macchina; nel nostro caso, la precedente espressione viene risolta dall'assembler che ottiene il risultato 350. Gli operatori matematici che l'Assembly ci mette a disposizione, si rivelano particolarmente utili anche per gestire i dati di un programma attraverso sofisticati metodi di indirizzamento; supponiamo a tale proposito che nel Data Segment di un programma sia presente una sequenza consecutiva e contigua di 10 dati di tipo WORD, con la prima WORD che si trova all'offset 002Ah.
Di conseguenza, la seconda WORD viene a trovarsi all'offset 002Ch, la terza a 002Eh, la quarta a 0030h e così via, sino alla decima WORD che viene a trovarsi all'offset 003Ch.
Anziché assegnare un nome simbolico a ciascun dato, possiamo etichettare solo il primo di essi attraverso, ad esempio, il nome VettWord; questo nome rappresenta quindi la WORD che si trova all'offset 002Ah. A questo punto, utilizzando gli operatori matematici dell'Assembly, possiamo affermare che: Infatti, VettWord+0 rappresenta l'offset:
002Ah + 0000h = 002Ah
VettWord+2 rappresenta l'offset:
002Ah + 0002h = 002Ch
VettWord+4 rappresenta l'offset:
002Ah + 0004h = 002Eh
e così via, sino a VettWord+18 (cioè VettWord+12h) che rappresenta l'offset:
002Ah + 0012h = 003Ch
(è anche permessa la sintassi DS:VettWord[0], DS:VettWord[2], DS:VettWord[4], etc).

In sostanza, un simbolo come [VettWord+Imm] (o VettWord[Imm]) non è altro che un banalissimo Disp16; infatti, rappresenta l'offset che si ottiene dalla somma tra Imm e l'offset di VettWord!
Nel caso generale, possiamo esprimere un Disp16 anche con espressioni complesse del tipo:
[VettWord + ((12 + 5) * 2) - ((6 + 4) / 5)]
Vediamo allora ciò che succede se vogliamo trasferire in BX la WORD rappresentata dal simbolo DS:VettWord[4]; l'Opcode per il trasferimento dati da Reg/Mem a Reg/Mem è 100010dw.
Il registro destinazione BX è l'operando di riferimento, per cui d=1; siccome gli operandi sono a 16 bit (w=1), otteniamo:
Opcode = 10001011b = 8Bh
Dalla Figura 11.5 ricaviamo il codice BX=011b da inserire nel sottocampo reg di Figura 11.4; per quanto riguarda l'operando sorgente, che è di tipo Mem, l'assembler calcola:
002Ah + 0004h = 002Eh
Si tratta in sostanza di un normalissimo Disp16, per cui dalla Figura 11.8 si ricava mod=00b, r/m=110b; otteniamo allora:
mod_reg_r/m = 00011110b = 1Eh
Dopo il campo mod_reg_r/m è presente anche il campo Displacement che contiene l'offset 002Eh; non è necessario il segment override prefix, per cui l'assembler genera il codice macchina:
10001011b 00011110b 0000000000101110b = 8Bh 1Eh 002Eh
Quando la CPU incontra questo codice macchina, copia in BX la WORD presente nella locazione di memoria che si trova all'indirizzo DS:002Eh.

11.4 Codice macchina delle istruzioni 80386

Passiamo ora al caso della 80386 che è la CPU di riferimento per le architetture a 32 bit della famiglia 80x86; tutte le considerazioni che seguono sono quindi valide anche per le CPU di classe superiore.
Da questo momento in poi è necessario distinguere tra la modalità reale "pura" delle CPU 8086 e l'emulazione della modalità reale 8086 da parte delle CPU 80386 e superiori; questa distinzione è necessaria in quanto, come è stato detto nei precedenti capitoli, un programma che gira in modalità reale sulle CPU 80386 o superiori, può sfruttare numerose novità architetturali introdotte a partire dai microprocessori a 32 bit. Abbiamo già visto che queste novità riguardano, in particolare: i registri (e quindi anche gli operandi) a 32 bit, i nuovi registri di segmento FS e GS, etc; appare ovvio il fatto che un programma destinato a girare su una vera CPU 8086, non può assolutamente utilizzare queste nuove caratteristiche.
Proprio per i motivi appena elencati, nel seguito del capitolo si utilizzerà la definizione "modo 16 bit" per indicare la modalità reale pura 8086; invece, la definizione "modo 32 bit" verrà utilizzata per indicare la modalità reale 8086 supportata dalle CPU 80386 e superiori.

Come è stato ampiamente spiegato nei precedenti capitoli, la 80386 deve garantire la compatibilità verso il basso per evitare di rendere inutilizzabile l'enorme quantità di software scritto per le vecchie CPU 80286 e soprattutto 8086; in pratica, un qualsiasi programma scritto, ad esempio, per la 8086, deve poter girare senza alcuna modifica sulla 80386.
Per raggiungere questo obiettivo, sulla 80386 viene utilizzato un metodo di codifica delle istruzioni che è una estensione di ciò che è stato precedentemente illustrato per la 8086; questo aspetto viene evidenziato dalla Figura 11.11 che mostra la struttura generale che assume una istruzione, valida per le CPU 80386. Notiamo subito la presenza di due nuovi prefissi opzionali che sono: l'Address Size Prefix e l'Operand Size Prefix. L'Address Size Prefix, se è presente, occupa 1 byte e dice alla CPU che l'accesso ad un eventuale operando di tipo Mem avviene attraverso uno spiazzamento a 32 bit; l'Operand Size Prefix, se è presente, occupa 1 byte e dice alla CPU che gli operandi dell'istruzione da elaborare hanno una ampiezza di 32 bit. Come si può facilmente intuire, questi due prefissi svolgono un ruolo fondamentale nella compatibilità verso il basso delle CPU 80386.
La Figura 11.12 illustra l'elenco completo dei codici macchina (in binario e in esadecimale) di tutti i prefissi disponibili. Rispetto alla Figura 11.2, notiamo la presenza dei prefissi di segmento relativi ai due nuovi registri di segmento FS e GS introdotti proprio a partire dalla CPU 80386; notiamo anche che tutti i codici macchina di Figura 11.2, rimangono inalterati nella 80386.

Tornando alla Figura 11.11, un'altra novità riguarda il campo Opcode che può occupare 1 o 2 byte; questo ampliamento è necessario in quanto gli 8 bit del campo Opcode della 8086 non sono sufficienti per codificare tutte le nuove istruzioni disponibili con la 80386. È chiaro che tutte le vecchie istruzioni della 8086, vengono codificate con i soliti Opcode da 1 byte; il secondo byte, invece, viene utilizzato solo per le nuove istruzioni introdotte dalla 80386.
Il campo Displacement può assumere una ampiezza di 32 bit necessaria per rappresentare anche gli offset a 32 bit disponibili con la 80386; osserviamo inoltre che con la CPU 80386, il campo Immediate può contenere ovviamente anche valori numerici a 32 bit.
Il campo mod_reg_r/m rimane inalterato; naturalmente, anche con la 80386 in certi casi i 3 bit del sottocampo reg si combinano con il campo Opcode.
Osserviamo infine la presenza di un campo opzionale da 1 byte chiamato S.I.B.; il significato di questo campo viene illustrato nel seguito del capitolo.

In Figura 11.13 vediamo come vengono codificati i registri generali e speciali sulla CPU 80386. Come si può notare, nel modo 16 bit i codici macchina dei registri sono identici a quelli di Figura 11.5; per gli operandi di tipo Reg a 8 o 16 bit si ottengono quindi codifiche identiche a quelle già viste negli esempi per la CPU 8086. Se l'Operand Size Prefix è assente, allora w=0 indica operandi a 8 bit, mentre w=1 indica operandi a 16 bit; se, invece, l'Operand Size Prefix è presente, allora w=0 indica operandi a 8 bit, mentre w=1 indica operandi a 32 bit.
Come al solito, se l'operando di riferimento è di tipo Reg, allora il suo codice a 3 bit ricavato dalla Figura 11.13 viene sistemato nel sottocampo reg del campo mod_reg_r/m; se anche il secondo operando è di tipo Reg, allora mod=11b, mentre r/m contiene il codice a 3 bit del registro, ricavato sempre dalla Figura 11.13.

In Figura 11.14 vediamo come vengono codificati i registri di segmento sulla CPU 80386. Abbiamo visto in Figura 11.6 che nel modo 16 bit, i 4 registri di segmento della 8086 vengono codificati con 2 soli bit; nel modo 32 bit, invece, i 6 registri di segmento vengono codificati con 3 bit. I primi 4 codici di Figura 11.14 coincidono con quelli di Figura 11.6, garantendo così la compatibilità verso il basso; si può anche notare che i codici 110b e 111b di Figura 11.14 sono riservati e non devono essere quindi utilizzati per rappresentare un SegReg nelle istruzioni.

La CPU 80386 fornisce inoltre 24 modalità di indirizzamento a 16 bit per gli operandi di tipo Mem; appare ovvio il fatto che le codifiche di queste modalità di indirizzamento (compresi i SegReg predefiniti) sono assolutamente identiche a quelle mostrate in Figura 11.8.
Nel modo 32 bit, invece, sono disponibili 21 modalità principali di indirizzamento a 32 bit; queste 21 modalità vengono mostrate dalla Figura 11.15. La Figura 11.15 ci permette di constatare che nel modo 32 bit, tutte le regole di indirizzamento relative alla 8086 vengono completamente stravolte; infatti, qualunque registro a 32 bit della 80386 può svolgere il ruolo di registro puntatore. Notiamo che negli effective address di Figura 11.15, non può essere utilizzato il registro ESP.
Le associazioni predefinite tra registri puntatori e registri di segmento visibili in Figura 11.15, non presentano alcuna novità rispetto a quanto si è visto in Figura 11.8; possiamo dire quindi che anche con i registri puntatori a 32 bit, in assenza di EBP, il SegReg predefinito è sempre DS, mentre in presenza di EBP, il SegReg predefinito è sempre SS.
Alcune combinazioni tra mod e r/m visibili in Figura 11.15, indicano la presenza del campo S.I.B. nel codice macchina di una istruzione; il significato di questo campo verrà illustrato nel seguito del capitolo.

11.4.1 Confronto tra "modo 16 bit" e "modo 32 bit"

In base alle considerazioni esposte in precedenza, si può facilmente intuire che se proviamo a ripetere gli esempi presentati nella sezione 11.3, riotteniamo gli stessi codici macchina; verifichiamolo subito partendo dall'addizione tra sorgente AL e destinazione BL.
Abbiamo già visto che l'Opcode per l'addizione tra Reg/Mem e Reg/Mem è 000000dw; naturalmente, questo stesso Opcode fa parte del set di istruzioni dalla 80386. Abbiamo quindi w=0 (operandi a 8 bit) e d=1 (operazione to register), per cui:
Opcode = 00000010b = 02h
Dalla Figura 11.13 ricaviamo il codice macchina dell'operando di riferimento BL=011b, per cui reg=011b; sempre dalla Figura 11.13 ricaviamo il codice macchina del secondo operando AL=000b, per cui mod=11b e r/m=000b. Si ottiene quindi:
mod_reg_r/m = 11011000b = D8h
L'istruzione non ha bisogno di altre informazioni, per cui il suo codice macchina generato dall'assembler è:
00000010b 11011000b = 02h D8h
Questo codice macchina è identico a quello ricavato nell'esempio della sezione 11.3!

Passiamo alla somma tra sorgente AX e destinazione BX; nell'Opcode l'unico cambiamento riguarda w=1 per indicare che gli operandi sono a 16 bit. Ricaviamo quindi:
Opcode = 00000011b = 03h
In Figura 11.13 notiamo che i codici macchina di AX e BX sono gli stessi di AL e BL, per cui il campo mod_reg_r/m rimane invariato; ricaviamo quindi:
mod_reg_r/m = 11011000b = D8h
L'istruzione non ha bisogno di altre informazioni, per cui il suo codice macchina generato dall'assembler è:
00000011b 11011000b = 03h D8h
Questo codice macchina è identico a quello ricavato nell'esempio della sezione 11.3!

Vediamo ora quello che succede quando si esegue una somma tra sorgente EAX e destinazione EBX; dalla Figura 11.13 si rileva che i codici macchina di EAX e EBX sono identici a quelli di AX e BX, per cui l'unico cambiamento rispetto all'esempio precedente riguarda il fatto che gli operandi sono a 32 bit. L'assembler inserisce quindi nel codice macchina l'Operand Size Prefix di Figura 11.12, che vale 66h e dice appunto alla CPU che gli operandi sono a 32 bit; si ottiene quindi:
01100110b 00000011b 11011000b = 66h 03h D8h
È necessario ribadire che in presenza del prefisso 66h, w=0 indica che gli operandi sono a 8 bit, mentre w=1 indica che gli operandi sono a 32 bit (full size); questi semplici esempi mostrano in modo chiaro il metodo che viene usato dalla CPU 80386 per supportare le istruzioni della CPU 8086.

Passiamo ad un trasferimento dati da Imm8 a Mem8; poniamo Imm8=-120, e Mem8=DS:[BX+002Dh]; per tutte le CPU della famiglia 80x86, l'Opcode di questa istruzione è 1100011w.
Come al solito, per indicare all'assembler che gli operandi sono a 8 bit, dobbiamo esprimere l'operando destinazione come:
BYTE PTR [BX+002Dh]
Abbiamo quindi w=0, per cui si ottiene:
Opcode = 11000110b = C6h
L'operando destinazione è del tipo [BX+Disp8], per cui dalla Figura 11.8 ricaviamo mod=01b e r/m=111b; il campo mod_reg_r/m assume come al solito la forma mod_000_r/m, per cui si ottiene:
mod_reg_r/m = 01000111b = 47h
Dopo il campo mod_reg_r/m è necessario il campo Displacement destinato a contenere lo spiazzamento 002Dh a 8 bit; infine, è necessario il campo Immediate che contiene il valore -120, cioè 10001000b=88h a 8 bit. Non essendo necessario alcun prefisso di segmento, si ottiene il codice macchina:
11000110b 01000111b 00101101b 10001000b = C6h 47h 2Dh 88h
Questo codice macchina è identico per qualsiasi CPU della famiglia 80x86.

Sostituendo BX con EBX, sempre nel caso di un trasferimento dati a 8 bit, dobbiamo esprimere l'operando destinazione come:
BYTE PTR [EBX+002Dh]
Abbiamo quindi w=0, per cui si ottiene:
Opcode = 11000110b = C6h
L'operando destinazione è del tipo [EBX+Disp8], per cui dalla Figura 11.15 ricaviamo mod=01b e r/m=011b; per il campo mod_reg_r/m si ottiene quindi:
mod_reg_r/m = 01000011b = 43h
Dopo il campo mod_reg_r/m è necessario il campo Displacement destinato a contenere lo spiazzamento 002Dh a 8 bit; è necessario poi il campo Immediate che contiene il valore -120, cioè 10001000b=88h a 8 bit. L'assembler inserisce anche il prefisso 67h di Figura 11.12 per indicare che gli indirizzamenti sono a 32 bit e ottiene il codice macchina:
01100111b 11000110b 01000011b 00101101b 10001000b = 67h C6h 43h 2Dh 88h
Questo codice macchina vale solamente per le CPU 80386 e superiori.

Analizziamo il caso degli operandi a 16 bit con l'operando destinazione espresso come:
WORD PTR [BX+002Dh]
Abbiamo quindi w=1, per cui si ottiene:
Opcode = 11000111b = C7h
L'operando destinazione è del tipo [BX+Disp8], per cui dalla Figura 11.8 ricaviamo mod=01b e r/m=111b; per il campo mod_reg_r/m si ottiene quindi:
mod_reg_r/m = 01000111b = 47h
Dopo il campo mod_reg_r/m è necessario il campo Displacement destinato a contenere lo spiazzamento 002Dh a 8 bit; è necessario poi il campo Immediate che contiene il valore -120, cioè 1111111110001000b=FF88h a 16 bit (con estensione del bit di segno). L'assembler ottiene quindi il codice macchina:
11000111b 01000111b 00101101b 1111111110001000b = C7h 47h 2Dh FF88h
Questo codice macchina è identico per qualsiasi CPU della famiglia 80x86.

Sostituendo BX con EBX, sempre nel caso di un trasferimento dati a 16 bit, dobbiamo esprimere l'operando destinazione come:
WORD PTR [EBX+002Dh]
Abbiamo quindi w=1, per cui si ottiene:
Opcode = 11000111b = C7h
L'operando destinazione è del tipo [EBX+Disp8], per cui dalla Figura 11.15 ricaviamo mod=01b e r/m=011b; per il campo mod_reg_r/m si ottiene quindi:
mod_reg_r/m = 01000011b = 43h
Dopo il campo mod_reg_r/m è necessario il campo Displacement destinato a contenere lo spiazzamento 002Dh a 8 bit; è necessario poi il campo Immediate che contiene il valore -120, cioè 1111111110001000b=FF88h a 16 bit. L'assembler inserisce anche il prefisso 67h di Figura 11.12 per indicare che gli indirizzamenti sono a 32 bit e ottiene il codice macchina:
01100111b 11000111b 01000011b 00101101b 1111111110001000b = 67h C7h 43h 2Dh FF88h
Questo codice macchina vale solamente per le CPU 80386 e superiori.

Analizziamo il caso degli operandi a 32 bit con l'operando destinazione espresso come:
DWORD PTR [BX+002Dh]
Abbiamo quindi w=1, per cui si ottiene:
Opcode = 11000111b = C7h
L'operando destinazione è del tipo [BX+Disp8], per cui dalla Figura 11.8 ricaviamo mod=01b e r/m=111b; per il campo mod_reg_r/m si ottiene quindi:
mod_reg_r/m = 01000111b = 47h
Dopo il campo mod_reg_r/m è necessario il campo Displacement destinato a contenere lo spiazzamento 002Dh a 8 bit; è necessario poi il campo Immediate che contiene il valore -120, cioè 11111111111111111111111110001000b=FFFFFF88h a 32 bit (con estensione del bit di segno). L'assembler inserisce anche il prefisso 66h di Figura 11.12 per indicare che gli operandi sono a 32 bit e ottiene il codice macchina: Questo codice macchina vale solamente per le CPU 80386 e superiori.

Sostituendo BX con EBX, sempre nel caso di un trasferimento dati a 32 bit, dobbiamo esprimere l'operando destinazione come:
DWORD PTR [EBX+002Dh]
Abbiamo quindi w=1, per cui si ottiene:
Opcode = 11000111b = C7h
L'operando destinazione è del tipo [EBX+Disp8], per cui dalla Figura 11.15 ricaviamo mod=01b e r/m=011b; per il campo mod_reg_r/m si ottiene quindi:
mod_reg_r/m = 01000011b = 43h
Dopo il campo mod_reg_r/m è necessario il campo Displacement destinato a contenere lo spiazzamento 002Dh a 8 bit; è necessario poi il campo Immediate che contiene il valore -120, cioè 11111111111111111111111110001000b=FFFFFF88h a 32 bit. L'assembler inserisce anche i prefissi 66h e 67h di Figura 11.12 per indicare che, sia gli operandi, sia gli indirizzamenti sono a 32 bit e ottiene il codice macchina: Questo codice macchina vale solamente per le CPU 80386 e superiori.

In quest'ultimo esempio, se proviamo ad esprimere l'operando destinazione come:
DWORD PTR SS:[EBX+002Dh]
imponiamo all'assembler di inserire anche il prefisso 36h per il registro di segmento SS; si ottiene quindi il codice macchina:

11.4.2 Il campo S.I.B. - Scale Index Base

Abbiamo visto che la CPU 8086 permette di gestire gli indirizzi di memoria attraverso effective address del tipo:
[BX+SI+03F8h]
Abbiamo anche visto che in questo caso, BX rappresenta la base dell'indirizzo, SI rappresenta l'indice, mentre 03F8h rappresenta lo spiazzamento.
Naturalmente, tutto ciò può essere fatto anche con i registri a 32 bit della CPU 80386; ricordando che in questa CPU, i registri a 32 bit sono tutti puntatori, possiamo scrivere, ad esempio:
[EAX+EDX+000003F8h]
Anche in questo caso, EAX rappresenta la base dell'indirizzo, EDX rappresenta l'indice, mentre 000003F8h rappresenta lo spiazzamento.
La CPU 80386 introduce anche una novità che permette al programmatore di richiedere la moltiplicazione del registro indice per un fattore che può essere 1, 2, 4, 8; questo fattore prende il nome di scale factor (fattore di scala). In questo modo possiamo esprimere quindi indirizzi del tipo:
[EAX+(EDX*4)+000003F8h]
Le varie componenti di questo indirizzo vengono denominate, nell'ordine: base, indice, scala e spiazzamento.

Per capire come avviene la codifica binaria di questo tipo di indirizzi, cominciamo con l'osservare che in Figura 11.15, certe combinazioni dei sottocampi mod e r/m indicano alla CPU che il campo mod_reg_r/m è seguito da un ulteriore campo da 1 byte che prende il nome di S.I.B. o Scale Index Base; il campo S.I.B. contiene proprio la codifica binaria della modalità di accesso in memoria.
Sempre attraverso la Figura 11.15, possiamo notare, in particolare, che la presenza del S.I.B. byte viene indicata da r/m=100b, e mod che può valere 00b, 01b o 10b; in una situazione del genere, la CPU sa che dopo il campo mod_reg_r/m è presente il campo S.I.B., il quale assume la struttura mostrata in Figura 11.16. Il sottocampo scale è formato da 2 bit (posizioni dalla 6 alla 7) e codifica il fattore di scala; le 4 possibili codifiche del fattore di scala vengono mostrate in Figura 11.17. Il sottocampo index di Figura 11.16 occupa 3 bit (posizioni dalla 3 alla 5) e codifica il registro a 32 bit che svolge il ruolo di indice; il sottocampo base di Figura 11.16 occupa 3 bit (posizioni dalla 0 alla 2) e codifica il registro a 32 bit che svolge il ruolo di base. Questi codici a 3 bit possono essere ricavati dalla quarta e quinta colonna della Figura 11.13.
Per ogni valore del sottocampo mod (00b, 01b o 10b) sono previste 8 differenti modalità di indirizzamento; in totale abbiamo quindi 3*8=24 modalità che vengono illustrate dalla Figura 11.18. Il termine "(indice scalato)" indica simbolicamente il prodotto tra un registro indice e un fattore di scala come, ad esempio, (ESI*8). Osserviamo subito che questa volta è permessa anche la presenza del registro puntatore ESP che però può svolgere esclusivamente il ruolo di registro base; è proibito quindi scrivere indirizzi del tipo [EAX+ESP+02h]. Dalla Figura 11.13 si rileva che il codice macchina di ESP è 100b; possiamo dire quindi che nel campo index del S.I.B. byte di Figura 11.16, non deve assolutamente comparire il codice macchina 100b! Per chiarire questo delicato aspetto, analizziamo i seguenti due casi:
[EAX+EBP+08h]
e:
[EBP+EAX+08h]
Nel primo caso, il registro base è EAX, per cui il SegReg predefinito è DS; nel secondo caso, il registro base è EBP, per cui il SegReg predefinito è SS.

11.4.3 Esempi pratici per il S.I.B. byte

Vediamo alcuni esempi che chiariscono meglio le considerazioni appena esposte; supponiamo di voler trasferire nel registro ECX, il contenuto a 32 bit della locazione di memoria [EDX+ESI+0Eh]. Abbiamo già visto che l'Opcode per questa istruzione è 100010dw; nel nostro caso w=1 (operandi a 32 bit) e d=1 (operazione to register ECX), per cui:
Opcode = 10001011b = 8Bh
L'operando destinazione è ECX, per cui dalla Figura 11.13 si ricava reg=001b; l'effective address è del tipo:
[EDX+(indice scalato)+Disp8]
per cui (Figura 11.15 e Figura 11.18) mod=01b e r/m=100b. Otteniamo quindi:
mod_reg_r/m = 01001100b = 4Ch
Il fattore di scala è 1 (nessuna moltiplicazione), il registro base è EDX, mentre il registro indice è ESI; dalle Figure 11.13, 11.17 e 11.18 otteniamo allora scale=00b, index=110b, base=010b. Per il campo S.I.B. si ha quindi:
S.I.B. = 00110010b = 32h
L'ultimo campo è il Displacement che contiene il valore 0Eh a 8 bit; l'assembler inserisce anche i due prefissi 66h e 67h per indicare che gli operandi e gli indirizzi sono entrambi a 32 bit. In definitiva, il codice macchina che si ottiene è: Il Microsoft MASM, invece, a causa del bug citato in precedenza, inverte base con index e produce:
S.I.B. = 00010110b = 16h
Vediamo ora quello che succede, se l'operando destinazione è il registro EBX e l'operando sorgente è la locazione di memoria [EAX+(EBP*8)+03FEh]; le parentesi tonde servono solo per chiarezza e non sono indispensabili in quanto la moltiplicazione ha la precedenza sull'addizione.
Il campo Opcode rimane invariato, per cui:
Opcode = 10001011b = 8Bh
L'operando destinazione è EBX, per cui dalla Figura 11.13 si ricava reg=011b; l'effective address è del tipo:
[EAX+(indice scalato)+Disp32]
(non è permesso usare un Disp16)

per cui (Figura 11.15 e Figura 11.18) mod=10b e r/m=100b. Otteniamo quindi:
mod_reg_r/m = 10011100b = 9Ch
Il fattore di scala è 8, il registro base è EAX, mentre il registro indice è EBP; dalle Figure 11.13, 11.17 e 11.18 otteniamo allora scale=11b, index=101b, base=000b. Per il campo S.I.B. si ha quindi:
S.I.B. = 11101000b = E8h
L'ultimo campo è il Displacement che contiene il valore 000003FEh a 32 bit; l'assembler inserisce anche i due prefissi 66h e 67h per indicare che gli operandi e gli indirizzi sono entrambi a 32 bit. In definitiva, il codice macchina che si ottiene è: Anche il Microsoft MASM, in presenza di un fattore di scala diverso da 1, genera correttamente il codice macchina.

Analizziamo infine un caso che non prevede il S.I.B. byte; consideriamo a tale proposito un trasferimento dati a 32 bit da [ECX+2Dh] a EBP.
Anche in questo caso abbiamo l'Opcode che vale 100010dw, con w=1 e d=1, per cui:
Opcode = 10001011b = 8Bh
L'operando destinazione è EBP, per cui dalla Figura 11.13 si ricava reg=101b; l'effective address è del tipo:
[ECX+Disp8]
per cui (Figura 11.15) mod=01b e r/m=001b. Otteniamo quindi:
mod_reg_r/m = 01101001b = 69h
L'ultimo campo è il Displacement che contiene il valore 2Dh a 8 bit; l'assembler inserisce anche i due prefissi 66h e 67h per indicare che gli operandi e gli indirizzi sono entrambi a 32 bit. In definitiva, il codice macchina che si ottiene è: Per questo tipo di indirizzamenti il bug del MASM non si manifesta.

11.5 Il codice Assembly

In base alle considerazioni esposte in questo capitolo, si può facilmente capire che l'idea di programmare un computer in codice macchina, appare assolutamente folle; eppure, i primi elaboratori elettronici, venivano programmati proprio in questo modo!
Il programma veniva scritto in codice macchina ed immesso nel calcolatore attraverso i più strani meccanismi; una volta che il calcolatore aveva terminato le elaborazioni, forniva i risultati (sempre in codice macchina), attraverso un dispositivo di output che generalmente era rappresentato dalla telescrivente (l'antenata della stampante). A questo punto, i tecnici analizzavano i risultati forniti dal computer (sotto forma di una marea di 0 e 1) e se veniva riscontrata qualche stranezza, si provvedeva a ricontrollare tutto il programma alla ricerca di eventuali errori.
Una situazione di questo genere creava enormi problemi, sia ai programmatori, sia agli analisti che avevano il compito di studiare i risultati prodotti dal programma; gli studi e le ricerche intraprese per trovare una soluzione a questi problemi, hanno portato alla nascita del linguaggio Assembly!

Lo scopo del linguaggio Assembly è quello di permettere la programmazione a basso livello dei computer attraverso una sintassi evoluta e quindi facilmente comprensibile dagli esseri umani; questo obiettivo deve essere raggiunto senza compromettere minimamente le doti di potenza, di efficienza e di compattezza che caratterizzano i programmi scritti in codice macchina.
Un programma scritto in Assembly, rappresenta il cosiddetto codice sorgente; questo codice sorgente, prima di poter essere eseguito dal computer, deve essere quindi convertito in codice macchina. Come già sappiamo, lo strumento che si occupa di questa fase di conversione prende il nome di assembler o assemblatore; l'assemblatore richiede che le istruzioni che formano un programma, assumano la seguente forma generale:
mnemonico destinazione, sorgente
Un mnemonico è un nome simbolico che ha lo scopo di richiamare alla mente l'operazione che la CPU deve svolgere; possiamo indicare, ad esempio, l'addizione con il mnemonico ADD (da "addition"), la sottrazione con SUB (da "subtraction"), il complemento a 1 con NOT, il trasferimento dati con MOV (da "move"), etc.
Subito dopo il mnemonico troviamo i due operandi sorgente e destinazione; l'assembler NASM segue la cosiddetta sintassi Intel in base alla quale l'operando destinazione viene disposto alla sinistra dell'operando sorgente (con i due operandi separati da una virgola). Altri assembler (come quello della AT&T utilizzato nel mondo UNIX), prevedono la convenzione opposta.
Attraverso questa sintassi evoluta, possiamo scrivere, ad esempio:
MOV EAX, CS:[EBX+(EDX*4)+00002F8Ah]
Questa istruzione indica in modo chiaro e semplice, un trasferimento dati a 32 bit dalla locazione di memoria [CS:EBX+(EDX*4)+00002F8Ah] (operando sorgente) al registro EAX (operando destinazione); analogamente, possiamo scrivere:
ADD BYTE PTR [BX+028Eh], +15
Questa istruzione indica chiaramente una somma a 8 bit tra il valore immediato +15 (operando sorgente) e il contenuto della locazione di memoria DS:[BX+028Eh] (operando destinazione).
Questi due semplici esempi rendono evidente l'abissale differenza che esiste tra Assembly e codice macchina in termini di chiarezza e semplicità.

Vediamo un ulteriore esempio più articolato.
Supponiamo che nel Data Segment di un programma siano presenti tre variabili statiche a 16 bit che possiamo chiamare simbolicamente Var1, Var2 e Var3; queste tre variabili si trovano, nell'ordine, agli offset 002Ah, 002Ch e 002Eh. Vogliamo sommare Var1 con Var2, mettendo il risultato finale in Var3; impiegando un numero volutamente esagerato di istruzioni, otteniamo la porzione di programma mostrata in Figura 11.19 (in Assembly il punto e virgola delimita l'inizio di un commento che termina non appena si va a capo). Questo programma presuppone che DS sia già stato inizializzato con la componente Seg dell'indirizzo logico iniziale del Data Segment; confrontiamo ora il programma Assembly di Figura 11.19 con il corrispondente programma in codice macchina mostrato dalla Figura 11.20. Appare evidente il fatto che, grazie all'Assembly, il programmatore può concentrarsi sul programma che deve scrivere, delegando poi all'assembler il compito di gestire la conversione in codice macchina; questo modo di procedere comporta una drastica riduzione del rischio di commettere errori e una altrettanto drastica riduzione del tempo necessario per scrivere un programma. Analizzando la Figura 11.19 e la Figura 11.20, si può constatare che esiste una corrispondenza biunivoca tra codice Assembly e codice macchina; ciò significa che ad ogni istruzione Assembly corrisponde una e una sola istruzione in codice macchina e viceversa.
Questo aspetto rende relativamente semplice il compito dell'assembler che, in sostanza, non fa altro che applicare le regole in parte esposte in questo capitolo; inoltre, una diretta conseguenza della corrispondenza biunivoca citata in precedenza, è data dall'esistenza dei cosiddetti disassembler. Il disassembler è uno strumento che svolge il lavoro opposto a quello dell'assembler; il suo scopo quindi è quello di convertire in Assembly un programma scritto in codice macchina.
Come si può facilmente intuire, i disassembler vengono largamente utilizzati dagli hackers per effettuare il cosiddetto reverse engineering; questa tecnica consiste nel "carpire" a scopo di studio, i segreti che si celano nel codice macchina dei programmi eseguibili. Naturalmente, il reverse engineering viene "praticato" anche a scopo di lucro da persone con pochi scrupoli.

Queste considerazioni, unite a ciò che è stato esposto in questo capitolo, ci fanno capire che un programmatore Assembly degno di questo nome non può assolutamente fare a meno della conoscenza della programmazione in codice macchina; nei prossimi capitoli vedremo che in certi casi, questa conoscenza si rivela estremamente utile e vantaggiosa.

A questo punto, siamo in grado di capire anche l'enorme differenza che esiste tra un assemblatore e un compilatore; abbiamo appena visto che il compito svolto dall'assemblatore è relativamente semplice in quanto una qualsiasi istruzione in codice macchina, può essere convertita in Assembly attraverso un procedimento univoco.
Il compilatore, invece, si trova davanti ad istruzioni scritte con un linguaggio di alto livello, molto più vicino al linguaggio umano che non a quello del computer; il programma di Figura 11.19, ad esempio, può essere riscritto in linguaggio Pascal con la seguente unica istruzione:
Var3 := Var1 + Var2;
Il compilatore quindi ha il difficile compito di convertire istruzioni complesse, in una sequenza di codici macchina che deve essere la migliore possibile in termini di compattezza e di efficienza; può capitare allora che due diversi compilatori, incontrando la stessa istruzione, generino due sequenze diverse di codici macchina. Tutto ciò rende (quasi) proibitiva l'idea di realizzare uno strumento analogo al disassembler, capace di convertire il codice macchina di Figura 11.20 nel codice Pascal della precedente istruzione.

Per quanto riguarda infine il confronto tra compilatori ed interpreti, è stato già spiegato che entrambi presentano vantaggi e svantaggi. I compilatori leggono il programma da noi scritto (codice sorgente) e lo traducono completamente in codice macchina, in un formato cioè, direttamente eseguibile dalla CPU alla massima velocità possibile; l'aspetto negativo di questa tecnica, è legata al fatto che il codice macchina generato dal compilatore, dovendo essere autosufficiente, contiene al suo interno una miriade di informazioni aggiuntive che determinano un notevole aumento delle dimensioni del codice stesso.
Le caratteristiche degli interpreti sono diametralmente opposte; l'interprete, infatti, legge la prima istruzione del codice sorgente, la traduce in codice macchina e la fa eseguire dalla CPU, ripetendo poi lo stesso procedimento con l'istruzione successiva. Le conseguenze negative di questa tecnica sono rappresentate dalla esasperante lentezza nell'esecuzione e dalla impossibilità di eseguire un programma in assenza dell'interprete; l'unico aspetto positivo, invece, è dato dalle ridottissime dimensioni di questi particolari programmi.
L'esigenza dei programmatori è quella di scrivere programmi velocissimi ed efficienti ed è per questo motivo che i compilatori dominano la scena; naturalmente, un qualunque linguaggio di programmazione di alto livello, per quanto potente possa essere, non potrà mai competere con l'Assembly.

Dopo aver fatto conoscenza con le prime istruzioni Assembly, è arrivato il momento di cominciare a scrivere i primi programmi; tali programmi hanno lo scopo di mostrare attraverso semplici esempi, le caratteristiche delle decine e decine di istruzioni che formano questo linguaggio di programmazione. Naturalmente, prima di compiere questo passo bisogna analizzare in dettaglio la struttura generale che assume un programma Assembly; lo scopo dei prossimi capitoli è proprio quello di mostrare come è organizzato un programma Assembly, come si esegue l'assemblaggio del programma e come viene generato il file in formato eseguibile.