Esercizio 34funzione ricorsiva in assembly ARM, Fibonacci
In questa pagina 6
Testo (tipo di esercizio previsto dalla modalità di verifica del corso per la programmazione in assembly ARM: "gestione dello stack, chiamata di funzioni"; nessun tema pubblico di questo tipo è stato trovato, quindi il testo è originale). Tradurre in assembly ARM a 32 bit, rispettando la convenzione AAPCS, la funzione ricorsiva
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}Poi: (a) disegnare lo stack durante fib(3); (b) dire quante chiamate fa fib(5) e quanto stack occupa al massimo; (c) verificare che i registri callee-saved siano preservati.
Teoria: Stack e chiamata di funzioni in ARMChiamata con BL e ritorno con BX LR; convenzione di chiamata AAPCS (argomenti in r0-r3, risultato in r0, registri da preservare r4-r11); stack discendente pieno con PUSH e POP; record di attivazione; funzioni foglia e non foglia; esempio ricorsivo del fattoriale con evoluzione dello stack.Stack e chiamata di funzioni in ARM →, RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →, Strutture di controllo in assembly ARMSalti B e condizionati, codici di condizione con e senza segno; traduzione di if, if-else, while, for e do-while da C ad ARM; esecuzione condizionata per eliminare salti brevi; switch con tabella di salto.Strutture di controllo in assembly ARM →.
Registri e salvataggi
- Argomento e risultato in
r0. fib(n)deve richiamare se stessa due volte e dopo la prima chiamata servono ancora due valori: (per calcolare ) e il risultato .BLdistrugger0–r3elr, quindi questi due valori vanno in registri callee-saved:r4per er5per .- Una funzione che usa
r4,r5li deve salvare all'ingresso e ripristinare all'uscita; e siccome contieneBL, deve anche salvarelr(altrimentiBLsovrascrive l'indirizzo di ritorno). PUSH {r4, r5, r6, lr}salva quattro registri: 16 byte. Si aggiunger6solo perché con tre registri (12 byte)spnon sarebbe multiplo di 8, e la convenzione richiedespallineato a 8 byte alle chiamate.- Il caso base () non tocca il resto: ritorna subito con
BX lr, senzaPUSH: la funzione foglia più economica.
fib: CMP r0, #2
BLT base @ n < 2: fib(n) = n (r0 resta n)
PUSH {r4, r5, r6, lr} @ r6 solo per mantenere sp allineato a 8
MOV r4, r0 @ r4 = n
SUB r0, r4, #1
BL fib @ r0 = fib(n-1)
MOV r5, r0 @ r5 = fib(n-1)
SUB r0, r4, #2
BL fib @ r0 = fib(n-2)
ADD r0, r0, r5 @ fib(n-1) + fib(n-2)
POP {r4, r5, r6, pc} @ ripristina e ritorna (pc <- lr salvato)
base: BX lrBLT è un confronto con segno: per ritorna direttamente (in C si assume ). POP {..., pc} carica nel pc il valore di lr salvato dalla PUSH: equivale a un ritorno.
(a) Lo stack durante fib(3)
Partendo da sp = 0x8000 (lo stack cresce verso indirizzi minori; PUSH sottrae 16 e scrive r4 all'indirizzo più basso, poi r5, r6, lr). Chiamate nell'ordine, con il valore di sp all'ingresso di ciascuna:
| chiamata | sp all'ingresso |
cosa fa |
|---|---|---|
fib(3) |
0x8000 |
: PUSH → sp = 0x7FF0; ; chiama fib(2) |
fib(2) |
0x7FF0 |
: PUSH → sp = 0x7FE0; ; chiama fib(1) |
fib(1) |
0x7FE0 |
caso base: ritorna senza stack; in fib(2) |
fib(0) |
0x7FE0 |
caso base: ritorna ; fib(2) calcola , POP → sp = 0x7FF0, ritorna 1 |
fib(1) |
0x7FF0 |
caso base: ritorna ; fib(3) calcola , POP → sp = 0x8000 |
Stato dello stack al punto più profondo (dentro fib(1) chiamata da fib(2)):
| indirizzo | contenuto |
|---|---|
0x7FFC |
lr salvato da fib(3): ritorno al chiamante |
0x7FF8 |
r6 del chiamante |
0x7FF4 |
r5 del chiamante |
0x7FF0 |
r4 del chiamante |
0x7FEC |
lr salvato da fib(2): ritorno dentro fib(3) |
0x7FE8 |
r6 salvato da fib(2) |
0x7FE4 |
r5 salvato da fib(2) |
0x7FE0 |
r4 salvato da fib(2) (qui è 3, il vecchio valore) ← sp |
Dopo il ritorno di fib(3), sp = 0x8000 come all'ingresso e r4, r5, r6 valgono come prima.
(b) Numero di chiamate e stack massimo
- Chiamate totali: con , quindi . Per : 15 chiamate (per i valori sono 1, 1, 3, 5, 9, 15). Il tempo cresce come , cioè esponenzialmente ():
fib(12)fa 465 chiamate. - Stack: la catena di chiamate annidate più lunga è (le prime quattro hanno un
PUSH;fib(1)ritorna senza): 4 record da 16 byte = 64 byte. In generale byte per : lo spazio sullo stack cresce linearmente con anche se il tempo è esponenziale.
| 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | |
|---|---|---|---|---|---|---|---|---|
fib(n) |
1 | 2 | 3 | 5 | 8 | 21 | 55 | 144 |
| chiamate | 3 | 5 | 9 | 15 | 25 | 67 | 177 | 465 |
| stack massimo (byte) | 16 | 32 | 48 | 64 | 80 | 112 | 144 | 176 |
(c) Registri preservati
All'uscita r4, r5, r6 hanno il valore che avevano all'ingresso, grazie al PUSH/POP simmetrico, e sp è tornato al valore iniziale. Una PUSH e una POP con registri diversi (o in numero diverso) o una POP mancante su un solo ramo lascerebbero lo stack sbilanciato e il programma tornerebbe a un indirizzo sbagliato.
Verifica
Il codice è stato assemblato ed eseguito in un emulatore ARM: fib(n) per da 0 a 12 restituisce i numeri di Fibonacci attesi (); a ogni prova sp torna a 0x8000 e r4, r5, r6 (impostati a valori noti prima della chiamata) sono invariati; i conteggi delle chiamate e dello stack massimo nella tabella sono quelli misurati.
Errori comuni
- Non salvare
lr: la secondaBLsovrascrive l'indirizzo di ritorno ePOP {..., pc}oBX lrtornano nel posto sbagliato (ciclo infinito). - Tenere o in
r0–r3:BLe la funzione chiamata li modificano. Servono registri callee-saved. - Usare
r4/r5senza salvarli: si corrompono i valori del chiamante (che a sua volta li usa). PUSHePOPcon insiemi diversi di registri.- Mettere il
PUSHprima del test del caso base: il caso base costerebbe inutilmente un frame (e per sarebbe la maggior parte delle chiamate). - Scordare l'allineamento dello stack a 8 byte: con tre soli registri salvati
spnon è multiplo di 8.
Versione ripasso
Tipo di esercizio previsto dalla modalità di verifica (assembly ARM: stack e chiamata di funzioni; testo originale): fib(n) = fib(n-1) + fib(n-2) con . Teoria: Stack e chiamata di funzioni in ARMChiamata con BL e ritorno con BX LR; convenzione di chiamata AAPCS (argomenti in r0-r3, risultato in r0, registri da preservare r4-r11); stack discendente pieno con PUSH e POP; record di attivazione; funzioni foglia e non foglia; esempio ricorsivo del fattoriale con evoluzione dello stack.Stack e chiamata di funzioni in ARM →, RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →.
Registri: argomento e risultato in r0; in r4 e in r5 (callee-saved: sopravvivono alle BL); PUSH {r4, r5, r6, lr} (16 byte, r6 per l'allineamento a 8; lr perché BL lo sovrascrive); POP {r4, r5, r6, pc} ritorna. Il caso base ritorna con BX lr senza PUSH.
fib: CMP r0, #2
BLT base
PUSH {r4, r5, r6, lr}
MOV r4, r0
SUB r0, r4, #1
BL fib
MOV r5, r0
SUB r0, r4, #2
BL fib
ADD r0, r0, r5
POP {r4, r5, r6, pc}
base: BX lrCosti: chiamate (esponenziale; fib(5): 15, fib(12): 465); stack massimo byte per (fib(5): 64 byte, lineare). Con sp = 0x8000: ingresso di fib(3) a 0x8000, fib(2) a 0x7FF0, fib(1) e fib(0) interni a 0x7FE0. All'uscita sp, r4, r5, r6 sono invariati. Verificato in un emulatore per .
Errori comuni: lr non salvato; valori in r0–r3 attraverso una BL; r4/r5 non salvati; PUSH e POP diversi; PUSH prima del caso base; sp non allineato.