Lo Stack

Lo stack è una struttura di dati di tipo LIFO che si trova in una zona di memoria riservata. LIFO è l'acronimo di Last In First Out, quindi l'ultimo dato inserito sarà il primo ad essere prelevato.
Lo stack inizia nella zona di memoria con indirizzi più alti e cresce di 4 bytes per volta verso gli indirizzi di memoria più bassi. La sua funzione è quella di memorizzare variabili locali, argomenti passati alle funzioni e indirizzi di ritorno.
Per aggiungere e rimuovere elementi (un registro o il contenuto di una locazione di memoria) dalla testa dello stack esistono due istruzioni: PUSH e POP. Quando usiamo l'istruzione PUSH, ESP viene decrementato di 4 e viene aggiunto un elemento allo stack (che cresce); quando usiamo l'istruzione POP, ESP viene incrementato di 4 e viene tolto un elemento dallo stack (che decresce).
ESP (Extended Stack Pointer) è un registro puntatore che contiene l'indirizzo di memoria corrispondente alla testa dello stack.

Immagine1 - Lo Stack

Immagine1 - Lo Stack

Nell'immagine 1 possiamo vedere la zona di memoria corrispondente allo stack. In alto troviamo indirizzi di memoria bassi ed è in questa direzione che lo stack cresce quando eseguiamo un istruzione di PUSH, viceversa con l'istruzione POP lo stack decresce verso indirizzi di memoria più alti. ESP punta alla locazione di memoria corrispondente all'indirizzo 0013FF28 (la testa dello stack) e il prossimo valore pushato verrà quindi scritto all'indirizzo 0013FF24, cioè la prossima locazione libera.

Passiamo ora ad analizzare un esempio pratico per vedere come si comporta lo stack quando un programma C chiama una funzione ASM esterna.

Creiamo un programma C che deve prendere in input 2 numeri interi inseriti dall'utente, passarli alla funzione asm sum (che eseguirà la somma) e stampare a video il risultato restituito dalla funzione.

Questo è il sorgente del programma C:


	#include <stdio.h>
	#include <conio.h>

	extern "C" int sum(int a, int b); 

	void main() {
		int a, b, r;
		printf("Inserisci a: ");
		scanf("%d", &a);
		printf("Inserisci b: ");
		scanf("%d", &b);
		r=sum(a, b);
		printf("\n%d+%d=%d", a, b, r);
		getch();
	}

Nel prototipo abbiamo usato extern "C" per indicare al compilatore che si tratta di una funzione esterna alla quale passeremo i parametri seguendo la modalità "C", cioè pushando i parametri sullo stack dall'ultimo al primo (prima b e poi a. - Nella modalità pascal, invece, i parametri vengono pushati dal primo all'ultimo).

Questo invece è il sorgente della funzione asm esterna chiamata sum che esegue la somma tra 2 int:


	.386
	.MODEL FLAT, C
	.CODE
	.STACK

	sum PROC
	MOV EDX, ESP
	MOV EAX, [EDX+4]
	MOV EBX, [EDX+8]
	ADD EAX, EBX
	RET
	sum ENDP

Immagine 2 - Prima di entrare nella funzione

Immagine 2 - Prima di entrare nella funzione

Dopo aver creato un nuovo progetto e aver compilato eseguiamo il programma step by step fino al breakpoint inserendo a=5 e b=3.

Per avere sott'occhio i registri e la memoria selezioniamo dalla debug toolbar le voci Registers e Memory.

Registers - Memory

Ora dobbiamo trovare la zona di memoria corrispondente allo stack, per farlo sarà sufficiente inserire come indirizzo il valore contenuto in ESP (o scrivere direttamente ESP) come mostrato nell'immagine 2.

Prima di procedere selezioniamo anche Disassembly in modo da vedere le operazioni che esegue la macchina prima e dopo la chiamata alla funzione.

Disassembly
Immagine 3 - Salvataggio parametri nello stack

