Il set di istruzioni: RISC e CISC, RISC-V e assembly
RISC e CISC, registri di RISC-V, codifica delle istruzioni con esempio in bit, modi di indirizzamento, un programma in C e in assembly, chiamate di funzione e stack.
Il set di istruzioni (ISA, Instruction Set Architecture) è il contratto tra il software e l’hardware: l’elenco delle istruzioni che un processore sa eseguire, come sono codificate in bit, quali registri ci sono e come si indirizza la memoria. Un programma compilato per un’ISA gira su qualsiasi processore che la realizza, dal più economico al più veloce. In questa lezione usiamo come esempio RISC-V, un’ISA aperta e moderna, e la confrontiamo con le architetture più complesse.
RISC e CISC
- Le architetture CISC (Complex Instruction Set Computer), come la x86 dei PC, hanno istruzioni molto varie, di lunghezza variabile, che possono operare direttamente sulla memoria e fare molte cose insieme.
- Le architetture RISC (Reduced Instruction Set Computer), come ARM e RISC-V, hanno poche istruzioni semplici e regolari, tutte della stessa lunghezza; solo le istruzioni di caricamento (load) e memorizzazione (store) accedono alla memoria, tutti i calcoli avvengono tra registri.
Le istruzioni RISC sono più numerose a parità di programma, ma ciascuna è più semplice da decodificare e da eseguire in pipeline:
cresce, il CPI e il periodo di clock calano. Oggi la distinzione è sfumata: i processori x86 traducono internamente le istruzioni complesse in micro-operazioni di tipo RISC, e i telefoni e molti computer portatili usano processori ARM.
I registri di RISC-V
RISC-V a 32 bit (RV32I) ha 32 registri da 32 bit, da x0 a x31. Il registro x0 vale sempre zero, e scriverci non ha effetto: sembra uno spreco, ma permette di ottenere molte operazioni senza istruzioni dedicate (copiare un registro è sommargli zero). Per convenzione gli altri hanno ruoli precisi: ra per l’indirizzo di ritorno, sp per il puntatore allo stack, a0–a7 per gli argomenti delle funzioni, t0–t6 per i valori temporanei, s0–s11 per quelli da preservare.
La codifica delle istruzioni
Tutte le istruzioni sono lunghe 32 bit e hanno pochi formati, con i campi negli stessi posti, così la decodifica è rapida. Il formato R serve per le operazioni tra registri:

Esempio. add x5, x6, x7 (cioè
): opcode 0110011 (operazione tra registri), rd = 5, rs1 = 6, rs2 = 7, funct3 e funct7 a zero (somma, non sottrazione). Mettendo in fila i bit si ottiene la parola 0x007302B3: è questo numero che il processore trova in memoria.
Gli altri formati: I per le operazioni con una costante e i caricamenti (12 bit di costante al posto di rs2 e funct7), S per le memorizzazioni, B per i salti condizionati, U e J per le costanti grandi e i salti lunghi.
I modi di indirizzamento
RISC-V ne ha pochissimi:
- immediato: la costante sta nell’istruzione,
addi t0, t0, 4; - registro:
add a2, a2, t0; - base più spiazzamento: l’indirizzo è un registro più una costante,
lw t0, 8(a0)legge la parola all’indirizzo
. Basta per accedere agli elementi di un vettore e ai campi di una struttura; - relativo al PC: i salti indicano di quanto spostarsi rispetto all’istruzione attuale, così il codice funziona in qualsiasi posizione della memoria.
Le architetture CISC aggiungono modi più ricchi (indiretto, indicizzato con scala, autoincremento), che risparmiano istruzioni ma complicano l’hardware.
Un esempio di programma
La somma degli elementi di un vettore di interi. In C:
int somma(int *v, int n) {
int s = 0;
for (int i = 0; i < n; i++)
s += v[i];
return s;
}
In assembly RISC-V, con a0 = indirizzo del vettore, a1 = numero di elementi, risultato in a0:
somma: li t1, 0 # s = 0
ciclo: beqz a1, fine # se n == 0 esci
lw t0, 0(a0) # t0 = *v
add t1, t1, t0 # s += t0
addi a0, a0, 4 # v++ (un int occupa 4 byte)
addi a1, a1, -1 # n--
j ciclo
fine: mv a0, t1 # valore restituito
ret
Si vede il lavoro del compilatore: l’indice è sparito, sostituito da un puntatore che avanza di 4 byte; il ciclo usa cinque istruzioni per elemento. Ogni istruzione fa una cosa sola, e proprio per questo il processore può eseguirne diverse in sovrapposizione.
Le chiamate di funzione e lo stack
jal ra, f salta alla funzione f salvando in ra l’indirizzo di ritorno; ret (cioè jalr x0, 0(ra)) torna indietro. Se una funzione ne chiama un’altra, deve salvare ra e i registri s che usa in una zona di memoria gestita a pila, lo stack: si fa spazio sottraendo a sp e si ripristina alla fine. Le regole su quali registri preservare formano la convenzione di chiamata, che permette a funzioni compilate separatamente, anche in linguaggi diversi, di chiamarsi tra loro.





