Linux

Compilare i programmi sotto Linux

Vediamo come compilare i programmi in C sotto Linux, utilizzando i vari strumenti presenti in questo sistema e nei vari sistemi Unix

di Lorenzo Bettini e Antonio Gallo

Gli utenti di PC, e soprattutto quelli che utilizzano Windows, sono ormai abituati al concetto di programma, pensato come eseguibile; con questo termine si intende il classico file .exe. In ambiente Unix e soprattutto Linux, le cose sono leggermente differenti, e ci si trova spesso a dover “creare” tale programma. In questo articolo vedremo come utilizzare i tool di programmazione per il linguaggio C, presenti sotto Linux, e nei vari sistemi Unix, per “costruirsi” gli eseguibili. L’articolo non avrà la pretesa di insegnare a programmare in C; l’intento principale sarà quello di mettere in grado anche chi non è un programmatore di compilare programmi forniti solo in formato sorgente (come spesso accade sotto i sistemi Unix); del resto i programmatori in ambiente Windows, abituati ad ambienti visuali, avranno la possibilità di conoscere i comandi utili per scrivere e compilare programmi sotto questi sistemi.

Sorgente, binario e compilatore

Gli eseguibili di cui si è parlato sopra sono anche detti binari; con questo si intende (estendendo il significato base del termine), che tali file contengono codice macchina, cioè scritto in linguaggio macchina, e quindi direttamente eseguibile dal processore. Il processore riconosce solo sequenze di 0 e di 1 (da qui il nome di binario), e sarebbe molto difficile, per un programmatore, scrivere un programma direttamente in linguaggio macchina. Un passo avanti sarebbe utilizzare il linguaggio assembler, in cui ogni istruzione corrisponde ad un’istruzione in linguaggio macchina. Per ovviare a questi inconvenienti sono stati creati i cosiddetti linguaggi ad alto livello, coi quali è possibile scrivere programmi utilizzando una sintassi molto più vicina al linguaggio naturale. Del resto, per quanto detto, il processore è in grado di riconoscere solo sequenze di 0 ed 1, e quindi si rende necessario un mezzo per tradurre un programma scritto con un linguaggio ad alto livello in un programma in linguaggio macchina. Per questo sono stati pensati i compilatori. La definizione classica di compilatore [Aho] è: un compilatore è un programma che traduce da un linguaggio detto sorgente in un altro linguaggio detto target. Tipicamente, e senz’altro nel nostro caso, il linguaggio sorgente è un linguaggio ad alto livello, mentre il linguaggio target è il linguaggio macchina. Il programma scritto col linguaggio ad alto livello viene detto sorgente. Ovviamente un programma complesso sarà costituito da più listati sorgente (di questo si parlerà nel corso dell’articolo). I programmi ad alto livello più utilizzati, almeno in ambiente Windows, sono Visual Basic, Delphi, e C/C++. In ambiente Unix invece prevale il C.

Due filosofie a confronto

Se adesso si andasse a cercare i sorgenti dei programmi Windows che si è utilizzato fino ad adesso, è altamente probabile che la ricerca abbia esito negativo. Questo del resto non ci impedisce di utilizzare tali programmi: una volta che si ha il programma eseguibile (scritto in linguaggio macchina) non si ha più bisogno dei sorgenti, come quando si legge un libro tradotto in Italiano non si ha bisogno della versione originale, scritta in un’altra lingua.

L’assenza dei sorgenti nei programmi commerciali è comunque dovuta alla volontà da parte della casa produttrice di proteggere il proprio software: se gli altri potessero vedere come è realizzato un certo programma, potrebbe poi non essere più necessario acquistare gli aggiornamenti, e senz’altro si sarebbe in grado di superare le varie protezioni software che rendono necessario l’acquisto della licenza dei programmi. Questa è una filosofia commerciale.

