Skip to the content.

Lezione 2.5 - Le Variabili Locali e lo Stack

Video di riferimento: Addendum lezione 2 - La vita delle variabili locali


1. Concetti Fondamentali

1.1 ELI5: Cos’è lo Stack

Immagina una pila di piatti in cucina:

Lo stack del computer funziona esattamente così. È una zona di RAM organizzata come una pila.

Dettaglio chiave: lo stack in x86 cresce al contrario - verso gli indirizzi più bassi. È come se i piatti li accatastassi verso il basso invece che verso l’alto.

1.2 RSP e RBP - I Due Registri Chiave

RSP (Stack Pointer) = “Dove si trova la cima della pila in questo momento”

RBP (Base Pointer) = “Punto di riferimento fisso per questa funzione”

Perché RSP scende quando fai PUSH? Sì, esatto! Lo stack pointer (RSP/ESP) scende perché in x86 lo stack cresce verso indirizzi più bassi. Quando fai PUSH, RSP diminuisce (es: da 0x1000 a 0x0FFC).


2. Evoluzione dello Stack - Visualizzazione Completa

Codice di esempio

int sum(int a, int b) {
    int x = a + b;
    return x;
}

int main() {
    sum(10, 20);
}

STEP 1: Nel main, prima di chiamare sum

Memoria (indirizzi alti → bassi)

0x2000  ┌─────────────┐
        │    ...      │
0x1FF8  ├─────────────┤
        │  main data  │
0x1FF0  └─────────────┘  ← RSP = 0x1FF0
                         ← RBP = 0x1FF0 (o altro valore del main)

Registri:


STEP 2: Push parametri

push 20    ; b
push 10    ; a
0x2000  ┌─────────────┐
        │    ...      │
0x1FF8  ├─────────────┤
        │  main data  │
0x1FF0  ├─────────────┤
        │     20      │  ← parametro b
0x1FE8  ├─────────────┤
        │     10      │  ← parametro a
0x1FE0  └─────────────┘  ← RSP = 0x1FE0 (sceso di 16 bytes)
                         ← RBP = 0x1FF0 (invariato)

Registri:


STEP 3: CALL sum

call sum   ; push automatico dell'indirizzo di ritorno
0x2000  ┌─────────────┐
        │    ...      │
0x1FF8  ├─────────────┤
        │  main data  │
0x1FF0  ├─────────────┤
        │     20      │  ← b
0x1FE8  ├─────────────┤
        │     10      │  ← a
0x1FE0  ├─────────────┤
        │ ret_address │  ← indirizzo di ritorno nel main
0x1FD8  └─────────────┘  ← RSP = 0x1FD8
                         ← RBP = 0x1FF0

Registri:


STEP 4: Prologo di sum - Dettagliato

Istruzione 1: push rbp

“Salva il vecchio RBP del main sullo stack”

0x1FD8  ├─────────────┤
        │ ret_address │
0x1FD0  ├─────────────┤
        │   0x1FF0    │  ← ho salvato il RBP del main qui!
0x1FC8  └─────────────┘  ← RSP = 0x1FC8

Registri:


Istruzione 2: mov rbp, rsp

“Il nuovo RBP di sum = dove si trova RSP ora”

0x1FD8  ├─────────────┤
        │ ret_address │
0x1FD0  ├─────────────┤
        │   0x1FF0    │  ← vecchio RBP salvato
0x1FC8  └─────────────┘  ← RBP = 0x1FC8 (NUOVO segnaposto!)
                         ← RSP = 0x1FC8

Registri:

Ora da 0x1FC8 (RBP) posso:


Istruzione 3: sub rsp, 8

“Fai spazio per le variabili locali”

0x1FD8  ├─────────────┤
        │ ret_address │
0x1FD0  ├─────────────┤
        │   0x1FF0    │  ← [RBP + 0] = vecchio RBP
0x1FC8  ├─────────────┤  ← RBP = 0x1FC8 (FERMO!)
        │      x      │  ← [RBP - 8] = variabile locale
0x1FC0  └─────────────┘  ← RSP = 0x1FC0 (sceso ancora)

Registri:

Stack Frame Completo

          indirizzi alti
