Nel capitolo dell’Instruction Pointer abbiamo analizzato in parte il seguente codice.
#include <stdio.h>
int main(){
int a=10,b=2;
int somma = a+b;
printf("Somma:%d\n",somma);
return 0;
}
Lo stesso programma in Assembly:
0x0000000000001139 <+0>: push rbp
0x000000000000113a <+1>: mov rbp,rsp
0x000000000000113d <+4>: sub rsp,0x10
0x0000000000001141 <+8>: mov DWORD PTR [rbp-0xc],0xa
0x0000000000001148 <+15>: mov DWORD PTR [rbp-0x8],0x2
0x000000000000114f <+22>: mov edx,DWORD PTR [rbp-0xc]
0x0000000000001152 <+25>: mov eax,DWORD PTR [rbp-0x8]
0x0000000000001155 <+28>: add eax,edx
0x0000000000001157 <+30>: mov DWORD PTR [rbp-0x4],eax
0x000000000000115a <+33>: mov eax,DWORD PTR [rbp-0x4]
0x000000000000115d <+36>: lea rdx,[rip+0xea0] # 0x2004
0x0000000000001164 <+43>: mov esi,eax
0x0000000000001166 <+45>: mov rdi,rdx
0x0000000000001169 <+48>: mov eax,0x0
0x000000000000116e <+53>: call 0x1030 <printf@plt>
0x0000000000001173 <+58>: mov eax,0x0
0x0000000000001178 <+63>: leave
0x0000000000001179 <+64>: ret
La prima ed unica funzione di questo programma scritto in C è la funzione main.
Appena il thread inizia ad eseguire il programma ed arriva al main la CPU esegue questa istruzione: push rbp chè è stata inserita dal compilatore C.
Questa istruzione salva nello stack il vecchio valore di rbp che prima dell’esecuzione del main puntava al sistema operativo.
Questo registro chiamato Base Pointer servirà per tutta la durata della funzione main come base per calcolare la posizioni delle variabili interne alla funzione, trovare parametri passati ad una funzione ecc…
Analizziamo meglio queste 2 istruzioni

mov = istruzione che sposta un valore, in questo caso 10(0xa), nelle parentesi quadre troviamo [rbp-0xc], quando troviamo le parentesi quadre significa che ci stiamo riferendo ad un indirizzo nello stack e questa istruzione muove il valore 10 all’indirizzo rbp - 12(0xc).
Guardiamo il seguente schema, lo stack.

I numeri a sinistra sono gli indirizzi di memoria nello stack, supponiamo che il base pointer abbia come posizione 100, abbiamo detto che il valore 10 viene inserito nello stack alla posizione [rbp-0xc] dove 0xc in esadecimale vale 12, quindi abbiamo che 100 -12 = 88, 88 sarà l’indirizzo di memoria nello stack che conterrà il valore della variabile a.
Stessa cosa per la variabile b, [rbp -0x8] = 92, 92 è l’indirizzo che conterrà il valore della variabile b.
Utilizzando gdb come debugger possiamo esaminare [rbp-0xc].
gdb -q ./ciao
Imposto un breakpoint nel main.
break main
ed avvio il programma
r
A questo punto vediamo cosa c’è all’interno di [rbp-0xc]
x/wb $rbp-12

Possiamo vedere nel rettangolo in alto a destra la prossima istruzione puntata dall’instruction pointer (non ancora eseguita) di conseguenza in basso a destra vediamo che all’indirizzo [rbp-0xc] c’è il valore 0.
Eseguo la prossima istruzione con il comando:
nexti
Adesso lanciando di nuovo.
x/wb $rbp-12

Vediamo in alto a destra che l’istruzione è stata eseguita, in basso a sinistra vediamo che il valore 10 è stato inserito all’indirizzo [rbp-0xc] che nel mio caso è
0x7fffffffe394
Fino ad ora il Base Pointer è rimasto invariato, questo perchè la funzione main non è ancora terminata.

Vediamo invece cosa succede se nel nostro programma abbiamo un’altra funzione oltre al main.
#include <stdio.h>
int prodotto(int a,int b){
return a*b;
}
int main(){
int a=10,b=2;
int somma = a+b;
printf("Somma:%d\n",somma);
prodotto(5,5);
return 0;
}
Una nota importante!!
Quando questa nuova funzione (prodotto) verrà eseguita, verrà creato nello stack uno “Stack Frame” temporaneo che conterrà valori di variabili o altro, quando finisce la funzione quei dati rimangono li come codice morto o in attesa di essere usati dal programma stesso.
Possiamo vedere che esattamente come all’inizio del programma main anche all’inizio di questa funzione(prodotto) la prima istruzione è: push rbp

eseguo il comando: stepi un paio di volte per entrare nella funzione.
stepi

Di seguito riportato il registro prima e dopo l’avvio della funzione prodotto.
$rbp : 0x00007fffffffe3a0 → 0x00007fffffffe4c8 → 0x00007fffffffe865 → "/home/fra/c/ciao"
$rbp : 0x00007fffffffe380 → 0x00007fffffffe3a0 → 0x00007fffffffe4c8 → 0x00007fffffffe865 → "/home/fra/c/ciao"
All’inizio del main, la prima istruzione inserita dal compilatore è proprio push rbp. Questa istruzione serve a salvare nello Stack il vecchio valore di RBP (che apparteneva alla funzione del sistema operativo che ha avviato il programma).
Quando entriamo nella funzione prodotto, viene eseguito un altro push rbp. In questo secondo caso, il ‘vecchio valore’ che viene salvato nello Stack è l’indirizzo del pavimento del main.
Questo è fondamentale perché, quando la funzione prodotto terminerà, la CPU userà quel valore per ripristinare lo Stack Frame del main esattamente come era prima.
Se analiziamo questi indirizzi di memoria a ritroso osserviamo che ognuno di essi contiene la base/pavimento dello stack frame precedente.
Nello screenshot qui sotto si può vedere che siamo già usciti dal main e che stiamo per terminare il programma.
A questo punto, gli stack frame della funzione main e della funzione prodotto non sono più attivi: la CPU ha modificato i registri RSP e RBP per tornare a gestire la memoria del sistema operativo. Sebbene i vecchi dati e i vecchi base pointer siano ancora fisicamente scritti nella RAM (come spazzatura di memoria), per il programma quella zona è ormai considerata libera e abbandonata.
