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 lettera r (come rbp). I registri a 32bit cominciano con la e (come eax) 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.