Linux, da parte sua, è nato come sistema operativo gratuito seguendo la filosofia del gruppo GNU [gnu], che sostiene il software OpenSource [OSS]; con questo termine riguarda soprattutto la disponibilità dei sorgenti: a partire dal sistema operativo stesso il software della GNU, e comunque tutto il software distribution sotto la licenza GPL, viene distribuito insieme ai sorgenti lasciando libera la possibilità di apportarvi modifiche. E’ un po’ come acquistare un libro tradotto, ma col testo originale a fronte. Da un lato è possibile consultare i sorgenti dei vari programmi in modo da apprendere nuove cose, ma anche, e soprattutto, apportare modifiche e correzioni (in questo modo tutti potranno usufruire del lavoro di tutti); dall’altro, avendo i sorgenti, è possibile costruire il programma (compilandolo) anche su altre macchine (teoricamente anche su altri sistemi, anche se più che altro tale software è pensato per ambienti Unix).

A tal proposito la maggior parte del software disponibile per Unix è scritto in C (ed in minor parte in C++); è quindi necessario avere un compilatore C per costruirsi il programma eseguibile (o i programmi) vero e proprio. Fortunatamente questo non è un problema, in quanto la GNU mette a disposizione un potentissimo compilatore C/C++ e tantissimi tool di sviluppo, che si possono trovare in tutte le distribuzioni di Linux. Del resto tali tool sono disponibili, come corredo standard, anche negli altri sistemi Unix.

Non è nostra intenzione criticare la prima filosofia rispetto a quella software libero: entrambe hanno le loro motivazioni ed il loro diritto di esistere.

Il compilatore C sotto Linux

Per chi è abituato ai vari ambienti visuali sotto Windows, l’impatto con la linea di comando dei tool per il C sotto Linux è abbastanza drammatico. Tuttavia, una volta preso un minimo di confidenza, si sentirà sempre meno la necessità di un ambiente integrato. Vediamo quindi alcuni esempi di utilizzo del compilatore C; per tale scopo utilizzeremo il classico programma Hello World, che semplicemente scrive un saluto sullo schermo (Listato 1):

per compilare un tale programma (supponendo che sia memorizzato nel file hello.c) basterà lanciare il seguente comando:

gcc hello.c

gcc è il comando del compilatore C della GNU; sugli altri sistemi Unix tale compilatore può chiamarsi semplicemente cc. In ambiente Linux, per mantenere questo nome è di solito presente anche cc (un link simbolico a gcc).

Se non ci sono errori nella compilazione, ed in questo caso non dovrebbero esserci, alla fine si avrà nella directory un file a.out, che potrà essere eseguito per vedere il programma hello in esecuzione. Nella directory sarà anche presente il file hello.o, che non è altro che la traduzione in linguaggio macchina del nostro programma (o sta per codice oggetto, mentre in ambiente Windows spesso l’estensione è obj), ma non direttamente eseguibile. Infatti l’istruzione printf è in realtà la chiamata di una funzione, che è implementata nella libreria standard del C (una sorta di repository con varie funzioni di utilità generale). Il prototipo di tale funzione (cioè il numero di parametri richiesti dalla funzione ed il tipo di ritorno) è contenuto nel file stdio.h che viene (letteralmente) incluso all’inizio del programma. Tali file sono una caratteristica del C e vengono detti file header (intestazione). In realtà la creazione di un programma è una procedura che avviene in diversi passi riassumibili principalmente in:

  1. compilazione: vengono creati i file oggetto (con la traduzione in linguaggio macchina dei sorgenti)
  2. linking: viene creato l’eseguibile vero e proprio, aggiungendo il codice della funzioni di libreria utilizzate.

In realtà col comando lanciato i due passi vengono eseguiti automaticamente, ed alla fine i file oggetto potranno essere cancellati in quanto non necessari per eseguire il programma.

Alcune opzioni di compilazione

In realtà il compilatore gcc ha tantissime opzioni [GCC], ed alcune di queste sono molto utili. La prima che vediamo è quella che ci permette di stabilire un nome per il file di output del compilatore, che nel nostro caso è il programma eseguibile: ad esempio se volessimo chiamare il programma hello (in Unix non è necessario che gli eseguibili abbiano una estensione, anzi di solito non ce l’hanno), il comando sarà

gcc –o hello hello.c

quindi l’opzione –o filename permette di definire il nome del file di output.

