- Messaggi
- 2,332
- Reazioni
- 1,928
- Punteggio
- 134
Il Linguaggio Macchina del 8086 (Parte 1)
Marco (DispatchCode) C.
Cos'è il codice macchina? Codice macchina ed assembly sono la stessa cosa? Come interpreta la istruzioni la CPU?
Credo che a molti di voi sia prima o poi accaduto di pensare a come la CPU interpreta le istruzioni, a cos'è il codice macchina. L'articolo tratterà proprio questi aspetti ad iniziare dalla CPU Intel 8086. La trattazione che ne segue non è divulgativa, pertanto potrebbe risultare di difficile comprensione a chi non ha competenze in materia.
Tutte le informazioni riportate nell'articolo sono frutto di anni di studio personale su materiale "non ufficiale" (autori di libri famosi, ma non appartenenti ad Intel) e materiale "ufficiale" (di Intel). Ho pensato quindi di mettere a disposizione queste conoscenze affinché altri possano trarne vantaggio (per qualsiasi loro scopo personale).
L'articolo è diviso in due parti. La prima tratta, che è la presente, tratta i seguenti argomenti:
- Panoramica
- Strumenti e Documentazioni Utili
- Architettura della CPU
- Associazioni di default tra Segmento e Offset
- Puntare ad un indirizzo di memoria
- Il Linguaggio Assembly
- Architetture CISC e RISC
potremmo definirla un'introduzione base alla CPU, che permetterà poi di approcciare la seconda parte con più semplicità:
- Codice Macchina
- Struttura di un'istruzione
- Instruction Prefix
- Segment Override Prefix
- Instruction Opcode
- mod_reg_r/m
- Displacement
- Immediate
- Esempi in linguaggio macchina
- Conclusione
- Struttura di un'istruzione
Panoramica
Prima di proporre i materiali utili e gli strumenti da utilizzare, voglio essere certo abbiate compreso a fondo il tipo di viaggio che affronteremo ed il relativo scopo.
Descrivere il viaggio è semplice, per chi lo conosce: inizieremo dando una brevissima panoramica sul linguaggio Assembly, proprio per comprendere cosa vedremo ad un livello più basso; proseguiremo poi con l'architettura CISC/RISC che ci servirà per introdurre proprio il tipo di istruzioni che andremo ad analizzare in seguito; un altro punto che affronteremo prima del codice macchina vero e proprio, sarà l'architettura del processore (brevi cenni su ciò che ci interessa maggiormente, in realtà).
Giunti al codice macchina partiremo proprio alla scoperta della CPU 8086 a 16bit; il viaggio proseguirà poi su CPU a 32bit (in articoli futuri).
L'indice mostrato poco più sopra dovrebbe essere chiaro.
A tratti vi sembrerà sicuramente di leggere un articolo che "esce dal seminato" trattando più concetti relativi ad assembly ed all'architettura, ma è l'unico modo per comprendere poi le istruzioni senza dover spiegare passo passo cosa state vedendo.
Strumenti e Documentazioni Utili
Il viaggio che affronteremo non richiederà strumenti particolari, solo un modesto Hex Editor ed un emulatore per DOS se siamo su un OS a 64bit.Gli strumenti da me utilizzati sono HxD e DOSBox. Se invece possedete un OS a 32bit potrete eseguire gli esempi a 16bit senza problemi con un doppio click. Su OS diversi da Windows è sempre necessario un emulatore (il codice macchina utilizzerà interrupt del DOS).
Un primo file utile e da tenere sempre sotto mano per quanto riguarda le documentazioni è il PDF di intel relativo alla CPU 8086.
- 231455.pdf (Mediafire)
Architettura della CPU
La CPU è composta da svariati circuiti e componenti interni, ciascuno di essi ha compiti ben precisi. Visti gli scopi dell'articolo mi è impossibile trattarli tutti, quindi ne citerò solo alcuni e poi ci concentreremo su ciò che effettivamente un programmatore Assembly utilizza (e di conseguenza, su ciò che è accessibile al programmatore).
La CPU è dotata di circuiti atti alla decodifica, di registri interni, di registri non accessibili al programmatore, di una ALU e di altre parti ancora. Oltre a questi componenti, sul chip è presente anche una cache di primo livello, chiamata L1, che mantiene le istruzioni più utilizzate. Questa cache è molto rapida da consultare per il processore, trattandosi in pratica di un'area interna ad esso. Va inoltre fatto notare che le CPU dei giorni nostri hanno 3 cache, chiamate L1, L2, L3. La cache più vicina alla CPU, accessibile con un costo inferiore in termini di tempo (ma con meno capacità) è la L1; via via seguono le altre,con la L3 che ha costi più alti ma che è grande anche qualche MB.
La cache L1 è come detto interna alla CPU; non è raro trovare anche due cache L1 all'interno della CPU (nelle architetture più moderne). La loro dimensione si aggira attorno ai 16/24KB (ciascuna): prendete con le pinze questo dato, non escludo che CPU recenti abbiano magari cache più grandi.La cache L2 contiene qualche MB di words usate di recente; la L3 è come detto sopra, la più grande. E' difficile stabilire in maniera assoluta dove queste cache risiedano dato che dipende dai progettisti (AMD ed Intel ad esempio hanno differenze nelle progettazioni).
Nella CPU le informazioni transitano sui bus. I bus sono delle "linee di collegamento" e su di esse transitano i bit (1 linea = 1bit).
I bus principali sono 3: il bus di controllo, che abilita e gestisce le operazioni tra i componenti, abilitando ad esempio una cella della memoria; il bus degli indirizzi, su cui transitano gli indirizzi di una cella a cui si deve accedere; ed il bus dati, su cui transitano i dati effettivi da spostare.
L'Address Bus della CPU 8086 è di 20bit, mentre il Data Bus è a 16bit. Prima di continuare, considerate la piedinatura dell'8086:
Immagine tratta da Wikipedia.org
Dall'immagine mostrata precedentemente possiamo osservare alcune cose: innanzi tutto, possiamo riconoscere l'address bus ed il data bus.L'address bus è indicato dalle linee nominate AD0, sino ad AD19 (notare che sono infatti 20). Il data bus sfrutta esattamente le stesse linee del bus indirizzi una volta che l'address bus ha eseguito la sua operazione (ovvero una volta trasmesso l'indirizzo a cui si dovrà accedere). Guardando bene la figura infatti noterete che le linee da AD0 ad AD15, recano tutte una 'D', mentre le successive hanno solo la lettera 'A'. Quelle linee sono dedicate al data bus.
Il bus indirizzi è quindi a 20bit, e questo significa che è in grado di indirizzare 1MB di memoria (RAM); il data bus è a soli 16bit. I registri interni - che tratteremo a breve - sono quindi a 16bit. Per considerazioni relative al modo in cui viene indirizzata la memoria, vi lascio alla parte sotto spoiler.
(chiedo venia, ma trattandosi di una risposta data in un post, troverete magari riferimenti ad un singolo)
Sotto 8086 la memoria è divisa in segmenti: ciascun segmento è grande 65536byte. Per spostarsi all'interno di un segmento si usa un Offset (spiazzamento, sarebbe il termine corretto). Ecco che nasce quindi la coppia Segmento:Offset.
In pratica, il segmento 0 ha 65536byte, il segmento 1 ha 65536byte... il segmento 65536 ha 65536byte. Questa rappresentazione era necessaria in quanto a quell'epoca si utilizzavano appunto i segmenti (infatti era un disastro quando un programma cresceva di dimensioni); ogni segmento è di 64KB, appunto.
Con 65536 segmenti e 65536 spiazzamenti, puoi indirizzare 2^32 byte di memoria (questo perchè sia i segmenti sia gli offset/spiazzamenti sono a 16bit, quindi 2^16 * 2^16=2^32). Detto così sembra poco, ma in realtà si tratta di 4294967296byte, ovvero 4GB.
Ora la situazione si fa contorta: come diceva anche Madda, l'indirizzo fisico è espresso a 20bit, ma quello logico soltanto a 16. Questo perchè l'architettura dell'8086 era a 16bit; il problema è che il bus degli indirizzi (l'address bus) è a 20bit. 20bit significa 2^20, ovvero 1MB di memoria. In pratica poteva indirizzare 1MB di memoria (ma grazie all'idea dei progettisti spiegata li sopra, divennero ben 4GB).
La coppia logica è formatta come dicevo prima da Segmento:Offset, ciascuno di 16bit. Ciascun segmento di memoria parte inoltre da un indirizzo fisico che è un multiplo di 16, chiamato paragrafo.
Abbiamo alcune "costanti" numeriche da ricordare. 65536 in esadecimale si scrive FFFFh, e sono appunto 16bit. Se utilizziamo 20bit per rappresentarlo, scriveremo 0FFFFh. Il fatto che ciascun paragrafo si trovi ad un multiplo di 16 permette una conversione abbastanza rapida e semplice da una coppia logica (Segmento:Offset) in una fisica.
Vogliamo sapere l'indirizzo fisico (quindi nella RAM) di questa coppia: 1234h:0005h. Qui abbiamo quindi come segmento 1234h, e come Offset 0005h. La conversione in indirizzo fisico avviene come descritto da Madda (16 in esadecimale corrisponde a 10h):
L'indirizzo fisico corrispondente è quindi 12345h (che come noti è a 20bit). Al posto di moltiplicare per 16, si può utilizzare lo shift a sinistra. Ogni spostamento a sinistra equivale ad una moltiplicazione per 2 del numero. Spostare 4bit a sinistra, significa quindi moltiplicare per 16. Si usa lo shift in quanto è più veloce di una moltiplicazione (in realtà il chip includeva un circuito speciale dedicato a questo tipo di conversione).
La conversione da fisico a logico risulta altrettanto semplice. Si può prendere come esempio l'indirizzo ottenuto nell'esempio qui sopra, 12345h. Per ottenere Segmento:Offset si considerano i bit di livello più alto, sino a contarne 16; gli altri rimanenti rappresentano l'offset. Per farla semplice: se hai l'indirizzo espresso in esadecimale, i primi 4 bit a sinistra rappresentano l'indirizzo logico, quindi:
Vi sono alcune osservazioni molto importanti da fare ora: se vi è stata spiegato l'indirizzamento lineare, saprai che esiste una corrispondenza biunivoca tra l'indirizzo lineare e l'indirizzo fisico, ovvero: un indirizzo lineare corrisponde ad un indirizzo fisico. Purtroppo con la coppia logica la situazione non è più la stessa in quanto i segmenti sono parzialmente sovrapposti.
Cosa significa in pratica? Per capire, osserva sempre la coppia logica dell'esempio precedente. L'indirizzo fisico risultante è: 12345h. Ma esistono altri modi per ottenerlo:
Te ne ho mostrati solo alcuni (e l'ultimo). Qualche anno fa scrissi due righe in Java per convertire indirizzi logici in indirizzi fisici e viceversa. Se lo trovo lo posto, o scrivo due righe in C/C++ o altro e posto l'exe, che magari è più utile.
Come noti comunque, tutte quelle coppie logiche daranno come risultato lo stesso indirizzo fisico.
L'altro aspetto da considerare è che gli ultimi segmenti hanno meno di 65536byte. Qual è l'ultima coppia logica che genera l'ultimo indirizzo fisico? Questa:
Come noti è l'ultimo. Questo significa che dell'ultimo segmento sono usati solo 16byte. Il segmento prima invece avrà solo 32byte... e così via a ritroso.
Sotto 8086 la memoria è divisa in segmenti: ciascun segmento è grande 65536byte. Per spostarsi all'interno di un segmento si usa un Offset (spiazzamento, sarebbe il termine corretto). Ecco che nasce quindi la coppia Segmento:Offset.
In pratica, il segmento 0 ha 65536byte, il segmento 1 ha 65536byte... il segmento 65536 ha 65536byte. Questa rappresentazione era necessaria in quanto a quell'epoca si utilizzavano appunto i segmenti (infatti era un disastro quando un programma cresceva di dimensioni); ogni segmento è di 64KB, appunto.
Con 65536 segmenti e 65536 spiazzamenti, puoi indirizzare 2^32 byte di memoria (questo perchè sia i segmenti sia gli offset/spiazzamenti sono a 16bit, quindi 2^16 * 2^16=2^32). Detto così sembra poco, ma in realtà si tratta di 4294967296byte, ovvero 4GB.
Ora la situazione si fa contorta: come diceva anche Madda, l'indirizzo fisico è espresso a 20bit, ma quello logico soltanto a 16. Questo perchè l'architettura dell'8086 era a 16bit; il problema è che il bus degli indirizzi (l'address bus) è a 20bit. 20bit significa 2^20, ovvero 1MB di memoria. In pratica poteva indirizzare 1MB di memoria (ma grazie all'idea dei progettisti spiegata li sopra, divennero ben 4GB).
La coppia logica è formatta come dicevo prima da Segmento:Offset, ciascuno di 16bit. Ciascun segmento di memoria parte inoltre da un indirizzo fisico che è un multiplo di 16, chiamato paragrafo.
Abbiamo alcune "costanti" numeriche da ricordare. 65536 in esadecimale si scrive FFFFh, e sono appunto 16bit. Se utilizziamo 20bit per rappresentarlo, scriveremo 0FFFFh. Il fatto che ciascun paragrafo si trovi ad un multiplo di 16 permette una conversione abbastanza rapida e semplice da una coppia logica (Segmento:Offset) in una fisica.
Vogliamo sapere l'indirizzo fisico (quindi nella RAM) di questa coppia: 1234h:0005h. Qui abbiamo quindi come segmento 1234h, e come Offset 0005h. La conversione in indirizzo fisico avviene come descritto da Madda (16 in esadecimale corrisponde a 10h):
Codice:
(segmento * 10h) + Offset
(1234h * 10h) + 0005h = 12340h + 0005h = 12345h
L'indirizzo fisico corrispondente è quindi 12345h (che come noti è a 20bit). Al posto di moltiplicare per 16, si può utilizzare lo shift a sinistra. Ogni spostamento a sinistra equivale ad una moltiplicazione per 2 del numero. Spostare 4bit a sinistra, significa quindi moltiplicare per 16. Si usa lo shift in quanto è più veloce di una moltiplicazione (in realtà il chip includeva un circuito speciale dedicato a questo tipo di conversione).
La conversione da fisico a logico risulta altrettanto semplice. Si può prendere come esempio l'indirizzo ottenuto nell'esempio qui sopra, 12345h. Per ottenere Segmento:Offset si considerano i bit di livello più alto, sino a contarne 16; gli altri rimanenti rappresentano l'offset. Per farla semplice: se hai l'indirizzo espresso in esadecimale, i primi 4 bit a sinistra rappresentano l'indirizzo logico, quindi:
Codice:
12345h -> 1234h:0005h
Vi sono alcune osservazioni molto importanti da fare ora: se vi è stata spiegato l'indirizzamento lineare, saprai che esiste una corrispondenza biunivoca tra l'indirizzo lineare e l'indirizzo fisico, ovvero: un indirizzo lineare corrisponde ad un indirizzo fisico. Purtroppo con la coppia logica la situazione non è più la stessa in quanto i segmenti sono parzialmente sovrapposti.
Cosa significa in pratica? Per capire, osserva sempre la coppia logica dell'esempio precedente. L'indirizzo fisico risultante è: 12345h. Ma esistono altri modi per ottenerlo:
Codice:
1234h:0005h
1233h:0015h
1232h:0025h
1231h:0035h
1230h:0045h
122Fh:0055h
122Eh:0065h
122Dh:0075h
.................
.................
0235h:FFF5h
Te ne ho mostrati solo alcuni (e l'ultimo). Qualche anno fa scrissi due righe in Java per convertire indirizzi logici in indirizzi fisici e viceversa. Se lo trovo lo posto, o scrivo due righe in C/C++ o altro e posto l'exe, che magari è più utile.
Come noti comunque, tutte quelle coppie logiche daranno come risultato lo stesso indirizzo fisico.
L'altro aspetto da considerare è che gli ultimi segmenti hanno meno di 65536byte. Qual è l'ultima coppia logica che genera l'ultimo indirizzo fisico? Questa:
Codice:
FFFFh:000Fh
(FFFFh * 10h) + 000Fh = FFFFFh
Come noti è l'ultimo. Questo significa che dell'ultimo segmento sono usati solo 16byte. Il segmento prima invece avrà solo 32byte... e così via a ritroso.
Oltre ai registri non accessibili al programmatore (sono registri temporanei utilizzati dalla CPU), vi sono una serie di registri a cui il programmatore ha accesso. Questi registri prendono il nome di registri generali, registri di segmento e registri speciali. Un registro è una piccola area di memoria - ovviamente interna alla CPU - che consente di salvare una piccola quantità di dati al fine di eseguire poi operazioni su essi. La "piccola quantità di memoria" dipende dalla CPU, dalla sua progettazione, ma in generale si tratta dell'architettura. Abbiamo infatti visto che un data bus di 16bit (quindi di 16linee) permette di spostare 16bit alla volta. Ne consegue che una CPU a 32bit è in grado di leggere 32bit "in un colpo solo", sfruttando tutte le sue linee sul data bus.
I registri che il programmatore può utilizzare sono i seguenti, e sono a 16bit, per quanto riguarda il processore 8086:
La prima colonna elenca i registri di segmento: questi 4 registri memorizzano la parte "Segmento", e ciascuno di essi si occupa di un segmento in particolare. Il paragrafo che segue tratterà meglio questi aspetti, ma si possono trovare dettagli alla pagina linkata precedentemente. La memoria sotto 8086 è suddivisa in segmenti, e ciascun segmento non può essere più grande di 64KB (65536byte, rappresentabili in 16bit). I segmenti citati nella tabella consentono di memorizzare le parti principali di un programma; infatti un programma è composto solitamente da almeno 3 segmenti (questo anche sotto CPU superiori alla 8086, come vedremo in seguito) i cui nomi sono: CS (Code Segment), DS (Data Segment), e SS (Stack Segment). I registri segmento come vedremo tra poco vengono associati ad uno degli altri registri della CPU, ed insieme formano una coppia logica o indirizzo logico. L'indirizzo è quindi rappresentato dalla coppia Segmento:Offset (verrà ripreso tra poco questo concetto).
Il registro CS contiene il blocco codice del programma (le istruzioni eseguite dalla CPU, quelle del nostro programma);
il segmento DS contiene invece il segmento Dati del nostro programma, ovvero tutti i dati inizializzati e talvolta no, ma con una dimensione conosciuta (che non cambia nel tempo);
il segmento SS è lo stack del nostro programma. Lo Stack è un'area della memoria RAM usata dalla CPU quando deve ad esempio chiamare delle procedure. Lo Stack contiene i dati che vengono quindi creati e distrutti (come i parametri di una funzione/procedura ad esempio).
Il registro ES è un secondo registro di segmento dedicato ad un blocco dati; la E sta per Extra.
Ricordo che vi è una limitazione molto fastidiosa: 64KB massimi per un segmento, quindi un aumento dei registri permetteva ai programmatori di scrivere programmi più complessi e non dover impazzire per gestire più segmenti.
La seconda tabella contiene invece i registri generali AX, BX, CX, DX: questi segmenti sono solo 4, ma spesso anche i registri speciali SI e DI vengono utilizzati come "generali". I segmenti generali vengono utilizzati per operazioni di assegnamento (come vedremo tra non molto, l'istruzione MOV) e per operazioni matematiche.
Il registro AX è il registro accumulatore, ed è il registro di default per molte operazioni matematiche, oltre che essere il registro di default per i valori restituiti dalle funzioni (di solito restituisce uno stato per sapere se si sono verificati errori o un valore di una funzione); il discorso è valido come vedremo in seguito anche per processori superiori all'8086, dove sarà evidente l'uso di questo registro con le funzioni/procedure.
Il registro BX è il registro base, e viene utilizzato allo stesso modo di AX.
Il registro CX è il registro contatore, ed è l'operando di default utilizzato dalla CPU nel caso di cicli (while).
Il registro DX è il registro dati, e svolte le stesse operazioni di BX; tuttavia nel caso di alcune istruzioni viene utilizzatoper l'input o per l'output (ad esempio, nel caso di una divisione conterrà il resto).
I registri generali sono scomponibili in altri registri più piccoli, di 8 bit ciascuno. I registri AH e AL formano rispettivamente la parte più alta e la parte più bassa del registro AX; così come CH e CL sono la parte più alta e più bassa di CX; BH e BL quelle di BX e DH e DL quelle di DX.
La terza tabella evidenzia i registri speciali. I registri SI (Source Index) e DI (Destination Index) vengono utilizzati come se fossero registri generali, quindi per operazioni matematiche comuni e per spostamenti di dati; un altro utilizzo è destinato all'indirizzamento dei dati.
Il registro SP (Stack Pointer) di norma non è manipolato direttamente dal programmatore, e contiene il puntatore allo stack.
Il registro BP (Base Pointer) si riferisce sempre allo stack, ma è di norma manipolato dal programmatore per accedere allo stack.
Il registro IP (Instruction Pointer) si rifrisce al segmento del codice, e non è utilizzato dal programmatore. La CPU utilizza questo registri per tener traccia dell'istruzione da eseguire, e viene incrementato ad ogni istruzione (o quando serve, come nel caso di salti condizionati/incondizionato, viene impostato al nuovo indirizzo).
La quarta ed ultima tabella mostra il registro dei flags. Questo speciale registro è a 16bit. Alcuni bit sono riservati, altri indicano lo stato del processore (e non sono accessibili); altri ancora invece vengono settati per indicare lo stato di alcune operazioni, precisamente, quelle matematiche. E' raro che il programmatore debba settare dei bits in questo registro. Di norma il programmatore si occupa della lettura, anche se non in maniera diretta. Esistono alcune istruzioni, come CMP, che usa due operandi per effettuare una comparazione: ciò che fa è una sottrazione senza però memorizzare il risultato. Le istruzioni usate solitamente in seguito a questa istruzione, sono quelle di salto condizionato, ovvero JE, JG, JL e molte altre ancora. Queste istruzioni leggono il bit di loro interesse e se è settato effettuano un salto.
Associazioni di Default tra Registri e Segmenti
Utilizziamo questo breve paragrafo per fare il punto della situazione: i registri della CPU accessibli dal programmatore sono 15. Tra questi vi sono i registri speciali, il registro dei flags, i registri generali e quelli di segmento.
Le istruzioni hanno tutte almeno 1 operando. Quando ne hanno due la sintassi è sempre:
istruzione dest, sorgente
Come ad esempio:
MOV AX, BX
che causa un'assegnazione del valore di BX nel registro AX (corrisponde in alto livello ad un assegnamento tra due variabili, come a = b).
Come si avrà avuto modo di osservare, i registri di Segmento contengono la parte Segmento del programma. I segmenti sempre presenti sono CS (Codice), DS (Dati), e SS (Stack, dati dinamici). Il valore che contengono è quindi l'indirizzo di tale segmento. Per indirizzare un dato al suo interno (quindi per accedere ad una posizione particolare di quel segmento), è necessario utilizzare un puntatore, un altro registro.
La CPU crea le seguenti associazioni di default:
SS è il registro di segmento a cui vengono associati i registri SP e BP.
DS è il registro di segmento a cui vengono associati i registri AX, BX, CX, DX, SI e DI,
CS è il registro di segmento a cui viene associato il regostro IP.
Le associazioni avvengono utilizzando la seguente sintassi: [DS:BX]. In questo caso l'associazione è inutile ed anche ridondante, poichè come abbiamo visto il registro BX è già associato al registro DS. Con alcuni assemblatori l'associazione è anche controproducente visto che l'associazione viene comunque considerata ed elaborata dal compilatore, che - come si vedrà in seguito - genererà un codice macchina più lungo.Supponiamo di avere dei dati nel segmento ES e di utilizzare il registro DI per accedere ad essi e trasferire il contenuto nel registro AX. L'istruzione sarà quindi MOV AX, [ES:DI]
Puntare ad un Indirizzo di Memoria
Sin dagli inizi dell'articolo ho parlato di "memoria indirizzabile" oppure ho utilizzato termini come "indirizzamento" o ancora "la memoria indirizzabile da 8086 è 1MB". Ora è il momento di affrontare questo concetto importante, introducendo l'operatore di deriferimento o di indirezione.
Se avete delle basi di C/C++ o di un linguaggio che permette l'utilizzo di puntatore, avrete anche sentito probabilmente questi termini, o sicuramente avrete sentito parlare dei puntatori. In pratica l'operatore di riferimento è il puntatore, in C indicato come *, che consente di far riferimento ad un indirizzo di memoria per leggerne/scriverne il contenuto.
La particolarità di un puntatore è che viene manipolato il dato, attraverso il suo indirizzo, e non il solo valore. Questo causa il cambio del valore di quel dato a quel particolare indirizzo.Per essere più chiari, ipotizziamo che il registro DX punti ad un indirizzo di memoria (l'offset, in pratica) 1234h. All'interno di questa cella di memoria è presente il valore 10h.
In assembly l'operatore di deriferimento è la parentesi quadrata; quando racchiudiamo dei registri o dei valori tra parentesi quadrate, stiamo accedendo alla memoria puntata da quel valore. Il valore è quindi 10h nel nostro caso, e l'indirizzo di memoria in cui risiede il dato è 1234h.
Codice:
DX 1234h
[DX] 10h
Linguaggio Assembly
Codice macchina ed assembly sono la stessa cosa?
No, o almeno non esattamente. Il linguaggio Assembly è di basso livello, si interfaccia bene con la macchina, e le sue istruzioni sono degli mnemonici; l'assembly ha corrispondenze dirette con il linguaggio macchina.
Come vedremo in seguito, un'istruzione come B4 09 corrisponde in Assembly ad una MOV che coinvolge il registro AH ed il valore immediato 09h (con "immediato" si fa riferimento a valori hard-coded, a delle costanti numeriche). L'istruzione precisamente è la seguente mov ah, 09h
Assembly ed Assembler sono sinonimi?
Anche in questo caso la risposta è No.
Assembly è il linguaggio; Assembler (assemblatore) è invece il software utilizzato per trasformare il codice sorgente Assembly in codice oggetto. Quindi si può dire che l'assembler è un compilatore per il linguaggio assembly, solo meno sofisticato di un compilatore per C, C++ e gli altri. La maggior semplicità deriva dalla vicinanza che ha assembly con le istruzioni in codice macchina.
Assembly dispone di alcuni mnemonici. Qui farò sempre riferimento alla sintassi di Intel, che è la più utilizzata. In tutti i casi in cui sono coinvolte due istruzioni, il primo operando prende il nome di "destinazione" ed il secondo di "sorgente". Alcune istruzioni richiedono invece solo 1 operando.Alcuni degli mnemonici sono: MOV, ADD, DIV, SUB, INC, DEC,PUSH, POP, JMP, Jxx
L'istruzione MOV opera spostamenti tra registro/registro, tra registro/memoria, registro/immediato, memoria/immediato. Come vedremo in seguito dispone di codici differenti per ciascuna di queste operazioni, e di altri "speciali" (più brevi) utilizzati con il registro accumulatore (AX).
L'istruzione ADD svolge l'addizione tra due numero, e salva il risultato nel primo operando (destinazione).
L'istruzione DIV opera la divisione tra due numeri. Ha un solo operando, poichè l'altro è preso di default. L'operando di default è il registro AX, mentre il secondo deve essere specificato. Prima del suo utilizzo è necessario azzerare il registro DX, che conterrà poi il resto della divisione.
L'istruzione SUB opera una sottrazione tra due numeri; il primo operando è sempre quello di destinazione (dove verrà salvato il risultato).
Le istruzioni INC e DEC operano rispettivamente l'addizione e la sottrazione di 1 solo unità, ed usano solo un operando inc ax causa l'incremento del valore di AX.
Le istruzioni PUSH/POP sono molto importanti (anche su 32bit). Queste istruzioni operano sulla memoria (sullo Stack). Entrambe utilizzano solo 1 operando. Utilizzando PUSH si inserisce il valore passato sullo stack; questo causa l'incremento di un registro puntatore (SP). L'istruzione POP estrae dallo stack il valore PUSHato per ultimo. Vale la pena far notare che lo Stack è di fatto una struttura dati LIFO, vale a dire che l'ultimo elemento inserito sullo stack è il primo ad essere estratto.Le istruzioni JMP e quelle con forma Jxx le incontreremo più spesso nella parte dedicata al 32bit, ma svolgono di fatto le stesse operazioni a 16 ed a 32bit.JMP prende il nome di salto incondizionato, in quanto opera un salto senza tenere in considerazione i flags del registro FLAGS.
Le istruzioni con forma Jxx (come JE, JNE, JL, JNL, JG,...) provocano invece quello che viene definito salto condizionato, in quanto l'effettivo salto dipende dai flags del registro FLAGS. Se il bit coinvolti sono settati (o non settati, dipende dal tipo di salto), l'istruzione provoca un salto ad un altro indirizzo.Prima di utilizzare una di queste istruzioni condizionato, si utilizza di norma l'istruzione CMP o l'istruzione TEST.Dati gli scopi dell'articolo, la trattazione di Assembly termina qui; è inutile essere più specifici.
CISC e RISC
I primi elaboratori erano nettamente meno complessi dei processori della nostra epoca, ed erano meno complessi anche dell'8086. Questi elaboratori funzionavano per mezzo di schede programmate; il programma sulla scheda veniva scritto in base alle necessità. I processori moderni invece eseguono le più svariate operazioni.
Possiamo distinguere inoltre due tipi di architetture: l'architettura RISC, e l'architettura CISC.
Le istruzioni RISC (Reduced Instruction Set Computer) sono più rapide da eseguire, e sono anche più semplici rispetto alle CISC. Gli opcodes di macchine con architettura RISC (in breve ogni opcodes è una differente operazione, come somma, addizione, spostamento di dati, etc) sono di norma un numero molto inferiore. MIPS e SPARC sono due esempi di RISC. Il numero di opcodes non raggiunge il centinaio. L'architettura ARM, ad esempio, è RISC.
Le istruzioni CISC (Complex Instruction Set Computer) sono più lente da eseguire, e sono complesse da interpretare. Gran parte dei processori attuali utilizza questa progettazione, ma esistono anche altri set di istruzioni. Il set di x86 utilizza CISC, ed è proprio il set su cui focalizzeremo la nostra attenzione. La CPU 8086 dispone di un numero di opcodes molto elevato, circa 400. La nascita di CISC la si deve al bisogno di istruzioni in qualche modo vicine ai costrutti utilizzati ad alto livello da un punto di vista semantico; inoltre, il peso del codice risultante è ridotto.
La tecnica utilizzata per la loro codifica permette di ottenere un codice macchina molto compatto. Ogni istruzione è convertita in un codice che occupa un determinato numero di byte (nel caso di 8086 al massimo 2); la CPU sa cosa eseguire decodificando ogni byte (il prossimo paragrafo tratterà il codice macchina, e sarà chiara questa strutura).
Attualmente l'approccio utilizzato lo si potrebbe definire ibrido: le istruzioni sono CISC, ma le più semplici vengono tradotte in micro-codice e di fatto interpretate come RISC (essendo più veloce la sua esecuzione); cito da un mio precedente articolo (presente su un altro forum, al momento):
I processori IA-32 infatti usano il microcodice per implementare le istruzioni più complesse. Il microcode è salvato all'interno di una ROM; ogni istruzione ha quindi il corrispondente microcode. Dato che l'accesso continuo alla ROM produrrebbe dei rallentamenti, vi è una cache apposita che memorizza le istruzioni a cui la CPU accede più spesso.
Non è scopo di questo articolo occuparsi del µ-ops (Micro-Ops), quindi procederemo oltre senza ulteriori puntualizzazioni.
Conclusione Prima Parte
La prima parte termina qui.
La seconda è in realtà già scritta, ma aspetterò un pò a postarla. Nella seconda, come da indice, ci occuperemo della struttura delle istruzioni vere e propri, e per concludere ci saranno alcuni esempi in linguaggio macchina.
Spero sia stato di vostro gradimento, alla prossima!
Ultima modifica da un moderatore: