[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home] [volume] [parte]


Capitolo 571.   Compilazione C dall'alto in basso

Tradizionalmente, la compilazione di un programma scritto in linguaggio C avviene utilizzando il comando cc come nell'esempio seguente, sapendo che se non si usa l'opzione -o si ottiene il file a.out:

cc mio.c[Invio]

Tuttavia, l'elaborazione del file in linguaggio C richiede diversi passaggi, prima di arrivare al file eseguibile finale; passaggi che è bene tenere in considerazione.

In un sistema GNU il compilatore standard è GCC (GNU compiler collection) che si usa sia per il C, sia per altri linguaggi. Nel caso del linguaggio C, il programma frontale è precisamente gcc, al quale corrisponde comunque il collegamento cc.

Questo capitolo non esaurisce il problema e accenna soltanto alle situazioni più comuni. Per un approfondimento si vedano i documenti citati nella bibliografia che conclude il capitolo stesso.

571.1   Le fasi della compilazione

La compilazione di un programma scritto in linguaggio C prevede diverse fasi: precompilazione, trasformazione in linguaggio assemblatore, trasformazione in file-oggetto, collegamento (link) di uno o più file-oggetto in un file eseguibile. Per conservare i file intermedi della compilazione si può usare l'opzione -save-temps di gcc, come nell'esempio seguente:

gcc -save-temps mio.c[Invio]

In questo caso si ottengono i file mio.i, mio.s e mio.o, contenenti rispettivamente il risultato elaborato dal precompilatore, la trasformazione in linguaggio assemblatore e il file-oggetto finale. Se poi il programma contenuto nel file sorgente è completo, si ottiene anche il file a.out che costituisce il programma eseguibile.

Eventualmente, alcune opzioni di gcc consentono di fermare l'elaborazione a uno stadio prestabilito: -E serve a ottenere solo l'elaborazione da parte del precompilatore; -S serve a ottenere il sorgente in linguaggio assemblatore; -c serve a compilare, ma senza eseguire il collegamento finale (pertanto si ottiene il file-oggetto rilocabile).

571.2   Precompilatore

Ogni compilatore C «standard» prevede che il file sorgente venga elaborato, prima della compilazione vera e propria, attraverso un precompilatore, il quale elabora il sorgente e genera un altro sorgente ottenuto dall'interpretazione delle istruzioni di «precompilazione». Queste istruzioni di precompilazione costituiscono un linguaggio indipendente dal C vero e proprio. Il precompilatore di GCC è cpp e di norma viene chiamato automaticamente da gcc stesso, come già accennato nella sezione precedente.

#include <stdio.h>
int main (void)
{
    printf ("Ciao a tutti!\n");
    return 0;
}

Nell'esempio mostrato, l'istruzione #include <stdio.h> riguarda il precompilatore e richiede l'inclusione del file stdio.h in quella posizione (il file si deve trovare all'interno di una directory prestabilita). Con l'opzione -E di gcc (oppure anche con -save-temps) si può vedere il risultato della precompilazione:

gcc -E -o mio.i mio.c[Invio]

Il file mio.i che si genera dall'elaborazione ha un aspetto simile al pezzo che si vede nel listato successivo:

# 1 "mio_file.c"
# 1 "<built-in>"
# 1 "<command line>"
...
...
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;
...
...
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));
# 834 "/usr/include/stdio.h" 3 4

# 2 "mio_file.c" 2
int main (void)
{
    printf ("Ciao a tutti!\n");
    return 0;
}

571.3   Compilazione dei file intermedi

Di norma, ogni compilatore tradizionale del linguaggio C si prende cura di tutte le fasi della compilazione, chiamando a sua volta i programmi necessari. Pertanto, con lo stesso programma frontale è possibile avviare manualmente la compilazione da fasi successive. Per esempio:

  1. cc mio_file.i[Invio]

  2. cc mio_file.s[Invio]

  3. cc mio_file.o[Invio]

Negli esempi si mostra l'uso del comando cc, ma gcc è perfettamente conforme a questa convenzione tradizionale. Come si può intuire, dall'estensione del nome del file il programma frontale determina quali azioni deve intraprendere: nel primo caso avvia la compilazione saltando solo la fase iniziale dell'analisi del precompilatore; nel secondo caso avvia l'assemblatore (e quindi continua con il collegatore); nell'ultimo caso avvia soltanto il collegatore (linker).

