Codice in C:
long moltiplicazione(long a, long b){
long rst = a;
for(long i = 1; i < b; i++){
rst = rst + a;
}
return rst;
}
long area(long base, long altezza){
long rst = moltiplicazione(base, altezza)/2;
return rst;
}
...
printf("Area = %li\n", area(20, 23));
...
Si supponga di non avere un operatore per la moltiplicazione.
Guardando questo codice, vediamo che vi è un punto del programma (printf()
) in cui viene chiamata la funzione area()
, tale funzione a sua volta chiama la funzione moltiplicazione()
(funzione foglia) che restituisce il risultato della moltiplicazione tra due numeri.
In RISC-V dovremo sicuramente scrivere le due funzioni moltiplicazione
e area
e poi chiamarle.
Partiamo dal punto di ingresso di RISC-V (_start
) dove faremo in modo di chiamare le funzioni e stampare a video il risultato.
Il codice sarà presentato in maniera evolutiva, arricchito delle varie aggiunte opportunamente spiegate.
_start:
li a0, 20 # salvataggio della base in a0
li a1, 23 # salvataggio dell'altezza in a1
Per passare i parametri ad una procedura, essi vanno salvati nei registri che vanno da a0-a7
.
Vedi tabella dei registri e delle convenzioni sul loro utilizzo.
_start:
li a0, 20 # salvataggio della base in a0
li a1, 23 # salvataggio dell'altezza in a1
# chiamata della procedura: area(base, altezza)
jal ra, area
add t0, a0, zero
Visto che viene effettuata una chiamata a procedura, viene salvato nel registro ra
l'indirizzo dell'istruzione successiva, in modo che l'esecuzione del codice possa poi continuare, alla chiusura della procedura chiamata, dall'istruzione successiva alla chiamata. L'istruzione successiva non è altro che il salvataggio in t0
del valore di ritorno della chiamata che viene salvato in a0
come da convezione (vedi tabella).
_start:
li a0, 20 # salvataggio della base in a0
li a1, 23 # salvataggio dell'altezza in a1
# chiamata della procedura: area(base, altezza)
jal ra, area
add t0, a0, zero # a0 viene salvato in t0
# stampa del messaggio: Area =
la a0, visris # caricamento della stringa dalla memoria in a0
addi a7, zero, 4 # e stampa di tale stringa
ecall
# stampa del risultato
add a0, t0, zero # a0 contiene il valore di t0
addi a7, zero, 1 # a0 viene stamapato
ecall
# stampa \n
la a0, RTN
addi a7, zero, 4
ecall
# exit
addi a7, zero, 10
ecall
Gli ultimi pezzi di codice aggiunti non sono altro che la stampa a video del risultato, che per gli scopi del nostro corso non ricoprono una grande importanza, ma che per completezza illustreremo lo stesso.visris
è una pseudo-istruzione che consente di mostrare a video dei simboli, in questo caso la stringa Area = (salvata in memoria), infatti viene fatta una operazione di caricamento dalla memoria la
, nel registro a0
, poi viene settato il registro a7
a 4 (addi a7, zero, 4
), nel registro a7
viene specificato il codice della system call che il sistema operativo deve chiamare, in questo caso: 4 indica la system call print string. ecall
effettua la chiamata basandosi sui settaggi del registro a7
.
Stessa cosa avviene per la stampa del valore di ritorno e di \n
e la exit
.
In questa fase descriviamo la chiamata alla procedura area e i passi che devono essere eseguiti quando si effettua una generica chiamata a procedura.
area:
# creazione del frame di area sullo stack (24 byte = ra + fp + rst)
# lo stack cresce verso il basso
addi sp, sp, -24 # allocazione call frame nello stack
sd fp, 0(sp) # salvataggio del precedente frame pointer
sd ra, 8(sp) # salvataggio dell'indirizzo di ritorno
addi fp, sp, 16 # aggiornamento del frame pointer
Con queste righe di codice viene creato un nuovo frame.
La dimensione di tale frame è di 24 byte:
addi sp, sp, -24
viene esteso lo spazio dello stack per allocare il frame.fp
viene salvato l'indirizzo del precedente frame pointer.sp
punta ad una nuova zona di memoria:sd fp, 0(sp)
, fp
viene salvato in 0(sp)
.8(sp)
.rst
viene salvata in 16(fp)
.0(sp)
ci riferiamo alla 0-esima double word sopra sp
.Ma dove punta fp
?
Con l'istruzione addi fp, sp, 16
, viene aggiornato il valore del frame pointer che adesso punta a sp
+ 16 = -8. Ovvero due double word sopra sp
.
Sappiamo che lo stack cresce verso il basso, di fatto, quando abbiamo allocato nuovo spazio per lo stack, non abbiamo fatto altro che sommare allo stack pointer -24.
Lo stack è confusionario. Cresce verso indirizzi di memoria più bassi.
area:
# creazione del frame di area sullo stack (24 byte = ra + fp + rst)
# lo stack cresce verso il basso
addi sp, sp, -24 # allocazione call frame nello stack
sd fp, 0(sp) # salvataggio del precedente frame pointer
sd ra, 8(sp) # salvataggio dell'indirizzo di ritorno
addi fp, sp, 16 # aggiornamento del frame pointer
# calcolo dell'area
jal ra, moltplicazione
sd a0, 0(fp)
srai a0, a0, 1
Nel calcolo dell'area viene effettuata un'altra chiamata a procedura moltiplicazione. In a0
si troverà il valore ritornato da tale chiamata. Esso viene salvato in 0(fp)
, ovvero nella variabile rst
.
Viene poi implementato un shift aritmetico a destra per effettuare la divisione per due del risultato per calcolare l'area del triangolo (vedi shift aritmetico a destra).
area:
# creazione del frame di area sullo stack (24 byte = ra + fp + rst)
# lo stack cresce verso il basso
addi sp, sp, -24 # allocazione call frame nello stack
sd fp, 0(sp) # salvataggio del precedente frame pointer
sd ra, 8(sp) # salvataggio dell'indirizzo di ritorno
addi fp, sp, 16 # aggiornamento del frame pointer
# calcolo dell'area
jal ra, moltplicazione
sd a0, 0(fp)
srai a0, a0, 1
# uscita dalla funzione
ld fp, 0(sp) # recupero del vecchio frame pointer
ld ra, 8(sp) # recupero dell'indirizzo di ritorno
addi sp, sp, 24 # distruzione del frame di attivazione
jr ra # salto all'indirizzo di ritorno (ritorno al chiamante)
moltiplicazione:
# creazione del frame di area sullo stack (16 byte = fp + rst)
# lo stack cresce verso il basso
addi sp, sp, -24 # allocazione call frame nello stack
sd fp, 0(sp) # salvataggio del precedente frame pointer
addi fp, sp, 8 # aggiornamento del frame pointer
sd a0, 0(fp) # salvataggio di a0 in memoria (in 0(fp))
Per il frame di questa procedura vengono allocati solo 16 byte, perché trattandosi di una procedura foglia non è necessario tenere traccia dell'indirizzo di ritorno, poiché non verranno chiamate altre procedure che modificheranno tale registro. Bisogna tenere traccia nello stack solo del vecchio frame pointer e della variabile locale rst.
moltiplicazione:
# creazione del frame di area sullo stack (16 byte = fp + rst)
# lo stack cresce verso il basso
addi sp, sp, -24 # allocazione call frame nello stack
sd fp, 0(sp) # salvataggio del precedente frame pointer
addi fp, sp, 8 # aggiornamento del frame pointer
sd a0, 0(fp) # salvataggio di a0 in memoria (in 0(fp))
ld t2, 0(fp)
li t0, 1 # indice del for scritto in t0
for:
bge t0, a1, endfor
add t2, a0, t2
addi t0, t0, 1
j for
endfor:
ld fp, 0(sp) # recupero del vecchio frame pointer
addi sp, sp, 16 # distruzione del frame di attivazione
add a0, t2, zero # viene salvato il valore di ritorno in a0
jr ra # ritorno al chiamante
Il resto del codice è un normale ciclo for e alla fine viene deallocato il frame di attivazione.