E’ inoltre possibile eseguire separatamente le due fasi di creazione del programma (si vedrà più avanti che nel caso il programma sia costituito da più sorgenti questo è necessario): prima la compilazione (con l’opzione –c):

gcc –c hello.c

e poi la creazione dell’eseguibile, cioè la fase di linking:

gcc –o hello hello.o

Il comando del linker in Linux è ld, ma sarebbe necessario passare alla linea di comando esplicitamente altre opzioni, come la libreria standard del C, e questo può essere noioso. Fortunatamente però il gcc riconosce le estensioni degli argomenti, ed agisce di conseguenza: nell’ultimo comando non esegue la compilazione, ma il linking, in quanto il file passato è un file oggetto. Inoltre provvede automaticamente a collegare il nostro file oggetto con la libreria standard del C. Anche per un programma scritto in C++ non è necessario utilizzare un altro compilatore (il compilatore esplicito per il C++ si chiama g++ oppure c++): basterà specificare come estensione .cc; infatti per compilare il file hello.cc (versione scritta in C++ del precedente programma, e disponibile sul sito web dell’Infomedia), il comando di compilazione non sarà molto diverso:

gcc –c hello.cc

Il compilatore riconosce che si tratta di un programma C++ dall’estensione .cc. Purtroppo non è in grado di riconoscere che si tratta di un file oggetto C++, in quanto anche in questo caso viene creato hello.o; allora è necessario effettuare il linking richiamando direttamente il comando del compilatore C++ (e visto che ci siamo chiamiamo l’eseguibile in un altro modo):

g++ -o hello2 hello.o

Nella tabella 1 sono rappresentate alcune delle convenzioni per le estensioni dei file utilizzate dal gcc.

Librerie statiche e librerie dinamiche

Tutti i programmi C fanno uso di funzioni, come ‘printf’, che fanno parte di librerie (la libc in questo caso). Le librerie sono di due tipi: statiche e dinamiche. La differenza fondamentale tra le due tipologie è il modo in cui vengono collegate al programma eseguibile.

Le librerie statiche (con estensione .a), sono linkate insieme ai file oggetto per formare il file eseguibile. Questo aumenta di molto le dimensioni del programma ma in alcuni casi è utile per evitare problemi che spesso si hanno con l’utilizzo delle librerie dinamiche. La presenza delle librerie statiche non è necessaria al momento dell’esecuzione del programma.

Le librerie dinamiche (con estensione .so), non vengono linkate all’interno del file eseguibile ma restano separate. Sarà compito del sistema operativo, all’atto del caricamento del programma in memoria, di caricare tutte le librerie dinamiche richieste dall’eseguibile. In questo modo, oltre a ridurre le dimensioni di quest’ultimo, ogni libreria dinamica, è caricata in memoria una sola volta anche se sono presenti più processi che la utilizzano. Le librerie dinamiche, affinché l’eseguibile possa girare correttamente, devono essere presenti nel sistema, ma spesso ciò non accade.

Per risolvere quest’ultimo problema, Linux ci viene incontro fornendoci il tool “ldd”. Questo ci permette di determinare quali librerie vengono utilizzate da un file eseguibile. Eccone un esempio di utilizzo:

# ldd /sbin/ldconfig
 statically linked (ELF)
# ldd /bin/echo
 libc.so.5 => /lib/libc.so.5 (0x40009000)

Nella prima riga dell’esempio viene eseguito il comando ldd sul file ‘ldconfig’, il risultato è che il file risulta linkato staticamente in formato ELF. Nel secondo esempio, invece, il comando “echo” risulta linkato dinamicamente alla libreria di sistema (libc.so.5), inoltre viene indicato il percorso dove tale libreria si trova.

Risolvere alcuni problemi di compilazione

Tra i tanti problemi di compilazione che possono insorgere, i più frequenti sono quelli dovuti al fatto che il compilatore non riesce a trovare dei file header, o delle librerie. Vediamo come esempio il programma nel Listato 2 che crea una finestra in X-Window utilizzando la libreria GTK (www.gtk.org). Provando a compilare questo programma otteniamo:

# gcc gtk-hello.c gtk-hello.c:1: gtk.h: No such file or directory