Naturalmente è possibile mescolare file differenti assieme, se la somma di questi deve portare a un solo file-eseguibile finale. Per esempio, si può compilare un programma composto dai file uno.c, due.i, tre.s e quattro.o, dove ognuno viene elaborato in base alle proprie esigenze e alla fine il tutto viene collegato assieme:

cc uno.c due.i tre.s quattro.o[Invio]

571.4   L'uso di librerie

In generale, la compilazione di un programma scritto secondo il linguaggio C implica automaticamente l'utilizzo della libreria Libc e il collegamento (link) con dei file-oggetto predefiniti, che contengono il codice necessario a preparare il programma prima di passare all'esecuzione della funzione main().

Con gcc, per escludere l'utilizzo di qualunque libreria predefinita vanno usate le opzioni -nostartfiles e -nodefaultlibs; eventualmente l'opzione -nostdlibs dovrebbe valere per entrambe queste opzioni e può essere usata assieme a loro, benché sia ridondante.

Quando si fa uso di funzioni che non sono state dichiarate nel proprio programma, si tratta sempre di qualcosa che è contenuto in una libreria, di solito quella predefinita (Libc), ma per usarle correttamente è indispensabile che sia inserita all'inizio del file la dichiarazione del loro prototipo. Per questo, a seconda delle funzioni che si utilizzano, si includono i file che contengono i prototipi necessari; nel caso della funzione printf() si include comunemente il file stdio.h.

Se si utilizza una funzione che appartiene a una libreria prevista nella compilazione, della quale però non si dichiara il prototipo, si può anche ottenere una compilazione «corretta», ma non è detto che, durante il funzionamento del programma, il passaggio degli argomenti attraverso i parametri della funzione avvenga in modo altrettanto corretto. In pratica, è molto probabile che la chiamata di tali funzioni produca risultati errati.

571.5   Librerie statiche e librerie dinamiche

Le librerie statiche sono file-oggetto raccolti in archivi generati con il programma ar, dove i nomi dei file di tali archivi hanno estensione .a. L'uso di queste librerie implica l'incorporazione del codice utilizzato nel programma finale.

Per compilare un programma che utilizza delle librerie statiche è sufficiente indicare i nomi dei file che le contengono, assieme agli altri file del programma:

gcc mio.c /usr/lib/libncurses.a[Invio]

In alternativa, secondo la modalità normale, quando i file di tali librerie si trovano nelle directory previste, si può usare l'opzione -l, a cui si attacca il nome della libreria, ottenuto dal nome del file togliendo l'estensione e il prefisso lib. Pertanto, l'esempio appena mostrato andrebbe trasformato così:

gcc -static mio.c -lncurses[Invio]

Le librerie dinamiche sono realizzate in modo differente rispetto a quelle statiche e sono contenute normalmente in file con estensione .so. La compilazione con l'uso di librerie dinamiche avviene in modo analogo a quanto visto per quelle statiche:

gcc mio.c /usr/lib/libncurses.so[Invio]

Oppure:

gcc -dynamic mio.c -lncurses[Invio]

Come si può intuire dagli esempi mostrati, se una stessa libreria è fornita sia in versione statica, sia in versione dinamica, le opzioni -static e -dynamic servono a precisare che tipo di compilazione si vuole. Se però si omette di specificarlo, in generale vengono utilizzate le librerie dinamiche.

L'opzione -l implica una ricerca dei file delle librerie all'interno di directory prestabilite, ma può succedere che sia necessario esplicitarlo nella riga di comando. In tal caso si può usare l'opzione -L:

gcc mio.c -L/opt/mia/lib -lmia[Invio]

Nell'esempio appena mostrato, la compilazione richiede l'uso della libreria mia (libmia.so o libmia.a) che va cercata prima nella directory /opt/mia/lib/.

Dal momento che l'uso delle librerie si affianca all'inclusione dei file che ne contengono il prototipo, conviene ricordare anche l'opzione -I, con la quale si richiede di cercare i file da includere a cominciare dalla directory specificata:

gcc mio.c -I/opt/mia/include -L/opt/mia/lib -lmia[Invio]

In questo nuovo esempio, si specifica anche che i file da includere vanno cercati a cominciare dalla directory /opt/mia/include/.

