2.1 gcc (Gnu Compiler Collection)
Insieme di strumenti open-source che costituisce lo standard per la creazione di eseguibili su Linux.
GCC supporta diversi linguaggi, tra cui C, e consente la modifica dei vari passaggi intermedi per una completa personalizzazione dell’eseguibile.
Compilazione
Gli strumenti GCC possono essere chiamati singolarmente:
gcc -E <sorgente.c> -o <preProcessed.ii|.i> #crea un file pre-elaborato
gcc -S <preProcessed.i|.ii> -o <assembly.asm|.s> #converte il file pre-elaborato in codice assembly
gcc -c <assembly.asm|.s> -o <objectFile.obj|.o> #compila il codice assembly e crea un file oggetto
gcc <objectFile.obj|.o> -o <executable.out> #linka il file oggetto e crea il file eseguibile
- L’input di ogni comando può essere il file sorgente, e l’ultimo comando è in grado di creare direttamente l’eseguibile.
- L’assembly ed il codice macchina generato dipendono dall’architettura di destinazione.
Esempio
- Provate a compilare una semplicissima applicazione, invocando ogni step singolarmente osservandone l’output.
- Provate ad aggiungere #include<stdio.h> ad inizio file e ripetete il tutto
//main.c
int main()
{
return 0;
}
//main.c
#include <stdio.h>
int main()
{
return 0;
}
2.2 Make
Il Make tool è uno strumento della collezione GNU che può essere usato per gestire la compilazione automatica e selettiva di grandi e piccoli progetti. Make consente di specificare delle dipendenze tra i vari file, per esempio consentendo solo la compilazione di librerie i cui sorgenti sono stati modificati.
Make può anche essere usato per gestire il deployment di un’applicazione, assumendo alcune delle capacità di uno script bash.
Makefile
Make esegue i makefiles, che contengono tutte le direttive necessarie per la compilazione di un'applicazione o per svolgere altri task. Il makefile descrive le dipendenze tra i file di input e di output, mentre Make esegue solo le regole necessarie per generare i file di output richiesti, rendendo il processo di compilazione più efficiente.
make -f makefile
Il comando make è utilizzato per eseguire un makefile specifico, ma se viene invocato senza argomenti, cerca il file 'makefile' nella cartella di lavoro. Se non trova 'makefile', cerca GNUmakefile, makefile e Makefile nell'ordine specificato.
Make consente di includere makefiles secondari all'interno del makefile principale utilizzando delle direttive specifiche, come ad esempio la direttiva include. Questo consente di suddividere il processo di compilazione in più makefiles, ognuno dei quali si occupa di una specifica parte del progetto, e di includerli tutti nel makefile principale.
Target, prerequisite and recipes
In un makefile, una regola (o ricetta) è una serie di comandi bash che vengono eseguiti per generare un determinato target. Ogni regola viene eseguita indipendentemente dalle altre regole e target presenti nel makefile. Quando si esegue un makefile, il make esegue solo le regole dei target specificati sulla riga di comando o quelle del primo target definito nel makefile, mentre le altre regole e target non vengono eseguiti.
I target sono generalmente dei file generati da uno specifico insieme di regole. Ogni target può specificare dei prerequisiti, ovvero degli altri file o target che devono esistere affinché le regole di un target vengano eseguite. Un prerequisito può essere
esso stesso un target!
L’esecuzione di un file make inizia specificando uno o più target make -f makefile target1 ... e prosegue a seconda dei vari prerequisiti.
target: prerequisite
→ recipe/rule
→ recipe/rule
target1: target2 target3
rule (3)
rule (4)
...
target2: target3
rule (1)
target3:
rule (2)
In questo caso il target principale è "main", che dipende dai tre file di oggetti "main.o", "foo.o" e "bar.o". Ogni file di oggetti è a sua volta un target con le sue regole e prerequisiti. Ad esempio, "main.o" dipende dal file sorgente "main.c".
main: main.o foo.o bar.o
gcc -o main main.o foo.o bar.o
main.o: main.c
gcc -c main.c
foo.o: foo.c
gcc -c foo.c
bar.o: bar.c
gcc -c bar.c
Sintassi
Un makefile è un file di testo "plain" in cui righe vuote e parti di testo dal carattere "#" fino alla fine della riga e non in una ricetta (considerato un commento: sempre che non sia usato l’escaping con "\#" o che compaia dentro una stringa con ' o ") sono ignorati.
Le ricette devono iniziare con un carattere di TAB (non spazi).
Una ricetta che (a parte il TAB) inizia con @ non viene visualizzata in output, altrimenti i comandi sono visualizzati e poi eseguiti.
Una riga con un singolo TAB è una ricetta vuota.
Esistono costrutti più complessi per necessità particolari (ad esempio costrutti condizionali)
Target speciali
Se non viene passato alcun target, viene eseguito quello di default, ovvero il primo disponibile. Esistono poi dei target che hanno un significato e comportamento speciale.
target: prerequisite
→ rule
→ rule
...
.INTERMEDIATE e .SECONDARY hanno come prerequisiti i target "intermedi" e da cui dipendono.
I target da cui dipende .INTERMEDIATE sono trattati come file intermedi, ovvero file che vengono creati durante l’esecuzione di un makefile ma non sono il risultato finale desiderato, quindi vengono solitamente eliminati automaticamente da make dopo che non sono più necessari.
Invece, i target da cui dipende .SECONDARY sono trattati come file intermedi, ma non vengono mai eliminati automaticamente e vengono mantenuti alla fine dell'esecuzione del makefile.
all: ...
rule
.INTERMEDIATE: target1
.SECONDARY: target2
.PHONY è un target speciale in un makefile che viene utilizzato per specificare i target che non corrispondono a file reali.
I prerequisiti del target .PHONY sono considerati target fittizi e verranno sempre eseguiti quando si esegue make con uno di questi target come argomento, indipendentemente dal fatto che esista o meno un file con quel nome o dal momento dell’ultima modifica.
Ad esempio, supponiamo di avere un target clean nel nostro makefile che rimuove tutti i file oggetto:
clean:
rm -f *.o
Se esiste un file chiamato clean nella directory corrente, make potrebbe confondersi e non eseguire la ricetta per il target clean. Per evitare questo problema, possiamo specificare il target clean come prerequisito del target .PHONY:
.PHONY: clean
clean:
rm -f *.o
In questo modo, make clean eseguirà sempre la ricetta per il target clean, indipendentemente dal fatto che esista o meno un file chiamato clean.
In un target, % sostituisce qualunque stringa. In un prerequisito corrisponde alla stringa sostituita nel target.
%.s: %.c
#prova.s: prova.c
#src/h.s: src/h.c
Questo è come potrebbe venire applicata la regola, anche se in questo esempio non è presente una ricetta specifica.
Variabili utente e automatiche
Le variabili utente si definiscono con la sintassi nome:=valore o nome=valore e vengono usate con $(nome).
Inoltre, possono essere sovrascritte da riga di comando con make nome=value.
ONCE:=hello $(LATER)
EVERY=hello $(LATER)
LATER=world
target1:
echo $(ONCE) # ‘hello’
echo $(EVERY) # ‘hello world’
Le variabili automatiche possono essere usate all’interno delle regole per riferirsi ad elementi specifici relativi al target corrente.
target: pre1 pre2 pre3
echo $@ is ‘target’ # $@ rappresenta il nome del target della regola
echo $^ is ‘pre1 pre2 pre3’ # $^ rappresenta tutte le dipendenze della regola
echo $< is ‘pre1’ # $< rappresenta il nome della prima dipendenza necessaria per creare il file di output
In questo caso specifico:
- echo $@ is ‘target’ stamperà target is ‘target’
- echo $^ is ‘pre1 pre2 pre3’ stamperà pre1 pre2 pre3 is ‘pre1 pre2 pre3’
- echo $< is ‘pre1’ stamperà pre1 is ‘pre1’
Funzioni speciali
- $(eval …): consente di creare nuove regole make dinamiche.
- $(shell …): cattura l’output di un commando shell.
- $(wildcard *): restituisce un elenco di file che corrispondono alla stringa specificata.
LATER=hello
PWD=$(shell pwd) # definisce la variabile PWD e le assegna il valore restituito dal comando pwd eseguito nella shell (ovvero restituisce il percorso della directory di lavoro corrente)
OBJ_FILES:=$(wildcard *.o) # definisce una variabile chiamata OBJ_FILES e le assegna il valore restituito dalla funzione wildcard, che espande a tutti i file con estensione .o nella directory corrente (se ci sono i file `file1.o`, `file2.o` e `file3.c`, la funzione `wildcard` espanderà a `file1.o file2.o`)
target1:
echo $(LATER) # stampa "hello" perché la variabile LATER è stata definita come hello all'inizio del makefile
$(eval LATER+= world) # utilizza la funzione eval per aggiungere "world" alla variabile LATER
echo $(LATER) # hello world # stampa "hello world" perché il valore della variabile LATER è stato modificato dalla riga precedente
Esempio
all: main.out #Regola predefinita che viene eseguita quando si esegue il comando make senza specificare una regola. Dipende da main.out, quindi make cercherà una regola per creare main.out
@echo “Application compiled”
%.s: %.c #Regola modello che specifica come creare un file `.s` (assembly) da un file `.c` (codice sorgente C)
gcc -S $< -o $@ #Comando eseguito quando la regola modello viene eseguita
%.out: %.s
mkdir -p build
gcc $< -o build/$@
clean:
rm -rf build *.out *.s #Rimuove la directory build e tutti i file con estensione .out e .s
.PHONY: clean
.SECONDARY: make.s #indica a make di non cancellare automaticamente i file intermedi (in questo caso, i file .s) dopo averli utilizzati per creare altri file
Conclusioni
Docker, GCC e make possono essere utilizzati per la gestione delle varie applicazioni in C. Ognuno di questi strumenti non è indispensabile ma permette di creare un flusso di lavoro coerente e strutturato.