Basi del linguaggio assembly RISC-V
RISC-V è un linguaggio assembly di tipo RISC ed è basato sul principio che tutte le istruzioni hanno la stessa dimensione: 32 o 64 bit. Questo vincolo su RISC-V rende la progettazione del circuito di decode per RISC-V più semplice rispetto ad altri linguaggi in cui ci possono istruzioni di diversa dimensione (ad esempio l’assembly x86).
Altra caratteristica di RISC-V è il fatto che l’architettura è ti tipo load-store che significa che gli operandi devono tutti trovarsi nei registri prima si possano fare operazioni aritmetico-logiche su di essi. Anche in questo caso la progettazione del circuito interno della CPU ne risulta molto semplificato.
Sintassi delle istruzioni
Il formato delle istruzioni RISC-V a 32 bit comprende diverse modi di rappresentare le istruzioni utilizzando i 32 bit. Ci limitiamo qui a vedere i principali formati che sono 6.
- Registry
- Immediate
- Upper Immediate
- Store
- Branch
- Jump
Nella figura seguente possiamo vedere come ogni singolo di questi formati suddivida i 32 bit in modo da indicare:
opcode
: indica di che istruzione si trattafunct3
e funct
: ulteriori indicazione sul tipo di istruzionerd
: registro di destinazione per i risultatirs1
e rs2
: registri per gli operandiimm
: operandi immediati, vale a dire valori numerici costanti.
Istruzioni RISC-V
Le operazioni basilari presenti nell’interpret RISC-V online sono le seguenti:
Arithmetics: ADD
, ADDI
, SUB
Logical: AND
, ANDI
, OR
, ORI
, XOR
, XORI
Sets: SLT
, SLTI
, SLTU
, SLTIU
Shifts: SRA
, SRAI
, SRL
, SRLI
, SLL
, SLLI
Memory: LW
, SW
, LB
, SB
PC: LUI
, AUIPC
Jumps: JAL
, JALR
Branches: BEQ
, BNE
, BLT
, BGE
, BLTU
, BGEU
È importante sapere che queste non sono tutte le operazioni del linguaggio RISC-V.
Un programma RISC-V
Il seguente programma calcola la somma 1 + 2 + 3 + ... + 7 e mette il risultato nella cella di memoria di indirizzo 0.
ADDI x11, x0, 0
ADDI x10, x0, 7
.loop:
ADD x11, x11, x10
ADDI x10, x10, -1
BNE x10, x0, .loop
SW x11, 0(zero)
Vediamo cosa fa ogni singola istruzione del programma.
Esercizio
Scrivere un programma che fa la somma dei primi n numeri leggendo n dalla cella 0 della memoria e che mette il risultato nella cella 1 della memoria.
Istruzioni aritmetico-logiche
Le istruzioni aritmetico-logiche che consideriamo sono le seguenti.
- Addizione
ADD
e sottrazione SUB
- Operazioni logiche
AND
, OR
e XOR
- Shift aritmetico
SRA
, shift logico sinistro SLL
e shift logico destro SRL
Ognuna di queste istruzione ha lo stesso formato (R-type instructions)
OPER
è il codice dell’operazione (es. ADD
, AND
, …);rd
è il registro di destinazione, cioè il registro nel quale verrà scritto il risultato finale;rs1
e rs2
sono i registri sorgente dai quali vengono presi gli operandi.
Per ognuna delle operazioni aritmetico-logiche sopra elencate, esiste una versione immediate che, anziché avere il secondo registro sorgente rs2
, ha un valore numerico costante a 12 bit.
Esempi
Sommare i valori in x10
e in x11
mettendo il risultato in x12
ADD x12, x10, x11
Mettere il valore 42
nel registro x15
(aggiunge x0=0
a 42
e mettere il risultato in x15
)
ADDI x15, x0, 42
Fare la differenza tra x10-x9
mettendo il risultato in x10
SUB x10, x10, x9
Calcolare il resto della divisione per due di x2
mettendo il risultato in x3
(tale resto è proprio
il valore del bit meno significativo)
ANDI x3, x2, 1
Moltiplicare per 4
il valore di x22
e mettere il risultato in x30
(per moltiplicare per 4 basta
shiftare a sinistra il valore di un due bit)
SLLI x30, x22, 2
Istruzioni Load e Store
Ogni programma utilizza la memoria RAM per depositare i risultati calcolati, per leggere input e per scrivere output, è quindi fondamentale che un linguaggio assembly contenga istruzione per la lettura e la scrittura della memoria. In RISC-V queste di istruzioni vengono dette load e store.
La sintassi di queste istruzioni prevede due versioni: una per spostare da e per la memoria byte, l’altra per spostare parole (word).
Esistono, quindi, quattro tipi di istruzioni di accesso alla memoria: due di caricamento dalla memoria ad un registro rd
e due per il salvataggi in memoria del contenuto del registro rd
Come è facile capire, le due versioni B
e W
si usano per spostare byte o word. Mentre il primo parametro, da noi indicato con rd
,
serve ad indicare il registro nella CPU da cui prendere (store) a su cui mettere (load) il contenuto della memoria, l’indirizzo di memoria, che
noi abbiamo indicato con MEM
, deve essere calcolato utilizzando opportune tecniche di indirizzamento della memoria che discutiamo nel
seguito.
Indirizzamento della memoria
Le istruzioni load e store necessitano di una regola per indicare quale cella di memoria deve essere letta o scritta. La prima cosa che viene in mente è usare istruzioni in cui l’indirizzo viene dato direttamente come numero (es. 1234
per indicare la cella di memoria di indirizzo 1234
). Istruzioni di questo tipo, però, permettono di indicare un numero di celle di memoria che è limitato dal numero di bit che si possono usare nell’istruzione per l’immediate. Ad esempio in una I-instruction in cui l’immediate è di 12 bit, si possono solo indicare solo le celle con indirizzo tra 0 e 4095, questo permetterebbe di scrivere programmi che usano al massimo 4 KByte di memoria dati, piuttosto limitato per qualsiasi programma oggi giorno.
Per poter usare più di 4096 celle di memoria, RISC-V utilizza la tecnica del registro base. In pratica l’indirizzo viene dato indicando un indirizzo di base su un registro e un offset numerico. Per esempio si può indicare un offset di 430 rispetto al registro base x11
. Se in x11
, per esempio, c’è il numero 8500, allora andremmo in questo modo ad utilizzare la cella 8500 + 430 = 8930. Come si vede è possibile in questo modo utilizzare memoria molto grandi, basta impostare il valore del registro base.
Esempio
ADDI x10, x0, 1
SLLI x10, x10, 20
ADDI x11, x0, 123
SB x11, 2(x10)
Le prime due istruzioni fanno si che in x10 finisca il numero 2^20 = 0x100000
, la terza istruzione mette il numero 123
(decimale) in x11 ed infine la quarta istruzione carica il contenuto di x11
(che è 123
) nella cella di indirizzo 2 + x10
, siccome x10 contiene il numero 0x100000
(esadecimale), la cella che verrà scritta è quella di indirizzo 0x100002
(esadecimale). Siamo quindi riusciti a scrivere in una cella il cui indirizzo (circa un milione) è molto più grande del più grande immediate (circa 4000
), questo non sarebbe stato possibile senza la tecnica del registro base.
Esempi
Caricare in x10
il contenuto del byte memoria all’indirizzo 128
LB x10, 128(x0)
Caricare in x10
il contenuto della parola di memoria all’indirizzo 128
LW x10, 128(x0)
Salvare nella parola di memoria all’indirizzo 80
il valore 17
(utilizzando il registro x20
)
ADDI x20, x0, 17
SW x20, 80(x0)
Salvare nel byte di memoria all’indirizzo contenuto in x10
il valore contenuto nel registro x15
SB x15, 0(x10)
Istruzioni di branch e di salto
Non si potrebbero scrivere programmi interessanti in qualsiasi linguaggio di programmazione senza istruzioni di condizione come l’istruzione if o cicli come while. Lo stesso vale per programmi in linguaggio assembly in cui condizioni e cicli si fanno utilizzando istruzioni di salto. Queste istruzioni permettono di “saltare” ad una qualsiasi istruzione purché questa si possa identificare, per fare questo di usano delle etichette associate a quelle istruzioni che si vogliono raggiungere a seguito di un salto. Le etichette non sono altro che dei nomi che vengono date alle istruzioni, in pratica questi nomi indicano il numero dell’istruzione (indirizzo di memoria dove l’istruzione si trova), ovviamente le etichette sono più facili da ricordare dei numeri o degli indirizzi di memoria.
Le operazioni più importanti di salto sono quelle di salto condizionato cioè di salto nel caso in cui si verifica una certa condizione, in RISC-V queste istruzioni vengono anche chiamate istruzioni di branch. Tutti gli assembly contengono anche istruzioni si salto non condizionato che in RISC-V vengono chiamate jump.
Le istruzioni si salto condizionato in RISC-V sono
BEQ
Branch in EQualBNE
Branch if Not EqualBLT
Branch if Less ThanBGE
Branch if Greater than or EqualBLTU
Branch if Less Than UnsignedBGEU
Branch if Greater than or Equal Unsigned
Esempi
Saltare all’etichetta .fine
se il valore del registro x11
è nullo
BEQ x11, x0, .fine
Saltare all’etichetta .inizio
se il valore del registro x11
è non nullo
BNE x11, x0, .inizio
Saltare all’etichetta .fine
se il valore del registro x11
è negativo
BLT x11, x0, .fine
Saltare all’etichetta .loop
se il valore del registro x11
è positivo (si usa l’idea
che x11>0
è equivalente a 0<x11
in modo da usare BLT
)
BLT x0, x11, .loop
Saltare all’etichetta .fine
se il valore del registro x11
è negativo o nullo (anche
qui usiamo il fatto che x11<=0
equivale a 0>=x11
)
BGE x0, x11, .fine
Link