Naturalmente, il problema dei percorsi di ricerca per i file da includere riguarda solo quelli che nel sorgente si indicano tra parentesi angolari, come in questo esempio:

#include <stdio.h>

Diversamente, se il nome fosse messo tra apici doppi, il file verrebbe cercato nel percorso indicato esplicitamente nel sorgente stesso.

A ogni modo, quando la compilazione manifesta dei problemi che non sembrano dovuti a errori sintattici, conviene usare l'opzione -v, con la quale si vede esattamente cosa tenta di fare il programma frontale e dove si interrompe la compilazione. Ciò può essere molto utile per capire, per esempio, quando il problema deriva da file mancanti (librerie o altro).

Per il procedimento necessario alla produzione di una libreria, statica o dinamica, si veda il capitolo 566.

571.6   L'ordine dei file e delle librerie nella compilazione

La compilazione corretta richiede che i file e le librerie siano indicati nella riga di comando secondo un ordine logico: prima il file che contiene la funzione main(), poi i file o le librerie contenenti le funzioni chiamate dal primo file, poi i file o le librerie contenenti le funzioni chiamate dai predecessori e così di seguito. Per esempio, se il file uno.c contiene la funzione main() e a sua volta chiama la funzione due() contenuta nel file due.s, la riga di comando per la compilazione deve avere l'aspetto seguente:

gcc uno.c due.s ...

Se poi la funzione due() si avvale della funzione tre(), contenuta nella libreria libtre.a, la riga di comando si sviluppa così:

gcc uno.c due.s -ltre ...

Naturalmente, anche la funzione tre() potrebbe avvalersi di una funzione contenuta in una seconda libreria. Per esempio potrebbe usare la funzione quattro() della libreria libquattro.so:

gcc uno.c due.s -ltre -lquattro ...

Questa è una regola generale da considerare in fase di collegamento (link). Si osservi che GNU ld (ovvero il programma usato automaticamente da gcc per questo scopo) non richiede necessariamente tale accorgimento, ma ugualmente è meglio curarsi di rispettare il principio.

571.7   Prevenzione e ricerca degli errori

Il linguaggio C può essere usato «bene» o «male», così come ogni altro linguaggio. Nel caso particolare del C, certi modi leciti di scrivere un programma possono essere facilmente motivo di errori banali, evitabili se si chiede al compilatore di segnalare anche le piccole mancanze. In pratica, con gcc è bene usare sempre l'opzione -Wall per ottenere la segnalazione di una serie numerosa di avvertimenti; eventualmente a questa opzione si può aggiungere -Werror, con la quale si trasformano gli avvertimenti in errori, così da evitare che in loro presenza la compilazione vada a buon fine.

Per analizzare il funzionamento del programma con GDB o altri analizzatori simili, conviene aggiungere l'opzione -gstabs, oppure un'altra opzione che inizi per -g..., in base alle caratteristiche del programma usato per l'analisi.

Infine, disponendo di un sistema GNU, o di un altro sistema compatibile con il modello di Unix, è bene abilitare lo scarico dell'immagine dei processi elaborativi in un file (core dump). Così facendo, quando durante il funzionamento un programma tenta di eseguire un'azione che il sistema impedisce, questo programma viene fermato e scaricato in un file core che può essere analizzato successivamente con GDB. A titolo di esempio viene mostrato un sorgente che produce un errore del genere:

int main (void)
{
    int a;
    a = 1 / 0;
    return a;
}

Se si compila il programma con l'accortezza di aggiungere l'opzione -Wall si viene avvisati del problema, ma in questo caso si preferisce ignorarlo:

gcc -Wall -gstabs errore.c[Invio]

errore.c: In function ‘main’:
errore.c:4: warning: division by zero

Prima di proseguire, ci si assicura che lo scarico dell'immagine del processo elaborativo sia abilitata:(1)

ulimit -c unlimited[Invio]

Si avvia il programma difettoso:

./a.out[Invio]

/bin/sh: line 1: 12134 Floating point exception(core dumped) ./a.out

Il messaggio della shell avvisa di avere «scaricato la memoria», ovvero di avere creato il file core. Con GDB si può procedere alla ricerca di cosa è stato a causare l'errore:

gdb a.out core[Invio]

