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:
Ecco il corrispondente codice assembly x86:
Prologo
Nel prologo della funzione somma
, si salvano e si aggiornano i registri ebp
e esp
nel seguente modo:
push ebp
: Il valore corrente diebp
viene salvato nello stack.mov ebp, esp
: Il valore diesp
viene copiato inebp
.
Dopo queste due istruzioni, la situazione sullo stack è la seguente:
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
.
Epilogo
pop ebp
: L’istruzionepop
legge il valore nella posizione attuale del registroesp
(che punta al vecchio valore diebp
nello stack) e lo copia nel registroebp
. Quindi, il registroesp
viene automaticamente incrementato di 4 byte (la dimensione di un puntatore nell’architettura x86 a 32 bit), puntando ora all’indirizzo di ritorno.ret
: L’istruzioneret
ritorna al chiamante estraendo l’indirizzo di ritorno dalla cima dello stack (attualmente puntato dal registroesp
). Il registroesp
viene nuovamente incrementato di 4 byte per ripulire l’indirizzo di ritorno dallo stack. Il risultato della somma, memorizzato nel registroeax
, viene passato al chiamante.