Compilare programmi C++ con Linux e l'ambiente di sviluppo GCC 2
1 Introduzione
Questo documento vuole fornire le istruzioni principali per compilare programmi in C++ su sistemi Unix che dispongano del compilatore g++ (che è parte integrante della suite di sviluppo GCC).
In questa introduzione spiegheremo come fare tutto ciò tramite la riga di comando (shell). É possibile eseguire tutto anche all'interno di sofisticati ambieni integrati di sviluppo (IDE = Integrated Develompment Environment, come Kdevelop o SourceNavigator), talvolta anche senza conoscere i meccanismi della compilazione: in questo caso questo documento vi servirá per capire cosa succede dietro le quinte.
2 Compilare un programma di un solo file
Per prima cosa verifichiamo se sul sistema è presente il compilatore che ci serve; digitiamo con la shell il comando
g++ --version
Se la risposta è qualcosa del tipo ``command not found'' allora abbiamo un problema: contattate il sistemista oppure provvedete a installare correttamente il software. Se la risposta è un numero del tipo 2.96 allora quella è la versione del compilatore che avete a disposizione. Tenete a mente che le informazioni sugli errori di compilazione o il programma eseguibile che otterrete compilando, sono dipendenti dalla versione del compilatore.
A questo punto abbiamo il compilatore, per cui facciamolo girare su un nostro programma, per esempio (salviamo il codice sorgente che segue con il nome ciao.cpp3):
#include <iostream>
using namespace std;
int main() {
cout << "Ciao.\n";
}
Alla riga di comando, nella directory di ciao.cpp scriviamo
g++ ciao.cpp
se non appare alcun messaggio vuole dire che tutto è andato correttamente, e nella stessa directory troveremo un nuovo file a.out che possiamo eseguire:
a.out
(o ./a.out se la directory corrente non è presente nel PATH, come spesso accade). Il programma svolge il suo compito mostrando la scritta Ciao. .
Se all'atto della compilazione avete ricevuto alcuni messaggi di lamentela (warning e segnalazione di errori), copiate meglio il programma e preoccupatevi perchè avete appena dato prova di non saper scrivere neppure ``Hello World!'' in C++.
3 Alcuni flag di base
Non vogliamo sempre ottenere degli eseguibili che si chiamino a.out per cui GCC ha messo a disposizione il flag ``-o <nome_eseguibile>'' per fare ciò:
g++ ciao.cpp -o ciao
Otteniamo quindi un eseguibile di nome ciao. Niente di spettacolare insomma.
Talvolta vogliamo compilare del codice sorgente per ottenere una libreria più che un programma eseguibile. La libreria verrà poi utilizzata da un altro programma per farlo funzionare. Dobbiamo quindi dire al compilatore che deve produrre un file oggetto (di estensione .o), e lo facciamo utilizzando il flag -c :
g++ -c nuovalibreria.cpp
Otteniamo un file oggetto nuovalibreria.o che potrá essere utilizzato quando vorremo compilare un programma che lo utilizza. L'ambiente di compilazione GCC è piuttosto furbo e se non ci sono ambiguità permette di mettere i flag anche in ordine sparso (tipo ``g++ nuovalibreria.cpp -c''). Tu sii ordinato e segui le regole finchè stai imparando.
Succede spesso che il programma che scrivi debba farsi qualche giro di debugger prima di funzionare correttamente; per fare questo il debugger ha bisogno di alcune informazioni rigurdanti il tuo sorgente, altrimenti ti mostrerà solo l'assembler, il che è poco utile. Per aggiungere al tuo eseguibile le informazione sui nomi delle variabili che hai usato nel tuo sogente e i riferimenti ai numeri di riga più altre cose (che vanno sotto il nome di informazioni di debug), devi usare il flag -g:
g++ -g ciao.cpp -o ciao
Così ottieni un eseguibile un po' più grossetto che ha dentro quelle informazioni. In un secondo tempo puoi anche rimuovere questa informazione in più con il comando
strip ciao
che in realtà toglie un po' di cose in più che il compilatore mette dentro in ogni caso ma che per la pura esecuzione non servono; usalo solo se hai veramente finito lo spazio su disco, visto che il processo é irreversibile.
Quando il tuo programma funziona e non ha più bisogno di girare nel debugger (false illusioni), puoi compilarlo in modo che il codice sia ottimizzato per avere più efficienza. C'è a questo proposito il flag -O (nota che non é uno zero 0, ma la lettera ``o'' maiuscola); ha anche altre varianti -O2 e -O3 che permettono di ottenere un progrmma sempre più veloce:
g++ -O ciao.cpp -o ciao
g++ -O2 ciao.cpp -o ciao
g++ -O3 ciao.cpp -o ciao
Di norma GCC cerca di minimizzare il tempo di compilazione e lo spazio richiesto in memoria (detto costo di compilazione). Con -O (equivalente a -O1) si cambia questo obiettivo che diventa quello di minimizzare la dimensione del programma compilato e migliorare la sua velocità. Con -O2 si pone l'accento solo sulla velocità di esecuzione del compilato, mentre con -O3 si spinge a cercare la velocita ancora di più, molto a scapito della lunghezza del compilato finale: per esempio si usa molto l'inlining delle funzioni e lo srotolamento dei cicli. Nelle nuove versioni di GCC ci sono ottimizzazioni ancora più spinte, ma il loro utilizzo è ancora sperimentale e quindi non usarlo, visto che stai ancora imparando i fondamenti della compilazione. Nota che le ottimizzazioni non investono le funzionalità speciali eventualmente presenti sul nostro processore (pentium, MMX ecc.) ma solo l'assembler da un punto di vista teorico; per ottenere un eseguibile che sia fatto su misura per le potenzialità del nostro specifico processore ci sono altri flag che vedremo poi.
Si ha sempre l'esigenza di eliminare tutti i possbili bachi del software e quindi avere indicazioni sempre più dettagliate dal compilatore su possibili pasticci che abbiamo combinato è una vera manna. Per questo possiamo indicare al compilatore il grado di puntigliosità con cui analizzare il nostro progrmma sorgente quando lo compila; con -ansi imponiamo al compilatore di essere aderente alle specifiche ANSI in modo stretto e di segnalare ogni difformità; con -pedantic lo si fa diventare un po' paranoico, al punto che tende a non gradire costrutti del linguaggio che potrebbero dare problemi, anche se il codice è formalmente corretto; con -Wall gli si impone di controllare classici costrutti che possono creare problemi in determinate situazioni (es. variabili non inizializzate, non utilizzate e molto altro) e di segnalarne la presenza:
g++ -ansi ciao.cpp -o ciao
g++ -pedantic ciao.cpp -o ciao
g++ -Wall ciao.cpp -o ciao
Quando il programma vi sembra funzionare, utilizzate tutti questi flag e vedete quello che succede.
Nota importante: non ha senso richiedere un'ottimizzione e contemporaneamente le informazioni di debug, perchè non rimane più una corrispondenza diretta con il sorgente; in linea teorica è possibile farlo (GCC lo permette) ma tu non farlo, ti creerà solo problemi.
4 Compilare un programma composto da più file
Nulla di difficile, come metodo base basta compilare tutti i file insieme (ovviamente uno solo di essi dovrà contenere il main()):
g++ a.cpp b.cpp c.cpp main.cpp -o ciao
Se si devono fare continuamente modifiche, ricompilare ogni file tutte le volte non serve e può portare via molto tempo; in questo possiamo fare la compilazione separata delle varie parti del programma:
g++ -c a.cpp
g++ -c a.cpp
g++ -c a.cpp
g++ -c main.cpp
g++ a.o b.o c.o main.o -o ciao
Di volta in volta, a seconda di quale file andiamo a modificare, basta ripetere la riga corrispondente e quella finale.
Avendo più file da compilare spesso si desidera indicare al compilatore ulteriori directory dove cercare header file o librerie già compilate; il GCC mette a disposizione il flag -I<directory/degli/header> e -L<directory/delle/librerie>. É importante ricordare che, per compilare un programma che utilizza delle librerie già compilate, non basta indicare la directory della libreria in questione, ma occorre indicare esplicitamente il nome (ridotto) del file della libreria con il flag -l<nome_libreria>; siccome tutte i nomi di queste librerie iniziano per lib e finiscono con .a oppure .so (per esempio la libreria matematica si chiama, con poca fantasia, libm.a), allora basta indicare il nome ridotto, senza lib e .a (oppure .so, devi saperlo tu, visto che stai usando quella libreria!). Per esempio, se il nostro programma ciao.cpp utilizza la libreria matematica (il suo header è il notissimo math.h) allora per compilare dobbiamo scrivere:
g++ -lm ciao.cpp -o ciao
Tutte i flag esposti in precedenza (ottimizzazioni, informazioni di debug, controllo pignolo del sorgente ecc.) possono essere utilizzate tranquillamente anche quando si compilano più file.
5 Altri flag che possono tornare utili
Sebbene GCC abbia centinaia di flag che si possono usare, perlopiù molti non vengono comunemente usati o vengono inclusi in blocchi da altri (per esempio -Wall è un alias per una ventina di flag più specifici). Qui ti riporto alcune di quelle ancora non viste ma che incontrerai frequentemente.
* -D<macro_name> (nota che non c'è uno spazio tra la D e il nome della macro) equivale ad inserire nei file sorgente da compilare la riga
#define <macro_name>
In pratica e' utilizzato per attivare oppurtuni flag in compilazione, come nel seguente esempio:
g++ -DDEBUG ciao.cpp -o ciao
che attiverà eventuali controlli presenti nel codice del tipo
#ifdef DEBUG
...(codice C++)...
#endif
Molto utile.
* -S invece di generare il compilato genera un file di testo con dentro l'assembly del programma. Quando hai più file non esegue il linking. Utile se sai leggere l'assembly e hai tempo da perdere.
* -E fa girare solo il preprocessore (cpp, C Pre-Processor), quindi include il codice sorgente degli header, sostituisce le macro ecc. Utile se vuoi capire come sono sono andate a finire le tue macro.
* -static appiccica al tuo sorgente tutte le librerie che il tuo programma utilizzerà, compresa una copia di tutte quelle di sistema che gli servono. Il file eseguibile diventa enorme ma ha il grande pregio di funzionare anche dove mancano le librerie che ti servono o ci sono versioni differenti. Utile se distribuisci il tuo programma in vari formati (sorgente, eseguibile, autinstallante ecc.) . Attenzione a non abusarne.
* -b <machine> ebbene sí, potete usare il vostro Pentium per compilare un programma per Digital Alpha o processori RISC, basta che abbiate le librerie necessarie a bordo. Ricordiamoci che il compilatore alla fine è solo un traduttore da un linguaggio ad un altro!
* -v mostra tutte le singole azioni che sta compiendo il compilatore. Istruttivo.
* -pg genera codice aggiuntivo che insieme al programma gprof permette di studiare il tempo speso dal vostro software nelle varie funzioni, in modo da capire dove sono i colli di bottiglia. Studia gprof prima.
* -B<directory> aggiunge la directory indicata al PATH dove cercare librerie, include, eseguibili, dati e il compilatore stesso. Utile se si ha fretta e non vuoi specificare ogni singola voce.
*
-mcpu=i386
-mcpu=i486
-mpcu=i585
-mcpu=i686
-mcpu=pentium
Specifica quale cpu si ha a disposizione per creare codice macchina che utilizzi le funzionalità speciali del proprio processore. Utile, ma attenzione a distribuire in giro l'eseguibile: quello che funziona su un pentium non è detto che funzioni su un 486.
6 Conclusione
Il bellissimo GCC non è perfetto e ha sempre qualche bug che nel tempo viene risolto (è il compilatore più usato di sempre e quindi quello più analizzato e debuggato). Non sperare però di incappare facilmente in queste situazioni: molto probabilmente non le incontrerai in tutta la vita. Se il tuo programma non funziona è il tuo codice ad avere problemi e non è un errore del compilatore. Quanto ho scritto fin qui è solo un assaggio di quello che GCC può fare; consulta le man page apposite (con i comandi man gcc e man g++) e leggi la documentazione ufficiale (la puoi trovare con un qualsiasi motore di ricerca).
la guida è di Emanuele Olivetti


CITAZIONE
The quieter you become, the more you are able to hear
CITAZIONE
Citazione di LostPassword:
[21:52:01] per ora con queste cose, il loro buffer di memoria traboccherà, meglio non sovrascrivere l'EIP del loro cervello
[21:52:10] poi otteniamo una shell da root nella loro mente
[21:52:10] freghiamo la pass
[21:52:14] e pwniamo