Immagine 3 - Salvataggio parametri nello stack

Ora possiamo vedere (immagine 3) che prima di chiamare la funzione il compilatore pusha i parametri nello stack seguendo la modalità "C" (prima b e poi a). I 2 parametri che abbiamo inserito sullo stack occupano entrambi 4 byte di memoria, di conseguenza ESP è diminuito di 8 e lo stack è cresciuto (ricordiamo che lo stack cresce sempre verso indirizzi di memoria più bassi).

Quando eseguiamo la call, il compilatore copia sullo stack il valore di EIP (Extended Istruction Pointer – Registro che punta all'istruzione successiva), cioè l'indirizzo di ritorno da cui riprendere l'esecuzione del programma una volta usciti dalla funzione.

L'indirizzo dell'istruzione successiva alla chiamata (004010A1)

Indirizzo dell'istruzione successiva

viene inserito sullo stack e ESP viene decrementato di 4 (immagine 4).

Immagine 4 - Salvataggio dell'indirizzo di ritorno nello stack

Immagine 4 - Salvataggio dell'indirizzo di ritorno nello stack

Ora abbiamo nello stack i 2 parametri passati alla funzione e l'indirizzo di ritorno.

Con l'istruzione MOV EDX, ESP copiamo il valore di ESP in EDX.
Per recuperare i valori dallo stack eseguiamo le istruzioni


	MOV EAX, [EDX+4]

che copia in EAX il valore di a che si trova all'indirizzo 0013FF20 (0013FF1C+4) e


	MOV EBX, [EDX+8]

che copia in EBX il valore di b che si trova all'indirizzo 0013FF24 (0013FF1C+8).

Dall'immagine 5 possiamo vedere che abbiamo letto i parametri dallo stack senza però modificarlo, quindi ESP è rimasto invariato.

Immagine 5 - Recupero dei parametri dallo stack

Immagine 5 - Recupero dei parametri dallo stack

Abbiamo così recuperato i valori di a e b dallo stack e li abbiamo copiati nei registri EAX e EBX.

Ora eseguiamo la somma tra i 2 registri con l'istruzione ADD EAX, EBX ottenendo il risultato (8) in EAX.

Prima di entrare nella funzione abbiamo inserito sullo stack i 2 parametri e abbiamo lasciato per ultimo l'indirizzo di ritorno. La funzione termina con l'istruzione RET che ripristina in EIP l'indirizzo di ritorno leggendolo dalla testa dello stack, è per questo che deve sempre essere pushato per ultimo.

Una volta uscito dalla funzione, il compilatore incrementa ESP di 4 e prosegue con il programma principale (immagine 6).

Immagine 6 - Ripristino dell'indirizzo di ritorno in EIP

Immagine 6 - Ripristino dell'indirizzo di ritorno in EIP

Il programma chiamante incrementa ESP di 8 ripristinando lo stack (ormai i 2 parametri sullo stack non servono più) e copia nella variabile r il risultato della somma, leggendolo da EAX.

I valori che avevamo inserito nello stack non sono stati cancellati ma ESP punta ora all'indirizzo 0013FF28, quindi, quando verranno pushati altri byte sullo stack, i valori verranno sovrascritti (immagine 7).

Immagine 7 - Ripristino dello stack

Immagine 7 - Ripristino dello stack

Infine l'istruzione printf stamperà su schermo 5+3=8 e il programma attenderà la pressione di un tasto prima di chiudersi.

Queste immagini mostrano come, durante l'esecuzione del programma, lo stack aumenta e diminuisce seguendo il metodo LIFO, dove l'ultimo elemento ad essere inserito è il primo ad essere tolto.

Andamento dello stack durante l'esecuzione del programma
Prima della call Viene pushato b
Viene pushato a Viene pushato l'indirizzo di ritorno
Ritorno al programma chiamante Ripristino dello stack

Ezio Melotti - ©2005 - This work is licensed under a Creative Commons BY-NC-SA 3.0 License.