┌────────────────────────┐
│        ...             │
├────────────────────────┤
│        b               │ ← [RBP + 16]
├────────────────────────┤
│        a               │ ← [RBP + 8]
├────────────────────────┤
│ return address         │ ← [RBP + 4] (tecnicamente non lo usiamo così)
├────────────────────────┤
│ old RBP (0x1FF0)       │ ← [RBP + 0]
├────────────────────────┤  ← RBP = 0x1FC8 (punto di riferimento)
│ x (variabile locale)   │ ← [RBP - 8]
└────────────────────────┘  ← RSP = 0x1FC0
          indirizzi bassi

Chiave del modello mentale:


STEP 5: Esecuzione di sum

mov eax, [rbp + 8]    ; eax = a = 10
add eax, [rbp + 16]   ; eax = eax + b = 30
mov [rbp - 8], eax    ; x = 30
mov eax, [rbp - 8]    ; return value in eax

Lo stack non cambia, lavoriamo solo con i registri. EAX contiene il valore di ritorno secondo la calling convention.


STEP 6: Epilogo di sum - Il Ritorno

mov rsp, rbp    ; RSP torna dove era RBP
pop rbp         ; ripristino il vecchio RBP
ret             ; torno al chiamante

Dopo mov rsp, rbp:

PRIMA:
RBP = 0x1FC8
RSP = 0x1FC0

0x1FD0  ├─────────────┤
        │   0x1FF0    │  ← vecchio RBP salvato
0x1FC8  ├─────────────┤  ← RBP
        │      x      │
0x1FC0  └─────────────┘  ← RSP

DOPO mov rsp, rbp:
RBP = 0x1FC8
RSP = 0x1FC8 (uguale a RBP!)

0x1FD0  ├─────────────┤
        │   0x1FF0    │  ← sta per essere letto
0x1FC8  └─────────────┘  ← RBP e RSP (lo spazio di x è ora "garbage")

Dopo pop rbp:

DOPO pop rbp:
RBP = 0x1FF0 (RIPRISTINATO dal main!)
RSP = 0x1FD0

0x1FD8  ├─────────────┤
        │ ret_address │  ← sta per essere letto
0x1FD0  └─────────────┘  ← RSP

Dopo ret:

0x1FE0  ├─────────────┤
        │     10      │  ← RSP = 0x1FE0
0x1FD8  ├─────────────┤
        │ ret_address │  ← letto e messo in RIP (instruction pointer)
        └─────────────┘

Il main ora deve fare:

add rsp, 16    ; pulisco i parametri (10 e 20)

E RSP torna a 0x1FF0!


Riepilogo dell’evoluzione dei registri

Step RSP RBP Dove punta RBP
1 0x1FF0 0x1FF0 main frame
2 0x1FE0 0x1FF0 main frame
3 0x1FD8 0x1FF0 main frame
4 0x1FC0 0x1FC8 sum frame!
6 0x1FD8 0x1FF0 main frame (ripristinato)

Il meccanismo di ritorno:

  1. Non “torni fino al base pointer”
  2. RIPRENDI il vecchio RBP che avevi salvato all’inizio con push rbp!
  3. È come una matrioska: ogni funzione salva il segnaposto di chi l’ha chiamata, fa il suo lavoro, e poi lo ripristina

3. Teoria: Variabili, Registri e Memoria

3.1 Concetto fondamentale

Le variabili sono un concetto del linguaggio di programmazione e del compilatore, non dell’hardware.

Il processore mette a disposizione:

Il compilatore decide come mappare le variabili su queste risorse.

3.2 Registri

I registri costituiscono il livello più alto della gerarchia della memoria:

3.3 Allocazione delle variabili locali

Le variabili locali possono essere allocate:

3.4 Calling Convention

Le calling convention definiscono:

Permettono l’interoperabilità tra codice compilato per la stessa architettura e ABI.


4. Differenze Architetturali

4.1 Architettura 6502 (semplice)

Nelle architetture semplici come il 6502, il numero di registri è molto limitato.

Caratteristiche:

4.2 Architettura x86 (evoluta)

Nelle architetture più evolute come x86, la gestione è più sofisticata.

Caratteristiche:


5. Stack Frame in Dettaglio (x86 32-bit)

5.1 Contesto

5.2 Meccanismo di chiamata

Quando viene chiamata una funzione:

  1. Chiamante prepara i parametri (PUSH sullo stack)
  2. CALL automaticamente fa PUSH dell’indirizzo di ritorno
  3. Funzione chiamata esegue il prologo:
    • Salva il vecchio EBP
    • Imposta il nuovo EBP
    • Riserva spazio per variabili locali
  4. Funzione esegue il suo codice
  5. Epilogo ripristina lo stack e ritorna

