ISTRUZIONI E PSEUDOISTRUZIONI MIPS
ARITMETICA
add $s1, $s2, $s3 s1 ← s2 + s3 Addizione
addu $s1, $s2, $s3 s1 ← s2 + s3 Addizione naturale
addi $s1, $s2, cost s1 ← s2 + cost Addizione costante
addiu $s1, $s2, cost s1 ← s2 + cost Addizione costante naturale
sub $s1, $s2, $s3 s1 ← s2 - s3 Sottrazione
subu $s1, $s2, $s3 s1 ← s2 - s3 Sottrazione naturale
subi $s1, $s2, cost s1 ← s2 - cost Sottrazione costante Ψ
subiu $s1, $s2, cost s1 ← s2 - cost Sottrazione costante naturale Ψ
mult $s1, $s2 hi | lo ← s2 * s3 Moltiplicazione
mul $s1, $s2, $s3 s1 ← s2 * s3 Moltiplicazione Ψ
div $s1, $s2 hi ← s2 % s3 Divisione
lo ← s2 / s3
div $s1, $s2, $s3 s1 ← s2 / s3 Divisione Ψ
neg $s1, $s2 s1 ← -s2 Negazione aritmetica Ψ
LOGICA
and $s1, $s2, $s3 s1 ← s2 & s3 AND logico
andi $s1, $s2, cost s1 ← s2 & cost AND logico costante
or $s1, $s2, $s3 s1 ← s2 | s3 OR logico
ori $s1, $s2, cost s1 ← s2 | cost OR logico costante
nor $s1, $s2, $s3 s1 ← !(s2 | s3) NOR logico
not $s1, $s2 s1 ← !s2 NOT logico Ψ
sll $s1, $s2, cost s1 ← s2 << cost Shift a sinistra
srl $s1, $s2, cost s1 ← s2 >> cost Shift a destra
CONFRONTO
slt $s1, $s2, $s3 s1 = 1 if s2 < s3 Poni a 1 se <
sltu $s1, $s2, $s3 s1 = 1 if s2 < s3 Poni a 1 se < naturale
slti $s1, $s2, cost s1 = 1 if s2 < cost Poni a 1 se < costante
sltiu $s1, $s2, cost s1 = 1 if s2 < cost Poni a 1 se < costante naturale
SALTO CONDIZIONATO
beq $s1, $s2, addr if s1 == s2 Salta se uguale
bne $s1, $s2, addr if s1 != s2 Salta se diverso
blt $s1, $s2, addr if s1 < s2 Salta se minore Ψ
bgt $s1, $s2, addr if s1 > s2 Salta se maggiore Ψ
ble $s1, $s2, addr if s1 <= s2 Salta se minore o uguale Ψ
bge $s1, $s2, addr if s1 >= s2 Salta se maggiore o uguale Ψ
SALTO INCONDIZIONATO
j addr PC = addr Salto incondizionato
jr $ra PC = $ra Ritorno da funzione
jal addr PC = addr e salva $ra Chiamata a funzione
TRASFERIMENTO MEMORIA
lw $s1, offset($s2) s1 ← mem(s2 + offset) Carica word (4 byte)
sw $s1, offset($s2) mem(s2 + offset) ← s1 Memorizza word (4 byte)
lh,lhu $s1, offset($s2) s1 ← mem(s2 + offset) Carica half word (2 byte)
sh $s1, offset($s2) mem(s2 + offset) ← s1 Memorizza half word (2 byte)
lb,lbu $s1, offset($s2) s1 ← mem(s2 + offset) Carica byte
sb $s1, offset($s2) mem(s2 + offset) ← s1 Memorizza byte
TRASFERIMENTO TRA REGISTRI
mflo $s1 s1 ← lo Copia registro lo
mfhi $s1 s1 ← hi Copia registro hi
lui $s1, cost s1 (upper 16 bit) ← cost Carica costante su upper 16 bit
move $d, $s d ← s Copia registro Ψ
li $d, cost d ← cost (32 bit) Carica intero 32 bit Ψ
la $d, addr d ← addr (32 bit) Carica indirizzo 32 bit Ψ
REGISTRI REFERENZIABILI
0
0 Costante zero
at
1 Riservato per assemblatore e linker
v0-v1
2-3 Valori di ritorno delle funzioni
a0-a3
4-7 Argomenti delle funzioni
t0-t7
8-15 Registri per valori temporanei
s0-s7
16-23 Registri per variabili
t8-t9
24-25 Registri per valori temporanei (extra)
k0-k1
26-27 Riservati per il kernel del sistema operativo
gp
28 Global Pointer
sp
29 Stack Pointer
fp
30 Frame Pointer
ra
31 Return Address
In questo fighissimo corso parleremo dell'architettura dei calcolatori e dei sistemi
operativi. Impareremo a programmare in Assembly! Precisamente quello del processore
MIPS che, a detta del professore, è più semplice e intuitivo del classico e intricato 68000.
È quel mitico processore che è stato usato per le prime PlayStation e poi montato sulla
sonda New Horizons che è andata su Plutone.
L'Architettura è l'idea che ti fai della macchina considerando le istruzioni assembler che
compongono il linguaggio del processore utilizzato: l'accesso e la disponibilità di un certo
numero di registri, un metodo di accesso e utilizzo della memoria, il maneggiamento degli
operandi, la gestione dei tipi... L'idea che ti fai è l'architettura basata sul set di istruzioni
(ISA – Instruction Set Architecture). L'implementazione hardware poi è a se stante: due
processori con lo stesso linguaggio macchina sono uguali anche se hanno
implementazioni hardware differenti. È importante far notare che l'hardware è tanto “hard”
ma lo cambi in un attimo e nessuno se ne accorge. Il software lo chiamano “soft” ma ogni
modifica al software è un'apocalisse.
Il linguaggio ASSEMBLATORE è un linguaggio simbolico che ti permette di programmare
a basso livello in modo piacevole, con parole alfanumeriche che hanno un significato. Il
linguaggio MACCHINA invece è costituito interamente da tonnellate di 0 e di 1 e ti manda
ai pazzi. I due linguaggi sono quasi in corrispondenza 1:1, in pratica nel linguaggio
assemblatore possono esistere delle PSEUDOISTRUZIONI che in realtà equivalgono a
più istruzioni in linguaggio macchina (per esempio, una CPU non ha la moltiplicazione ma
la puoi implementare nel suo assembler con un'istruzione a parte). Come detto,
l'assemblatore è un linguaggio puramente simbolico, per cui possono esistere diverse
sintassi (che so in italiano, in inglese, in urdu o in pashto), ma genererebbe comunque lo
stesso codice macchina.
Durante la compilazione di un programma, naturalmente entra in gioco
l'ASSEMBLATORE che sostituisce i vari riferimenti e istruzioni simboliche con i
corrispettivi valori binari. Tuttavia di solito i programmi in assembly sono molto lunghi,
quindi vengono progettati da più programmatori, ognuno dei quali si dedica ad una parte
del programma. Il problema è che ovviamente ci sono variabili in comune, bisogna mettere
insieme i pezzi e fare in modo che ogni sottoprogramma si riferisca alla stessa variabile. È
qui che entra in gioco il LINKER! Prende i vari pezzi compilati, cerca di capire quali sono
le variabili in comune, ritocca il codice compilato e unisce tutto nell'eseguibile finale.
Esempio di progettazione di un compilatore C: non lo fai subito per una macchina
specifica, ma traduci in una via di mezzo, uno pseudo-assembly generico che "ti inventi tu"
e da quello puoi tradurre più facilmente in linguaggi macchina specifici. Puoi
tranquillamente fare un compilatore che traduce direttamente nel linguaggio macchina
specifico, dipende da cosa vuoi fare, ma la prima scelta è più furba :P
Ogni istruzione è composta da un codice operativo (opcode) che la identifica e dagli
operandi su cui l'istruzione agisce. Gli operandi possono essere impliciti nell'istruzione
stessa, oppure degli scalari numerici, dei registri, delle locazioni di memoria.
Esiste un'architettura generica di RIFERIMENTOH o di VON NEUMANN (anche se non è
esattamente sua) che è abbastanza chiara D: e ci dice che nella CPU oltre ai registri ci
stanno la ALU (che si occupa delle operazioni logico-matematiche), il registro PC
(Program Counter) che punta alla prossima istruzione da eseguire e il registro
contenente l'istruzione corrente. Per ogni istruzione il processore deve decodificare
l'istruzione, estrarre gli operandi, eseguire, memorizzare i risultati e incrementare il PC alla
prossima istruzione. La figata dei bit è che essi rappresentano sia istruzioni che dati,
quindi tecnicamente puoi eseguire un dato con una bella istruzione di salto :D
Un elemento importante nell'architettura di riferimento è l'UNITÀ DI CONTROLLO: un
circuito logico che gestisce i segnali da mandare alle varie periferiche ad ogni ciclo di
clock, il temporizzatore.
Informazioni storiche importanti: i processori vecchi di alcuni decenni fa avevano un'infinità
di istruzioni, tipo una trentina solo per fare somme e tante altre per operazioni inutili, ed
erano quindi macchine lente e complicate. Poi due tizi (guarda caso gli stessi del libro di
testo) hanno pensato che fosse possibile creare una CPU che fosse veloce con poche
istruzioni elementari (piuttosto che centinaia di istruzioni specifiche). Si pensava che non
ce l'avrebbero fatta, e invece hanno sbaragliato la concorrenza, perché con la loro
tecnologia è stato possibile semplificare la progettazione di hardware e compilatori,
massimizzare le prestazioni e diminuire i costi. Intel e IBM hanno dovuto convertirsi a
questi nuovi processori (RISC – Reduced Instruction Set Computer) e, per salvare
capre e cavoli, affiancarli ad un preprocessore che in tempo reale traducesse il vecchio
linguaggio macchina complicato (CISC) in quello semplificato (solo ed esclusivamente per
motivi di compatibilità). Ma erano proprio istruzioni lunghissime, magari da 128 bit e faceva
impazzire tutto, oppure cinquanta alternative per un salto incondizionato D: invece qua
ogni istruzione ha una dimensione fissa quindi non si ha nessun problema nell'estrazione
delle istruzioni. Queste architetture però hanno un vincolo: non è possibile maneggiare i
dati direttamente sulla memoria. Ogni operando dalla memoria deve essere caricato nei
registri, poi la ALU fa il suo dovere, e infine i risultati dai registri vengono messi in
memoria. Sembra uno spreco di risorse, ma nella gestione di grandi dati non è così.
Come detto nel processore MIPS ogni istruzione occupa esattamente 32 bit. Di sicuro dal
libro degli hacker ti ricorderai cosa significano BIG-ENDIAN e LITTLE-ENDIAN. Eh
confessa! Beh, il nostro MIPS è big-endian, anche se è possibile usarlo in little-endian.
Il MIPS dispone di vari registri che si suddividono in REFERENZIABILI e NON
REFERENZIABILI. Questi ultimi in sostanza sono quelli che non si possono toccare. I
primi invece sono 32 e proprio da 32 bit ciascuno. I vari registri sono consigliati per
essere utilizzati ad un certo scopo, ma teoricamente puoi usare che so, lo stack pointer
per memorizzare i valori di ritorno di una funzione. Tuttavia un po' di “igiene mentale” ti
dice di non farlo, anche perché il MIPS è un processore poco protetto, ti lascia fare
stronzate. Se un registro è riservato, NON TOCCARLO. Altrimenti tu fai casino, il
calcolatore fa casino, poi se il programma lo fanno più persone è ancora più casino.
QUALI SONO LE ISTRUZIONI?
Aritmetiche-logiche (compito della ALU)
– Trasferimento da o verso memoria/registri
– Modifica del flusso di esecuzione (salti condizionati e non, sottoprogrammi)
– Trasferimento dati input/output tra periferiche
– Istruzioni speciali di controllo (complicato da spiegare adesso)
–
TUTTE le istruzioni macchina MIPS per semplicità, velocità, prestazioni e minori consumi
hanno le stesse dimensioni: 32 bit, cioè una parola. Di questi i primi 5 sono riservati
all'OPCODE, che dice cosa fa l'istruzione e a cosa servono i bit rimanenti.
Le istruzioni si possono dividere in tre tipi:
Tipo R (register): istruzioni aritmetico-logiche, che coinvolgono la ALU e i registri
– Tipo I (immediate): accesso alla memoria e salti condizionati
– Tipo J (jump): salti incondizionati
–
Esistono cinque modalità di INDIRIZZAMENTO nel MIPS:
Immediato
– L'operando è già inserito nell'istruzione.
A registro
– L'operando è un valore contenuto in un registro.
Con base e spiazzamento
– Un indirizzo viene calcolato usando un registro base a cui si somma un offset.
Relativo al Program Counter
– Come base e spiazzamento, ma relativo al Program Counter
Pseudo-diretto
– Indirizzi assoluti inseriti nel Program Counter
Ora ci facciamo una simpatica carrellata di istruzioni Assembly!
ISTRUZIONI ARITMETICO-LOGICHE
Le istruzioni aritmetico-logiche in MIPS hanno tutte tre operandi, dei quali il primo è il
registro di destinazione (che memorizza il risultato) mentre gli altri due sono i registri
sorgente. Sono quindi istruzioni di tipo R, l'ordine degli operandi è fisso e ricorda
facilmente i linguaggi di alto livello.
add rd, rs, rt # rd ← rs + rt
sub rd, rs, rt # rd ← rs – rt
addu rd, rs, rt # addizione unsigned (tipo I)
subu rd, rs, rt # sottrazione unsigned (tipo I)
and rd, rs, rt # rd ← rs & rt
or rd, rs, rt # rd ← rs | rt
sll rd, rs, 10 # rd ← rs << 10
srl rd, rs, 10 # rd ← rs >> 10
Esistono varianti con operando immediato:
addi rd, rs, cost # rd ← rs + cost
addiu rd, rs, cost # addizione unsigned
Un paio di esempi di traduzione da C a MIPS:
1) R = A + B;
→ add $s0, $s1, $s2
2) A = B + C + D; # A = $s0, B = $s1, C = $s2...
E = F – A;
→ add $t0, $s2, $s3
add $s0, $t0, $s1
sub $s4, $s5, $s0
ISTRUZIONI DI TRASFERIMENTO LOAD/STORE
Sappiamo che nel MIPS non possiamo maneggiare direttamente la memoria, ma
dobbiamo caricare e scaricare i dati con istruzioni apposite. Queste istruzioni hanno due
operandi: il primo è la sorgente/destinazione mentre il secondo è l'indirizzo di memoria
interessato. In realtà hanno un terzo argomento, perché l'indirizzo che cerchiamo viene
identificato con la struttura offset(registro_base) dove l'offset è uno spiazzamento di 16
bit e il registro base è il registro da cui si calcola lo spiazzamento. Tale meccanismo ci
serve perché gli indirizzi di memoria occupano 32 bit, ma noi non possiamo inserire tutti i
32 bit nell'istruzione perché non ci stanno! Allora con questa furbata usiamo l'identificativo
di un registro e un offset, così possiamo navigare nella memoria come vogliamo :D
Queste istruzioni quindi hanno tre argomenti: il registro coinvolto nel trasferimento,
l'offset, il registro base. In generale l'indirizzo della parola di memoria è calcolato come
identificatore + offset + registro base (dove l'identificatore è l'etichetta data ad una certa
variabile la cui posizione verrà calcolata dal linker). Un po' di esempi che è meglio:
lw $t0, ($a0) # senza offset: t0 ← M[a0 + 0]
lw $t0, 20($a0) # con offset: t0 ← M[a0 + 20]
lw $t0, var1 # identificatore: t0 ← M[gp + …]
lw $s1, 100($s2) # s1 ← M[s2 + 100]
sw $s1, 100($s2) # M[s2 + 100] ← s1
Esempio di traduzione C → MIPS:
A[12] = h + A[8]; # h in $s2, &A[0] in $s3
→ lw $t0, 32($s3) # carica A[8] (indirizza i byte!)
add $t0, $t0, $s2
sw $t0, 48($s3)
Insomma un bel macello... Può esserti utile ricordare che, non essendoci la moltiplicazione
e la divisione, puoi fare cose più belle, tipo sommare più volte o, ancora più
intelligentemente, usare uno shift logico per moltiplicare o dividere per potenze di 2 :D
ISTRUZIONI DI SALTO
Le istruzioni di salto condizionato e incondizionato permettono di saltare in giro per il
programma. Come vedi, il tanto odiato goto del C è pane quotidiano in Assembly :D
L'indirizzo della cella di destinazione del salto viene solitamente indicato con un'etichetta.
I salti condizionati sono istruzioni di tipo I e di fatto consistono nell'aggiunta di un offset di
16 bit al Program Counter per modificare il flusso di esecuzione.
I salti incondizionati invece sono istruzioni di tipo J e fanno indirizzamento pseudo-
diretto.
Le istruzioni di salto condizionato (conditional branch) vengono eseguite se una
determinata condizione viene soddisfatta.
beq $s1, $s2, LABEL_1 # if s1 == s2
bne $s1, $s2, LABEL_2 # if s1 != s2
Per le condizioni di maggioranza/minoranza si usano altri metodi simpatici :)
Le istruzioni di salto incondizionato (unconditional branch) vengono sempre eseguite.
j LABEL_1 # salta e basta
jr $ra # salta a indirizzo in $ra
jal LABEL_1 # salta e salva indirizzo in $ra
Queste ultime due istruzioni equivalgono praticamente al ret e call dell'x86.
Esempio di traduzione C → MIPS:
if (i == j) # f in $s0, g in $s1...
f = g + h;
else f = g – h;
→ bne $s3, $s4, ELSE
add $s0, $s1, $s2
j END_IF
ELSE: sub $s0, $s1, $s2
END_IF:
Nota due cose importanti da questo esempio: normalmente non si controlla la condizione
di if ma quella di else per fare un salto, e poi al termine dell'if devi mettere un salto
incondizionato altrimenti quello esegue pure l'else!!!
Per le condizioni è utile il registro $0 che contiene la costante zero e non può essere
modificato. Inoltre il REGISTRO DI STATO contiene delle flag risultati dalle operazioni di
confronto. Alcune di queste sono:
N Risultato negativo
Z Risultato zero
V Segnale di overflow
C Risultato con riporto
Ma come facciamo il maggiore e minore? Con una bella istruzione: slt (set less than) setta
il primo registro a 1 se il secondo è minore del terzo, altrimenti lo mette a zero. Unito a
beq e bne questa istruzione implementa TUTTI gli operatori di confronto!
if (i < j) # i in $s0, j in $s1, k in $s2
k = i + j;
else k = i - j;
→ slt $t0, $s0, $s1 # t0 contiene 1
beq $t0, $0, ELSE
add $s2, $s0, $s1
j END_IF
ELSE: sub $s2, $s0, $s1
END_IF:
PSEUDOISTRUZIONI
Le pseudoistruzioni sono istruzioni fornite dal linguaggio Assembly ma che in realtà non
esistono nel linguaggio macchina. È l'assemblatore che le traduce nelle corrispondenti
istruzioni. Un paio di esempi:
Pseudoistruzione Equivalente reale
move $t0, $t1 add $t0, $0, $t1
mul $s0, $t0, $t1 mult $t1, $t2
mfhi $s0
La move sposta il contenuto del secondo registro nel primo. Questa istruzione in realtà
non esiste perché dovrebbe richiedere tre operandi e non due. L'istruzione effettivamente
tradotta in linguaggio macchina infatti fa una addizione con zero (oppure un or logico).
La mul effettua la moltiplicazione e salva il risultato in un registro: in realtà si usa la mult
che effettua la moltiplicazione e salva il risultato nei registri hi e lo. Con mfhi dici di
salvare il contenuto del registro hi in un altro registro.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
-
Appunti completi Architettura dei Calcolatori, prof. Umberto Villano
-
Appunti Architettura dei calcolatori
-
Appunti
-
Appunti Architettura dei Calcolatori Elettronici
- Risolvere un problema di matematica
- Riassumere un testo
- Tradurre una frase
- E molto altro ancora...
Per termini, condizioni e privacy, visita la relativa pagina.