Compiler Explorer
Vediamo nel dettaglio cosa succede quando compiliamo un programma.
Useremo come liguaggio C, ma qualsiasi linguaggio viene convertito in codice Assembly (ASM).
Usiamo il sito Compiler Explorer, che permette di vedere in modo interattivo cosa succede ad un codice compilato.
Da linea di comando, usando
gcc
, è possibile vedere il codice ASM usando l'opzione-S
.
Come caso di studio, riprendiamo l'esempio della somma.
Vediamo passo passo come le 3 linee di codice C diventano le linee di codice ASM.
Linea 1: definizione di una funzione
Per la prima linea abbiamo la seguente traduzione:
// Codice C
int somma(int a,int b) {
; Codice ASM
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
Quando viene chiamata una funzione, per prima cosa con il comando push
viene salvato il vecchio valore del Base Pointer (rbp
), il registro che punta sempre all'indirizzo di memoria all'inizio della funzione corrente. Questo valore verrà ripristinato alla fine della funzione.
Quindi, con mov rbp, rsp
, il valore del Base Pointer viene assegnato all'indirizzo dell'attuale cima della stack.
Nota: essendo un'architettura a 64bit (Intel
x86_64
), i registri degli indirizzi sono a 64bit, e questo è visibile perché i nomi cominciano con la letterar
(comerbp
). I registri a 32bit cominciano con lae
(comeeax
) ed i registri a 16 bit sono formati da solo due lettere (ex.ax
). Altre architetture (es.arm
) usano altre convenzioni.
Infine, con le ultime due istruzioni vengono copiati nella stack i valori dei registri edi
ed esi
, che contengono i parametri di ingresso a
e b
. La posizione in cui vengono copiati dipende dalla dimensione dei valori da copiare: in questo caso essendo int
, hanno una dimensione di 4 byte, quindi il primo viene copiato in rbp - 4
ed il secondo in rbp - 8
. Questi valori detti di offset sono negativi perché, come detto in precedenza, la stack parte dai valori alti di memoria e cresce verso i valori bassi, quindi gli offset devono essere sottratti all'indirizzo di partenza della funzione.
Linea 2: corpo della funzione
Abbiamo la seguente traduzione della seconda linea.
return a+b;
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
Nelle prime due linee, il processore copia i valori dalla stack nei registri edx
e eax
; questi sono i registri che vengono usati dall'ALU per le operazioni.
Nell'ultima riga, l'ALU fa la somma dei valori nei registri indicati e salva il risultato dell'operazione nel primo registro indicato, in questo caso eax
.
Linea 3: uscita da una funzione
Abbiamo infine la traduzione dell'ultima linea.
}
pop rbp
ret
Il comando pop
riassegna nuovamente al registro rbp
il valore salvato nel comando push
, infine il comando ret
fa saltare l'esecuzione alla funzione che ha chiamato.
Ricordiamo che il valore ritornato dalla funzione è sempre all'interno del registro eax
.