Codice assembly
Codice macchina e codice assembly
In un calcolatore, le istruzioni macchina sono in codice binario. Esse vengono decodificate direttamente dalla cpu (hardware) che poi esegue le istruzioni operando sui dati (registri e memoria).
Possiamo in teoria usare direttamente il linguaggio macchina (che nella quasi totalità delle implementazioni è come sappiamo in codice binario), ma per rendere questo codice comprensibile agli esseri umani, senza perdere in validità, gli sviluppatori preferiscono usare il linguaggio assembly, che da una corrispondenza delle istruzioni codice macchina con codici alfanumerici che rappresentano istruzioni, registri e valori).
In questa lezione introduciamo una versione puramente didattica del linguaggio assembly, con un set limitato di istruzioni.
Nel presentare le istruzioni si ricorda che:
– la CPU presenta i registri di servizio PC, IR e SP (Program Counter, Instruction Register e Stack Pointer)
– la CPU presenta i registri dati R0… R9
– la memoria centrale è esterna alla cpu e ci si accede per caricare/scaricare celle da/verso i registri.
In linguaggio assembly ogni istruzione ha la forma:
#INDIRIZZO: CODICEISTRUZIONE operando1, operando2, operando3
dove gli operandi possono essere da 0 a 3 (a seconda dell’istruzione).
Gli operandi possono essere:
– registri: nominati con prefisso $: ad esempio $R5, prende il valore contenuto nel registro
– valori assoluti: si indica il numero senza prefissi (es. “5”)
– indirizzi di memoria: si indica il numero con prefisso # (es. “#250” dove 250 è l’indirizzo di una cella di memoria)
Vediamo quindi le istruzioni:
Operazioni | Elenco |
operazioni aritmetiche (somma, prodotto, ecc.) e di confronto tra operandi, eseguite dalla ALU: | ADD, SUB, MUL, DIV il primo operatore è dove si memorizza il risultato (sempre un registro), gli altri due sono gli operandi (possono essere o registri o valori assoluti)ad esempio: ADD $r0, 1, $r1 -> salva in $r0 il valore di $r1 + MULT $r5, 2, $r3 -> moltiplica $r3 * 2 e salva in $r5 CMP (compare)compara due valori.ad esempio: CMP $r0, $r2, $r5 -> metto in $r5 il valore 0 se sono uguali, altrimenti -1 se minore, +1 se maggiore |
operazioni di controllo, ad esempio salti ad una istruzione specifica (a seconda di una condizione o liberi), eseguiti dalla CU | JMP (salto)es: JMP #50 -> salta all’indirizzo 50 BNE (salto condizionato su diseguaglianza)es. BNE $r0, 5, #81 -> salta all’indirizzo 81 se r0 è diverso da 5 BEQ (salto condizionato su uguaglianza)Es: BEQ $r0, 3, $r1 – > salto all’indirizzo contenuto nel registro r1 se r0 è uguale a 3 |
operazioni di caricamento di registri dalla memoria e viveversa, eseguiti dalla CU | STO sorgente destinazione (save), LOD destinazione sorgente (load)carica e salva un dato dalla memoriaEs: STO $r0 #451 -> memorizzo il valore di $r0 nella cella #451LOD $r6 $r2 -> carico il valore all’indirizzo indicato da r2 in r6 |
Subroutine | CAL XXX: questa istruzione salva all’indirizzo $SP l’indirizzo dell’istruzione successiva, decrementa di 1 $SP ed esegue una jump all’indirizzo XXX.RET Questa istruzione copia in $PC il valore di $SP L’istruzione gestisce da sola l’incremento dello stack pointer. E’ necessario l’utilizzo del registro $SP per eseguire push e pop, aggiornando la posizione dello stack dopo ogni push e pop. Si ricorda che lo stack cresce dall’alto verso il basso (quindi $SP diminuisce di 1 con push, aumenta di 1 con pop). Es: CAL #212 -> memorizza indirizzo di ritorno in stack, aggiorna lo stack pointer, ed esegue un salto all’indirizzo 212.RET -> carica l’indirizzo di ritorno dallo stack, aggiorna lo stack (pop) ed esegue il salto all’istruzione indicata |
Un esempio pratico
Esempio: vogliamo memorizzare la somma degli interi da 1 a n (cioè 1+2+3+…+n).
Qui il codice C++:
int sum(int n) {
int result = 0;
for (int i=0; i<n; i++) {
result += i;
}
return result;
}
Useremo i seguenti registri per memorizzare il valore delle variabili:
$r0 -> n
$r1 -> result
$r2 -> i
$r3 -> registro di servizio che useremo per gestire il ciclo
$r8 -> contiene l’indirizzo di memoria fisica dove si trova n (inserito dall’utente)
$r9 -> contiene l’indirizzo di memoria fisica dove memorizzare il risultato result alla fine
Vediamo il codice (che ipotizziamo partire dall’indirizzo #10)
#10: LOD $r0, $r8 carichiamo in $r0 il valore della cella di memoria il cui indirizzo è in $r8
#11: LOD $r1, 0 carichiamo in $r1 il valore 0 (valore iniziale di result)
#12: LOD $r2, 0 carichiamo in $r2 il valore 0 (valore iniziale di i)
#13: ADD $r2, $r2, 1 incrementiamo $r2 di 1
#14: ADD $r1, $r1, $r2 sommiamo a $r1 il valore di $r2
#15: SUB $r3, $r0, $r2 memorizziamo in $r1 il risultato della sottrazione $r0 - $r2 (n-i)
#16: BNE $r3, 0, #13 se il risultato non è 0 (cioè se n-i != 0) allora saltiamo all'istruzione #13 (3 è l'etichetta) e il ciclo ricomincia
#17: STO $r1, $r9 memorizza nell'indirizzo di memoria indicato da $r9 il valore di $r1, cioè il risultato
TPSI – Elenco istruzioni assembly
Elenco istruzioni con tutti gli operatori
Istruzione | Esempi | Significato |
ARITMETICO-LOGICHE | ||
ADD registro, operando1, operando2 | ADD $r0, 1, 2 ADD $r0, $r1, $r2 ADD $r0, $r1, 1 | Somma.Operando1 e operando2 possono essere valori dei registri o valori assoluti |
SUB registro, operando1, operando2 | SUB $r0, 1, 2 SUB $r0, $r1, $r2 SUB $r0, $r1, 1 | Sottrazione.Operando1 e operando2 possono essere valori dei registri o valori assoluti |
MUL registro, operando1, operando2 | MUL $r0, 1, 2 MUL $r0, $r1, $r2 MUL $r0, $r1, 1 | Moltiplicazione.Operando1 e operando2 possono essere valori dei registri o valori assoluti |
DIV registro, operando1, operando2 | DIV $r0, 1, 2 DIV $r0, $r1, $r2 DIV $r0, $r1, 1 | Divisione.Operando1 e operando2 possono essere valori dei registri o valori assoluti |
CMP registro, operando1, operando2 | CMP $r0, 1, 2 CMP $r0, $r1, $r2 CMP $r0, $r1, 1 | Comparazione. Operando1 e operando2 possono essere valori dei registri o valori assoluti.Il risultato è:-1 se operando1 < operando20 se uguali+1 se operando2 > operando1 |
SALTO | ||
JMP operando | JMP $r0JMP #232 | Salto.L’operando può essere un registro (il cui valore sarà considerato un indirizzo) o un valore assoluto. |
BNE operando1, operando2, operando3 | BNE $r0, $r1, #232BNE $r0, $r1, $r3BNE $r0, 5, #232BNE 3, $r1, $r1 | Salto condizionato: eseguito se i valori degli operandi non sono uguali. Operando1 e operando2 possono essere valori dei registri o valori assoluti.Operando3 può essere un indirizzo assoluto oppure un registro (il cui valore sarà considerato un indirizzo) |
BEQ operando1, operando2, operando3 | BEQ $r0, $r1, #232 BEQ $r0, $r1, $r3 BEQ $r0, 5, #232 BNE 3, $r1, $r1 | Salto condizionato: eseguito se i valori degli operandi sono uguali. Operando1 e operando2 possono essere valori dei registri o valori assoluti.Operando3 può essere un indirizzo assoluto oppure un registro (il cui valore sarà considerato un indirizzo) |
CAL indirizzo | CAL $r0CAL #300 | Esegue una push sullo stack con il valore dell’indirizzo dell’istruzione successiva, ed esegue un salto alla destinazione indicata come operando. |
RET | RET | Esegue una pop dallo stack di un indirizzo, e vi esegue un salto. |
MEMORIA | ||
STO sorgente, destinazione | STO $r0, $r1 STO 5, $r1 STO 5, #128 STO $r1, #421 | Salva un valore in memoria. La sorgente può essere un registro o un valore assoluto.La destinazione può essere un registro (il cui valore sarà considerato un indirizzo), o un indirizzo assoluto. |
LOD destinazione, sorgente | LOD $r0, $r1 LOD $r1, 5 LOD $r2, #128 | Carica un valore dalla memoria. La sorgente può essere un registro (il cui valore sarà considerato un indirizzo), un valore assoluto o un indirizzo assoluto. La destinazione deve essere un registro. |
Note:
– $ indica il prefisso di un registro, # indica un prefisso di un indirizzo, senza prefissi è un valore assoluto;
– i registri della CPU sono PC (program counter), IR (istruzione corrente) e SP (indirizzo prima cella libera dello stack), ed infine R0…R9 (registri dati);
– si ricorda che in condizioni normali al termine di una istruzione, il PC viene incrementato di 1;
– l’esecuzione di una istruzione di salto consiste nel sovrascrivere il PC con l’indirizzo verso cui bisogna saltare;
– occorre fare attenzione al contenuto dei registri:
– nelle istruzioni aritmetico-logiche, il contenuto del registro è considerato un valore;
– nelle istruzioni di salto, quando viene usato un registro per indicare l’istruzione verso cui saltare, il valore sarà considerato un indirizzo (quindi “5” sarà considerato “#5”).
– nelle istruzioni di memoria, la sorgente è sempre un valore, la destinazione è sempre un indirizzo.
– la PUSH verso lo stack è sempre data dalla coppia di istruzioni:
STO $sp, value |
SUB $sp, $sp, 1 |
in quanto è necessario aggiornare SP alla prima posizione libera.
– la PULL dallo stack è sempre data dalla coppia di istruzioni:
ADD $sp, $sp, 1 |
LOD $rX, $sp |
dove RX è un generico registro. E’ necessario in questo caso aggiornare SP all’ultima posizione occupata.
In conclusione
– per un semplice ciclo sono necessarie più operazioni a basso livello rispetto a quelle ad alto livello.
– il codice macchina (di cui vediamo) è composto da istruzioni semplici e poco costose da realizzare
– le istruzioni hanno lo stesso tempo di esecuzione
Un set di istruzioni reale è molto più ampio, e si diversifica per architettura, secondo tecnologie che si sono evolute soprattutto con l’avvento dei microprocessori e le architetture moderne, come RISC e CISC (che differivano proprio per la complessità delle istruzioni) con le loro più recenti evoluzioni (X86 -tecnologia del mercato desktop – e ARM – tecnologia del mercato mobile).