5.3 Accesso ai dati tramite offset

Da EBP (Base Pointer) posso accedere a:

Offset positivi (verso l’alto nello stack):

Offset negativi (verso il basso nello stack):

5.4 Nota importante sui dati “cancellati”

Dopo un’operazione di POP (o dopo che lo Stack Pointer viene spostato manualmente con un’istruzione come ADD ESP, 8), i dati non vengono cancellati fisicamente dalla memoria.

Essi rimangono scritti in quella locazione della RAM, ma vengono considerati “distrutti” perché:


6. Esempi di Codice Assembly

6.1 Esempio 6502 - Fill memory con pattern

Codice Assembly mostrato nel video:

START:
    LDA #50
    JSR FILL_FIVE
    BRK

FILL_FIVE:
    TAX
    BEQ DONE
    LDY #$00
    LDA #$05
LOOP:
    STA $0200,Y
    INY
    DEX
    BNE LOOP
DONE:
    RTS

Note sul simulatore:

Memory location $fe contains a new random byte on every instruction. Memory location $ff contains the ascii code of the last key pressed.

Memory locations $200 to $5ff map to the screen pixels. Different values will draw different colour pixels. The colours are:

$0: Black, $1: White, $2: Red, $3: Cyan, $4: Purple, $5: Green, $6: Blue, $7: Yellow, $8: Orange, $9: Brown, $a: Light red, $b: Dark grey, $c: Grey, $d: Light green, $e: Light blue, $f: Light grey


6.2 Esempio x86 - Funzione sum

Codice C:

#include <stdio.h>

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

int main(void) {
    return sum(10,20);
}

Assembly generato (x86 32-bit, cdecl):

_a$ = 8                                       ; size = 4
_b$ = 12                                      ; size = 4
int sum(int,int) PROC                         ; sum
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _a$[ebp]
        add     eax, DWORD PTR _b$[ebp]
        pop     ebp
        ret     0
int sum(int,int) ENDP                         ; sum

_main   PROC
        push    ebp
        mov     ebp, esp
        push    20                            ; 00000014H
        push    10                            ; 0000000aH
        call    int sum(int,int)              ; sum
        add     esp, 8
        pop     ebp
        ret     0
_main   ENDP

6.3 Esempio dettagliato con variabile locale

Codice C:

int sum(int a, int b) {
    int x = a + b;
    return x;
}

Assembly con commenti:

push ebp                  ; salva frame pointer del chiamante
mov ebp, esp              ; imposta il frame pointer per questa funzione
sub esp, 4                ; riserva spazio per variabile locale x

mov eax, [ebp + 8]        ; primo parametro (a)
mov edx, [ebp + 12]       ; secondo parametro (b)
add eax, edx              ; somma
mov [ebp - 4], eax        ; salva in x

mov eax, [ebp - 4]        ; valore di ritorno in EAX
mov esp, ebp              ; ripristina stack pointer
pop ebp                   ; ripristina frame pointer
ret                       ; ritorna (EAX contiene il risultato)

Il chiamante:

push 4                    ; parametro b
push 2                    ; parametro a
call sum                  ; chiama la funzione
add esp, 8                ; pulisce i parametri dallo stack (2 × 4 bytes)

Spiegazione del flusso:


7. Note Finali

7.1 RBP/EBP non è obbligatorio

EBP non è obbligatorio, ma è didatticamente potentissimo

Serve per:

Nei compilatori ottimizzati:

Ma il concetto resta:

Stack frame = parametri + return address + stato salvato + variabili locali

7.2 Il linguaggio C e i prototipi

Il linguaggio C, conoscendo il prototipo di una funzione, sa:

Per questo è possibile usare una funzione come printf semplicemente includendone il prototipo con #include <stdio.h>.

7.3 Principio generale

In x86 a 32 bit, il compilatore può organizzare le funzioni usando uno stack frame basato su EBP, nel quale parametri e variabili locali sono accessibili tramite offset fissi. Questo modello, pur non universale, è fondamentale per comprendere il funzionamento delle chiamate di funzione a basso livello.


8. Strumenti Didattici


9. Approfondimenti

Ulteriori approfondimenti