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
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:
| ... |
+-------------+
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
.
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.