Salta al contenuto
Note per Studenti Esercizio 34 · funzione ricorsiva in assembly ARM, Fibonacci

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

c
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: nn (per calcolare n−2n-2) e il risultato fib(n−1)\text{fib}(n-1). BL distrugge r0–r3 e lr, quindi questi due valori vanno in registri callee-saved: r4 per nn e r5 per fib(n−1)\text{fib}(n-1).
  • Una funzione che usa r4, r5 li deve salvare all'ingresso e ripristinare all'uscita; e siccome contiene BL, deve anche salvare lr (altrimenti BL sovrascrive l'indirizzo di ritorno).
  • PUSH {r4, r5, r6, lr} salva quattro registri: 16 byte. Si aggiunge r6 solo perché con tre registri (12 byte) sp non sarebbe multiplo di 8, e la convenzione richiede sp allineato a 8 byte alle chiamate.
  • Il caso base (n<2n < 2) non tocca il resto: ritorna subito con BX lr, senza PUSH: la funzione foglia più economica.
armasm
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    lr

BLT è un confronto con segno: per n<0n < 0 ritorna direttamente nn (in C si assume n≥0n \ge 0). 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 n≥2n \ge 2: PUSH → sp = 0x7FF0; r4=3r4 = 3; chiama fib(2)
fib(2) 0x7FF0 n≥2n \ge 2: PUSH → sp = 0x7FE0; r4=2r4 = 2; chiama fib(1)
fib(1) 0x7FE0 caso base: ritorna 11 senza stack; in fib(2) r5=1r5 = 1
fib(0) 0x7FE0 caso base: ritorna 00; fib(2) calcola 0+1=10 + 1 = 1, POP → sp = 0x7FF0, ritorna 1
fib(1) 0x7FF0 caso base: ritorna 11; fib(3) calcola 1+1=21 + 1 = 2, 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: C(n)=1+C(n−1)+C(n−2)C(n) = 1 + C(n-1) + C(n-2) con C(0)=C(1)=1C(0) = C(1) = 1, quindi C(n)=2 fib(n+1)−1C(n) = 2\,\text{fib}(n+1) - 1. Per n=5n = 5: 15 chiamate (per n=0,…,5n = 0, \dots, 5 i valori sono 1, 1, 3, 5, 9, 15). Il tempo cresce come fib(n)\text{fib}(n), cioè esponenzialmente (≈1,618n\approx 1{,}618^n): fib(12) fa 465 chiamate.
  • Stack: la catena di chiamate annidate più lunga è fib(5)→fib(4)→fib(3)→fib(2)\text{fib}(5) \to \text{fib}(4) \to \text{fib}(3) \to \text{fib}(2) (le prime quattro hanno un PUSH; fib(1) ritorna senza): 4 record da 16 byte = 64 byte. In generale 16 (n−1)16\,(n-1) byte per n≥2n \ge 2: lo spazio sullo stack cresce linearmente con nn anche se il tempo è esponenziale.
nn 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 nn da 0 a 12 restituisce i numeri di Fibonacci attesi (0,1,1,2,3,5,8,13,21,34,55,89,1440, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144); 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 seconda BL sovrascrive l'indirizzo di ritorno e POP {..., pc} o BX lr tornano nel posto sbagliato (ciclo infinito).
  • Tenere nn o fib(n−1)\text{fib}(n-1) in r0–r3: BL e la funzione chiamata li modificano. Servono registri callee-saved.
  • Usare r4/r5 senza salvarli: si corrompono i valori del chiamante (che a sua volta li usa).
  • PUSH e POP con insiemi diversi di registri.
  • Mettere il PUSH prima del test del caso base: il caso base costerebbe inutilmente un frame (e per n<2n < 2 sarebbe la maggior parte delle chiamate).
  • Scordare l'allineamento dello stack a 8 byte: con tre soli registri salvati sp non è 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 n<2→nn < 2 \to n. 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; nn in r4 e fib(n−1)\text{fib}(n-1) 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.

armasm
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    lr

Costi: chiamate C(n)=2 fib(n+1)−1C(n) = 2\,\text{fib}(n+1) - 1 (esponenziale; fib(5): 15, fib(12): 465); stack massimo 16 (n−1)16\,(n-1) byte per n≥2n \ge 2 (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 n=0,…,12n = 0, \dots, 12.

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.

Teoria collegata