Il messaggio d’errore è stato generato perché il compilatore non ha trovato l’header “gtk.h” nei suoi percorsi di ricerca. Per risolvere questo problema dobbiamo specificare al compilatore dove cercare tale file. Se non si conosce la directory dove si trova tale file, è possibile cercarlo con il seguente comando:

# find / -xdev -name gtk.h
/usr/include/gtk/gtk.h

una volta localizzato il file, utilizzando l’opzione -I, possiamo specificare al compilatore un ulteriore percorso aggiuntivo per la ricerca degli header file

#gcc gtk-hello.c -I/usr/include/gtk/
/tmp/cca006371.o: In function Main’:
/tmp/cca006371.o(.text+0xc): undefined reference to G_print’
/tmp/cca006371.o(.text+0x1c): undefined reference to Gtk_init’
.
.
.
collect2: ld returned 1 exit status

La compilazione del programma è andata bene, ma questa volta è il linker ‘ld’ a segnalare che non è stato possibile ‘risolvere’ alcuni ‘simboli’. Questo perché non abbiamo specificato quali sono le librerie dinamiche da collegare al programma. Ciòsi ottiene utilizzando l’opzione -l (elle minuscola), come nel seguente esempio:

# gcc gtk-hello.c -I/usr/include/gtk -lgtk -lgdk -lglib -lXext -lX11
-lm
/usr/i586-pc-linux-gnulibc1/bin/ld: cannot open -lXext: No such file or directory
collect2: ld returned 1 exit status

specificare quali librerie dinamiche utilizzare non è a volte sufficiente, occorre anche dire al linker dove cercarle. Per far ciò si utilizza il parametro -L per specificare percorsi di ricerca aggiuntivi per le librerie.

# gcc gtk-hello.c -I/usr/include/gtk -lgtk -lgdk -lglib -lXext -lX11
-lm -L/usr/X11R6/lib -o gtk-hello.exe

Al termine del comando l’eseguibile è pronto per essere lanciato con il nome di gtk-hello.exe

Compilare un programma più complesso

Non tutti i programmi sono basati su un unico file sorgente, progetti software medio-grandi superano facilmente il centinaio. In questi casi si utilizza il tool ‘make’.

Make è un programma scritto appositamente per automatizzare il processo di compilazione. Il funzionamento di make è controllato da un “Makefile”. Questi è un programma che contiene al suo interno una serie di “macro” (comandi orientati all’elaborazione di testo) e “rules” (regole), che gli forniscono informazioni su quali sorgenti compilare e secondo quale modalità, basandosi sulla data dell’ultima modificadei file. Un esempio di Makefile utilizzabile per compilare il precedente programma è illustrato nel Listato 3. Partendo dall’alto notiamo una serie di variabili: CC, indica il compilatore da utilizzare; CFLAGS, i parametri da passare al compilatore; LDFLAGS, le librerie da utilizzare per linking; LDPATHS, i percorsi dove il linker deve cercare le librerie; INCLUDES, i percorsi dove il compilatore deve cercare gli header file; HEADERS, la lista degli header del progetto; OBJECTS, la lista di tutti i file oggetto che make deve creare; PROGRAM, infine, è il nome del programma eseguibile da creare.

Dopo questo elenco di variabili incontriamo la prima regola, in cui vengono specificate le ‘dipendenze’ per il target ‘all’, questo è il target di default quando make viene eseguito senza specificare parametri.

Nell’esempio fornito il target ‘all’ viene utilizzato per istruire make in modo tale che crei il file eseguibile e tutti i file oggetto.

La regola successiva, invece, istruisce make su come creare il programma eseguibile (viene specificata la riga di comando per effettuare il linking dei file oggetto). Inoltre viene stabilito che l’eseguibile gtk-hello.exe “dipende” da i file specificati nelle variabile OBJECT ed HEADER.

Come creare i file oggetto, partendo dai file sorgente, é specificato nella regola ‘.c.o:’. Questa regola dice a make che un file .o può essere creato elaborando il corrispondente file .c utilizzando il comando specificato nella linea successiva.