...
Core was generated by `./a.out'.
Program terminated with signal 8, Arithmetic exception.
#0  0x08048344 in main () at errore.c:4
4           a = 1 / 0;

571.8   Problemi con l'ottimizzazione

Il compilatore gcc consente di utilizzare diverse opzioni per ottenere un risultato più o meno ottimizzato. L'ottimizzazione richiede una potenza elaborativa maggiore, al crescere del livello di ottimizzazione richiesto. In situazioni particolari, può succedere che la compilazione non vada a buon fine a causa di questo problema, interrompendosi con segnalazioni più o meno oscure, riferite alla scarsità di risorse. In particolare potrebbe essere rilevato un uso eccessivo della memoria virtuale, per arrivare fino allo scarico della memoria (core dump).

È evidente che in queste situazioni diventa necessario diminuire il livello di ottimizzazione richiesto, modificando opportunamente le opzioni relative. L'opzione in questione è -On, come descritto nella tabella 571.7. In generale, l'assenza di tale opzione implica la compilazione normale senza ottimizzazione, mentre l'uso dell'opzione -O0 può essere utile alla fine della serie di opzioni, per garantire l'azzeramento delle richieste di ottimizzazione precedenti.

Tabella 571.7. Opzioni di ottimizzazione per gcc.

Opzione Descrizione
-O
-O1
Ottimizzazione minima.
-O2
Ottimizzazione media.
-O3
Ottimizzazione massima.
-O0
Annullamento delle richieste precedenti di ottimizzazione.

Alle volte, compilando un programma, può succedere che a causa del livello eccessivo di ottimizzazione prestabilito, non si riesca a produrre alcun risultato. In questi casi, può essere utile ritoccare lo script di Make, dopo l'uso del comando configure; per la precisione si deve ricercare un'opzione che inizia per -O. Purtroppo, il problema sta nel fatto che spesso si tratta di più di uno script, in base all'articolazione dei file che compongono il sorgente.

Ammesso che si tratti dei file Makefile, si potrebbe usare il comando seguente per attuare la ricerca:

find . -name Makefile \
  \-exec echo \{\} \; \
  \-exec grep \\-O \{\} \;
[Invio]

Il risultato potrebbe essere simile a quello che si vede qui di seguito:

./doc/Makefile
./backend/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./frontend/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./include/Makefile
./japi/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./lib/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./sanei/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./tools/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./Makefile

In questo caso, si può osservare che i file ./doc/Makefile, ./include/Makefile e Makefile, non contengono tale stringa.

Tabella 571.9. Riepilogo delle altre opzioni utilizzate con gcc nel corso del capitolo.

Opzione Descrizione
-E
Elabora il file solo con il precompilatore.
-S
Genera un file in linguaggio assemblatore (prende il sopravvento sull'opzione -c).
-c
Fa sì che la compilazione salti la fase di collegamento (link). In condizioni normali serve a generare solo i file-oggetto. Se si usa questa opzione, ma non si specifica l'opzione -o, il file-oggetto ha un nome con la stessa radice del file sorgente e l'estensione .o.
-o nome_file
Dichiara il nome del file che si vuole ottenere.
-static
-dynamic
Richiede espressamente di compilare utilizzando le librerie statiche o dinamiche.
-llibreria
Indica il nome di una libreria da utilizzare. Il nome del file che la contiene può essere liblibreria.a o liblibreria.so, a seconda che si tratti di una libreria statica o dinamica.
-Lpercorso
Indica un percorso in cui ricercare i file delle librerie, che prende la precedenza sugli altri già considerati.
-Ipercorso
Indica un percorso in cui ricercare i file da includere, che prende la precedenza sugli altri già considerati.
-Wall
Richiede di mostrare tutti i messaggi che avvertono dell'uso imperfetto del linguaggio (warning).
-Werror
Fa sì che tutte le segnalazioni di avvertimento siano trattate come errori e portino al fallimento della compilazione.
-gstabs
Inserisce delle annotazioni, con le quali i programmi come GDB possono abbinare il sorgente originale all'esecuzione controllata del programma.

571.9   Riferimenti


1) Di norma, il comando ulimit è gestito internamente dalla shell; in questo esempio si fa riferimento a una shell POSIX.


Appunti di informatica libera 2008 --- Copyright © 2000-2008 Daniele Giacomini -- <appunti2 (ad) gmail·com>


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome compilazione_c_dall_x0027_alto_in_basso.htm

[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home]

Valid ISO-HTML!

CSS validator!

Gjlg Metamotore e Web Directory