C è un linguaggio di medio livello e ha bisogno di un compilatore per convertirlo in un codice eseguibile in modo che il programma possa essere eseguito sulla nostra macchina.
Come compiliamo ed eseguiamo un programma C?
Di seguito ci sono i passi che usiamo su una macchina Ubuntu con compilatore gcc.
- Prima creiamo un programma C usando un editor e salviamo il file come filename.c
$ vi filename.c
- Il diagramma sulla destra mostra un semplice programma per sommare due numeri.
- Poi compilatelo usando il seguente comando.
$ gcc –Wall filename.c –o filename
- L’opzione -Wall abilita tutti i messaggi di avviso del compilatore. Questa opzione è raccomandata per generare un codice migliore.
L’opzione -o è usata per specificare il nome del file di output. Se non usiamo questa opzione, allora viene generato un file di output con nome a.out.
- Dopo la compilazione viene generato l’eseguibile ed eseguiamo l’eseguibile generato usando il comando sottostante.
$ ./filename
Cosa succede nel processo di compilazione?
Il compilatore converte un programma C in un eseguibile. Ci sono quattro fasi perché un programma C diventi un eseguibile:
- Pre-elaborazione
- Compilazione
- Assemblaggio
- Collegamento
Con l’esecuzione del seguente comando, otteniamo tutti i file intermedi nella directory corrente insieme all’eseguibile.
$gcc –Wall –save-temps filename.c –o filename
La seguente schermata mostra tutti i file intermedi generati.
Vediamo uno per uno cosa contengono questi file intermedi.
Pre-elaborazione
Questa è la prima fase attraverso la quale viene passato il codice sorgente. Questa fase include:
- Rimozione dei commenti
- Espansione delle macro
- Espansione dei file inclusi.
- Compilazione condizionata
L’output pre-elaborato è memorizzato nel nome del file.i. Vediamo cosa c’è dentro filename.i: usando $vi filename.i
Nell’output di cui sopra, il file sorgente è riempito con un sacco di informazioni, ma alla fine il nostro codice è conservato.
Analisi:
- printf contiene ora a + b piuttosto che add(a, b) perché le macro sono state espanse.
- I commenti sono stati rimossi.
- #include<stdio.h> manca invece vediamo molto codice. Quindi i file di intestazione sono stati espansi e inclusi nel nostro file sorgente.
Compilazione
Il prossimo passo è compilare filename.i e produrre un; file di output compilato intermedio filename.s. Questo file è in istruzioni a livello assembly. Vediamo questo file usando $vi filename.s
L’istantanea mostra che è in linguaggio assembly, che l’assemblatore può capire.
Assemblaggio
In questa fase il file filename.s è preso come input e trasformato in filename.o dall’assemblatore. Questo file contiene istruzioni a livello macchina. In questa fase, solo il codice esistente viene convertito in linguaggio macchina, le chiamate di funzione come printf() non vengono risolte. Vediamo questo file usando $vi filename.o
Linking
Questa è la fase finale in cui tutti i collegamenti delle chiamate di funzione con le loro definizioni sono fatti. Il linker sa dove sono implementate tutte queste funzioni. Il linker fa anche del lavoro extra, aggiunge del codice extra al nostro programma che è richiesto quando il programma inizia e finisce. Per esempio, c’è un codice che è richiesto per impostare l’ambiente come passare gli argomenti della linea di comando. Questo compito può essere facilmente verificato usando $size filename.o e $size filename. Attraverso questi comandi, sappiamo che come il file di output aumenta da un file oggetto a un file eseguibile. Questo è dovuto al codice extra che il linker aggiunge al nostro programma.