Funzioni e stack

Funzioni e stack

Una funzione è un blocco denominato di istruzioni che puoi usare più di una volta. Ci salti, essa svolge il suo compito e restituisce il controllo al punto da cui l'hai chiamata. Questo ti permette di scrivere una parte di logica una sola volta e usarla da molti punti invece di ripetere le stesse righe in tutto il programma.

Usa per chiamare una funzione: salta all'etichetta e memorizza la posizione di ritorno in ra. La funzione termina con , che salta indietro all'indirizzo in ra. e sono pseudo-istruzioni: nomi comodi che l'assembler espande nelle istruzioni effettive, e .

Gli argomenti vengono inseriti in a0, a1 e nei registri che seguono. Il valore di ritorno viene inserito in a0. Questa disposizione è la convenzione di chiamata, l'accordo condiviso che permette a parti separate di codice di lavorare insieme.

Una funzione square()
li a0, 6 # argomento
call square # chiama, e ra ricorda dove tornare
li a7, 1 # square lascia il suo risultato in a0
ecall
li a7, 10
ecall
square:
mul a0, a0, a0 # a0 = a0 * a0
ret # torna a ra

Segui la chiamata. inserisce l'argomento 6 in a0. salta all'etichetta square e memorizza l'indirizzo di ritorno in ra. All'interno della funzione, calcola 6 * 6 = 36 e lascia il risultato in a0, il registro usato per i valori di ritorno. salta indietro all'istruzione subito dopo la chiamata, dove e stampano 36, e poi il programma termina.

Chiamata di funzione e ritorno
caller
call A
...
A
addi sp, sp, -4
sw ra, 0(sp)
call B
lw ra, 0(sp)
addi sp, sp, 4
ret
B
mul a0, a0, a0
ret
STACK
sp →
0x7ffffff0
0x7fffffec

Il chiamante sta per chiamare A.

Osserva la riga evidenziata compiere l'intero viaggio di andata e ritorno. caller esegue , che salva automaticamente il punto di ritorno a caller nel registro ra. Prima che A possa eseguire , incontra un problema: l'istruzione sta per sovrascrivere ra con un nuovo indirizzo di ritorno, distruggendo quello che A necesita per tornare a casa. Per risolvere questo, A libera spazio sullo stack () e vi salva il ra originale (). Ora è sicuro eseguire . B viene eseguito e ritorna con un normale , atterrando di nuovo dentro A. Infine, A ripristina il ra originale dallo stack (), ripulisce il puntatore dello stack (), e il suo stesso usa quel valore ripristinato per saltare con successo indietro a caller. Se A avesse saltato questo passaggio, l'istruzione avrebbe sovrascritto permanentemente ra, e il finale di A sarebbe saltato nel posto sbagliato invece di tornare a casa.

Lo stack

I registri disponibili sono pochi, quindi le funzioni che ne chiamano altre hanno bisogno di spazio temporaneo. Lo stack è memoria che si prende in prestito tramite il puntatore allo stack sp: sottrarre per fare spazio, memorizzare ciò che deve essere conservato, ripristinarlo prima di .

Lo stack è una struttura Last-In, First-Out (LIFO): il valore memorizzato più di recente è il primo che recuperi. In RISC-V, lo stack cresce verso il basso nella memoria. Sottrarre da sp riserva nuovo spazio che è sicuro usare. Quando riprendi i valori, devi farlo nell'ordine inverso rispetto a come li hai memorizzati, altrimenti il flusso di esecuzione del programma verrà corrotto.

Salvataggio e ripristino di ra tra una chiamata e l'altra
addi sp, sp, -4 # crea spazio per una parola
sw ra, 0(sp) # salva l'indirizzo di ritorno
# ... chiama altre funzioni qui ...
lw ra, 0(sp) # la ripristina
addi sp, sp, 4 # libera lo spazio
ret

Ogni / sovrascrive ra, quindi se la tua funzione chiama un'altra funzione, la chiamata interna distrugge il tuo indirizzo di ritorno e il tuo salta nel posto sbagliato. Per evitarlo, riserva 4 byte sullo stack, e vi salva ra. Dopo che le chiamate interne sono terminate, ripristina l'indirizzo di ritorno, e restituisce lo spazio prima di .

⚠
Se hai chiamato qualcosa, ripristina ra prima di , e riporta sempre sp al valore che aveva all'inizio. Uno stack che non corrisponde al suo stato iniziale è la causa abituale di un crash nei programmi in assembly.