Lo stack Γ¨ una area di archiviazione in RAM sequenziale, in particolare Γ¨ una β€œpila” LIFO (Last in first out) che viene utilizzato per gestire variabili locali, parametri di funzione e informazioni sul controllo del flusso del programma. Quando viene chiamata una funzione, un blocco Γ¨ riservato nella parte superiore dello stack per le variabili locali e alcuni dati.

Quando quella funzione termina, il blocco diventa inutilizzato e puΓ² essere usato alla successiva chiamata di una funzione. La memoria viene quindi allocata e deallocata automaticamente quando le funzioni vengono chiamate e ritornano.

Questa allocazione lineare e sequenziale della memoria e viene utilizzata nell’allocazione della memoria statica (variabili passate per valore).

La dimensione dello stack Γ¨ fissa e decisa dal sistema operativo quando viene creato assegnandolo ad un thread. Se la dimensione dello stack supera il valore iniziale vi Γ¨ un errore di stack overflow. Questa eccezione puΓ² avvenire, per esempio, nei casi di ricorsione infinita.

Vantaggi

  • VelocitΓ  di allocazione e deallocazione: Lo stack Γ¨ generalmente piΓΉ veloce nell’allocazione e nella deallocazione della memoria rispetto all’Heap. Questo Γ¨ dovuto alla sua natura lineare e alla gestione automatica della memoria, che permette di allocare e deallocare semplicemente spostando un puntatore.
  • Gestione automatica della memoria: Le variabili allocate nello stack vengono automaticamente rimosse quando il blocco di codice che le contiene termina la sua esecuzione. CiΓ² semplifica la gestione della memoria e riduce il rischio di perdite di memoria (memory leaks) che possono verificarsi quando si utilizza l’heap.
  • Minore frammentazione della memoria: PoichΓ© la memoria nello stack viene allocata e deallocata in modo lineare, il rischio di frammentazione della memoria Γ¨ ridotto. Questo contribuisce a mantenere l’efficienza del programma e riduce la necessitΓ  di interventi per deframmentare la memoria.
  • Allocazione deterministica: L’allocazione di memoria nello stack avviene in modo deterministico e prevedibile, il che significa che il comportamento del programma risulta piΓΉ consistente e piΓΉ facile da analizzare. Al contrario, l’allocazione di memoria nell’heap puΓ² essere piΓΉ complessa e imprevedibile.
  • Minore overhead: Le variabili nello stack hanno generalmente un overhead inferiore rispetto a quelle allocate nell’heap. CiΓ² Γ¨ dovuto al fatto che non Γ¨ necessario gestire esplicitamente la memoria o utilizzare strutture dati aggiuntive (come tabelle di allocazione) per tenere traccia delle variabili allocate.

Limiti

  • Dimensione limitata: Lo stack ha una dimensione limitata, solitamente molto inferiore rispetto all’heap. CiΓ² significa che Γ¨ possibile esaurire lo spazio dello stack rapidamente, specialmente quando si lavora con strutture dati di grandi dimensioni o con ricorsione profonda.
  • Allocazione statica e temporanea: Le variabili allocate nello stack hanno una durata limitata e vengono automaticamente rimosse quando il blocco di codice che le contiene termina la sua esecuzione. CiΓ² limita la flessibilitΓ  nella gestione della memoria e impedisce l’uso di variabili dinamiche o la persistenza dei dati tra diverse chiamate di funzione.
  • Non adatto per grandi oggetti: A causa della dimensione limitata dello stack, non Γ¨ adatto per l’allocazione di oggetti o strutture dati di grandi dimensioni. Al contrario, l’heap offre molto piΓΉ spazio e flessibilitΓ  per gestire oggetti piΓΉ grandi.
  • Nessuna gestione manuale della memoria: Sebbene la gestione automatica della memoria nello stack possa sembrare un vantaggio, a volte puΓ² essere un limite. L’allocazione e la deallocazione automatica della memoria impediscono al programmatore di gestire direttamente la memoria, il che puΓ² essere utile in situazioni in cui Γ¨ richiesta una maggiore flessibilitΓ  o un controllo piΓΉ preciso sulle risorse.

Esempio

Il seguente metodo calcola la somma di due numeri interi:

int somma(int a, int b) {
    return a + b;
}

Ecco il corrispondente codice assembly x86:

; Prologo della funzione
somma:
    push ebp        ; Salva il valore corrente di ebp nello stack
    mov ebp, esp    ; Imposta ebp come base dello stack per questa chiamata di funzione
 
; Corpo della funzione
    mov eax, [ebp+8] ; Carica il valore del primo parametro (a) in eax
    mov ecx, [ebp+12]; Carica il valore del secondo parametro (b) in ecx
    add eax, ecx    ; Esegui la somma tra eax e ecx, e salva il risultato in eax
 
; Epilogo della funzione
    pop ebp         ; Ripristina il valore di ebp precedentemente salvato nello stack
    ret             ; Ritorna al chiamante, il risultato Γ¨ in eax

Nel prologo della funzione somma, si salvano e si aggiornano i registri ebp e esp nel seguente modo:

  1. push ebp: Il valore corrente di ebp viene salvato nello stack.
  2. mov ebp, esp: Il valore di esp viene copiato in ebp.

Dopo queste due istruzioni, la situazione sullo stack Γ¨ la seguente:

    | ...         |
    +-------------+
ebp | indirizzo di ritorno | <- ebp+4
    +-------------+
    | vecchio ebp | <- ebp
    +-------------+
    | parametro a | <- ebp+8 (viene letto in eax)
    +-------------+
    | parametro b | <- ebp+12 (viene letto in ecx)
    +-------------+
    | ...         |

Ora ebp punta al vecchio valore di ebp salvato sullo stack e esp punta alla posizione successiva. Si noti che l’indirizzo di ritorno Γ¨ posizionato tra il vecchio ebp e i parametri a e b sullo stack. L’indirizzo di ritorno occupa 4 byte (la dimensione di un puntatore nell’architettura x86 a 32 bit).

Quindi, per accedere al primo parametro a, si utilizza ebp+8, poichΓ© a si trova 8 byte piΓΉ in alto rispetto al valore corrente di ebp (4 byte per il vecchio ebp e 4 byte per l’indirizzo di ritorno). Analogamente, per accedere al secondo parametro b, si utilizza ebp+12, poichΓ© b si trova 12 byte piΓΉ in alto rispetto al valore corrente di ebp (4 byte per il vecchio ebp, 4 byte per l’indirizzo di ritorno e 4 byte per il parametro a).

Corpo

Le istruzioni mov eax, [ebp+8] e mov ecx, [ebp+12] caricano i valori dei parametri a e b nei registri eax e ecx, rispettivamente. Questi valori sono accessibili tramite ebp, che punta alla base dello stack per questa chiamata di funzione. Successivamente, l’istruzione add eax, ecx esegue l’operazione di somma e memorizza il risultato nel registro eax.

  1. pop ebp: L’istruzione pop legge il valore nella posizione attuale del registro esp (che punta al vecchio valore di ebp nello stack) e lo copia nel registro ebp. Quindi, il registro esp viene automaticamente incrementato di 4 byte (la dimensione di un puntatore nell’architettura x86 a 32 bit), puntando ora all’indirizzo di ritorno.
  2. ret: L’istruzione ret ritorna al chiamante estraendo l’indirizzo di ritorno dalla cima dello stack (attualmente puntato dal registro esp). Il registro esp viene nuovamente incrementato di 4 byte per ripulire l’indirizzo di ritorno dallo stack. Il risultato della somma, memorizzato nel registro eax, viene passato al chiamante.