L’ultima regola, ‘clean’ viene utilizzata per iniziare ex-novo una nuova compilazione cancellando l’eseguibile e tutti i file oggetto.

Il programma d’esempio può essere compilato nel seguente modo:

# make clean ; make
rm -f gtk-hello.o gtk-hello.exe
gcc -c gtk-hello.c -g -O2 -Wall -I/usr/include/gtk -o gtk-hello.o
gcc -lgtk -lgdk -lglib -lXext -lX11 -lm -lc -L/lib -L/usr/lib -L/usr/X11R6/lib -o gtk-hello.exe gtk-hello.o

L’eseguibile è stato creato e, volendo, si può controllare quali sono le librerie che utilizza.

Make ed i Makefile oltre ad automatizzare il processo di compilazione lo velocizzano. Infatti rilanciando nuovamente make otteniamo:

# make
make: Nothing to be done for ‘all’.

questo perché make non ricrea nessun file a meno che una delle ‘dipendenze’ non sia soddisfatta (sempre basandosi sulla data dell’ultima modifica). Infatti, se modifichiamo un file sorgente, solo il file oggetto (che risulterà più “vecchio”), corrispondente al sorgente modificato, verrà ricreato; il vantaggio in termini di tempo è chiaramente tangibile.

Creare un Makefile è fuori dallo scopo di quest’articolo, chi vuole approfondire il discorso può leggere la documentazione (info make) allegata al tool.

Compilare un programma GNU

Compilare un programma GNU è un’operazione abbastanza semplice se si hanno già a disposizione tutte le librerie come prerequisito. Una volta ottenuto l’archivio contenente il programma in formato sorgente è consigliabile estrarlo nel percorso /usr/local/src destinato alla compilazione di programmi per uso ‘locale’. Per estrarre un programma in formato tar.gz si può fare nel seguente modo:

# cd /usr/local/src
# tar -zxvf nome.archivio.tar.gz

Prima di procedere a qualsiasi operazione successiva vale la pena spendere un pò di tempo nella lettura dei file README ed INSTALL di solito forniti a corredo. La lettura di questi file è indispensabile per capire se il nostro sistema ha tutti i prerequisiti necessari all’installazione del programma.

I programmi GNU non sono di solito forniti con Makefile, è infatti necessario lanciare lo script ‘configure’ per crearli. Questo script analizza il sistema su cui sta girando, creando un Makefile ‘ad-hoc’ da poter essere utilizzato sull’attuale piattaforma. Questo sistema rende un programma GNU, non solo portatile attraverso i vari tipi di Unix, ma anche attraverso i vari tipi di processori.

Prima di eseguire ‘configure’ e può valere la pena leggere le opzioni che a volte contengono informazioni non contenute nella documentazione, col seguente comando:

# ./configure --help | more

Se configure termina con successo il programma potrà essere compilato ed installato con i seguenti comandi:

# ./configure --prefix=/usr/local/
# make
# make install

Il parametro prefix specifica a configure di creare i Makefile in modo tale che i file eseguibili vengano installati all’interno della gerarchica /usr/local

Per districarsi tra la miriade di messaggi generati da make & make install si puo’ usare la seguente sintassi:

# make && make install && echo "Tutto OK !"

cio’ garantisce che il messaggio finale venga stampato solo se le due operazioni precedenti sono andate a buon fine.

Conclusioni

Come abbiamo visto la compilazione di un programma GNU risulta abbastanza facile se il sistema risulta correttamente configurato. Chi volesse approfondire l’argomento e non ha a disposizione Linux può cercare sul sito della Cygnus (www.cygnus.com) il compilatore gcc per win-32, oppure sul sito di DJ Delorie (www.delorie.com) per il compilatore gcc per DOS (djgpp), che genera comunque programmi a 32 bit.

Approfondiremo alcuni degli argomenti trattati in articoli successivi.

Bibliografia

[Aho] A.V.Aho, R.Sethi, J.D.Ullman Compilers, Principles, Techniques, and Tools, 1987, Addison Wesley.

[gnu] GNU Home site: www.gnu.org

[GCC] Guida in linea del compilatore C della GNU

[OSS] www.opensource.org