Per fornire un esempio concreto di come un compilatore gestisce lo stack e come si accede ai valori nello stack, possiamo guardare le rappresentazioni visive, oltre al codice generato da GCC
in un ambiente Linux con i386 come architettura di destinazione.
1. Stack cornici
Come sapete, lo stack è una posizione nello spazio degli indirizzi di un processo in esecuzione che viene utilizzato da funzioni o procedure , nel senso che lo spazio viene allocato nello stack per le variabili dichiarate localmente, nonché gli argomenti passati alla funzione ( lo spazio per le variabili dichiarate al di fuori di qualsiasi funzione (ovvero variabili globali) è allocato in una regione diversa nella memoria virtuale). Lo spazio allocato per tutti i dati di una funzione è riferito a uno stack frame . Ecco una rappresentazione visiva di più frame di stack (da Computer Systems: A Programmer's Perspective ):
2. Gestione dello stack frame e posizione variabile
Affinché i valori scritti nello stack all'interno di un particolare frame dello stack siano gestiti dal compilatore e letti dal programma, è necessario un metodo per calcolare le posizioni di questi valori e recuperare il loro indirizzo di memoria. I registri nella CPU indicati come puntatore di stack e il puntatore di base aiutano in questo.
Il puntatore di base, ebp
per convenzione, contiene l'indirizzo di memoria del fondo, o base, dello stack. Le posizioni di tutti i valori all'interno del frame dello stack possono essere calcolate utilizzando l'indirizzo nel puntatore di base come riferimento. Questo è rappresentato nella figura sopra: %ebp + 4
è l'indirizzo di memoria memorizzato nel puntatore di base più 4, per esempio.
3. Codice generato dal compilatore
Ma quello che non capisco è come le variabili nello stack vengono quindi lette da un'applicazione: se dichiaro e assegno x come numero intero, dico x = 3, e lo spazio di archiviazione è riservato nello stack e il suo valore di 3 viene archiviato lì, e quindi nella stessa funzione dichiaro e assegno y come, diciamo 4, e quindi dopo che uso quindi x in un'altra espressione, (diciamo z = 5 + x) come può il programma leggere x per valutare z quando è sotto y in pila?
Usiamo un semplice programma di esempio scritto in C per vedere come funziona:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
Esaminiamo il testo dell'assemblaggio prodotto da GCC per questo testo sorgente C (l'ho ripulito un po 'per motivi di chiarezza):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
Rileviamo però che le variabili x, yez sono situati a indirizzi %ebp - 12
, %ebp -8
e %ebp - 4
, rispettivamente. In altre parole, le posizioni delle variabili all'interno del frame dello stack per main()
vengono calcolate utilizzando l'indirizzo di memoria salvato nel registro CPU %ebp
.
4. I dati in memoria oltre il puntatore dello stack non rientrano nell'ambito
Mi manca chiaramente qualcosa. La posizione nello stack riguarda solo la durata / ambito della variabile e l'intero stack è effettivamente accessibile al programma in ogni momento? In tal caso, ciò implica che esiste un altro indice che contiene solo gli indirizzi delle variabili nello stack per consentire il recupero dei valori? Ma poi ho pensato che l'intero punto dello stack fosse che i valori erano memorizzati nello stesso posto dell'indirizzo variabile?
Lo stack è un'area nella memoria virtuale, il cui uso è gestito dal compilatore. Il compilatore genera codice in modo tale che i valori oltre il puntatore dello stack (valori oltre la parte superiore dello stack) non vengano mai indicati. Quando viene chiamata una funzione, la posizione del puntatore dello stack cambia per creare spazio nello stack ritenuto non "fuori limite", per così dire.
Man mano che le funzioni vengono chiamate e restituite, il puntatore dello stack viene decrementato e incrementato. I dati scritti nello stack non scompaiono dopo che sono fuori dall'ambito, ma il compilatore non genera istruzioni che fanno riferimento a questi dati perché non è possibile per il compilatore calcolare gli indirizzi di questi dati utilizzando %ebp
o %esp
.
5. Riepilogo
Il codice che può essere eseguito direttamente dalla CPU viene generato dal compilatore. Il compilatore gestisce lo stack, i frame dello stack per le funzioni e i registri della CPU. Una strategia utilizzata da GCC per tenere traccia delle posizioni delle variabili nei frame dello stack nel codice che deve essere eseguito sull'architettura i386 è utilizzare l'indirizzo di memoria nel puntatore della base del frame dello stack %ebp
, come riferimento e scrivere i valori delle variabili nelle posizioni nei frame dello stack a offset all'indirizzo in %ebp
.