How To… Gcc E Make
Total Page:16
File Type:pdf, Size:1020Kb
1 Gcc e make How to… gcc e make I passi della compilazione Sia gcc che g++ processano file di input attraverso uno o piu' dei seguenti passi: 1) preprocessing -rimozione dei commenti -interpretazioni di speciali direttive per il preprocessore denotate da "#" come: #include - include il contenuto di un determinato file, Es. #include<math.h> #define -definisce un nome simbolico o una variabile, Es. #define MAX_ARRAY_SIZE 100 2) compilation -traduzione del codice sorgente ricevuto dal preprocessore in codice assembly 3) assembly -creazione del codice oggetto 4) linking -combinazione delle funzioni definite in altri file sorgenti o definite in librerie con la funzione main() per creare il file eseguibile. 2 Gcc e make Pre-compilazione Il preprocessor e' un programma che viene attivato dal compilatore nella fase precedente alla compilazione, detta di precompilazione. Il preprocessor legge un sorgente C e produce in output un altro sorgente C, dopo avere espanso in linea le macro, incluso i file e valutato le compilazioni condizionali o eseguito altre direttive. Una direttiva inizia sempre con il carattere pound '#' eventualmente preceduto e/o seguito da spazi. I token seguenti definiscono la direttiva ed il suo comportamento. Una direttiva al preprocessor puo' comparire in qualsiasi punto del sorgente in compilazione ed il suo effetto permane fino alla fine del file. Espansione in linea delle macro Definire una macro significa associare una stringa ad un identificatore. Ogni volta che il preprocessore C incontra l'identificatore cosi' definito, esegue la sua sostituzione in linea, con la stringa ad esso associata. La definizione delle macro avviene per mezzo della direttiva #define Esempio: #define MAX 100 #define STRING_ERR "Rilevato errore !\n" Le macro possono essere definite anche in forma paramentrica; in tal caso la sostituzione dei parametri formali con quelli attuali avviene in modo testuale durante la fase di espansione della macro. Esempio: #define NUM_SCRITTORI 100 void main(){… For(int i=0;i<NUM_SCRITTORI;i++) … } Questo consente di avere costanti con un nome (per esempio NUM_SCRITTORI rappresenta il numero di processi scrittori da allocare) usate in tutto il sorgente, definite in un solo posto e aggiornate automaticamente dappertutto non appena il valore cambia. Se un parametro formale e' preceduto dal carattere pound #, il suo valore attuale e' espanso testualmente come stringa. Esempio: 3 Gcc e make #define DEBUG_OUT(expr) fprintf(stderr, #expr " = %g\n", (float)(expr)) ... DEBUG_OUT(x*y+z); /* espansione: * fprintf(stderr, "x*y+z" " = %g\n", (float)(x*y+z)) */ E' anche possibile annullare la definizione di una macro con la direttiva #undef. Di solito #undef e' impiegata per assicurarsi che una funzione sia definita come tale, piuttosto che come macro. Un altro possibile impiego di #undef e' per la gestione della compilazione condizionale. Esempio: #undef DEBUG Tipicamente sono predefinite 5 macro: __LINE__ Valore decimale del numero della linea corrente del sorgente. __FILE__ Stringa del nome del file in corso di compilazione. __DATE__ Stringa della data di compilazione (formato Mmm dd yyyy). __TIME__ Stringa dell'ora di compilazione (formato hh:mm:ss). __STDC__ Contiene il valore 1 se il compilatore e' conforme allo standard ANSI. Di solito il compilatore accetta nella linea di comando delle opzioni per definire e cancellare delle macro analogamente a quanto viene eseguito con #define e #undef. Tipicamente tali opzioni sono -Dmacro o -Dmacro=def per definire una macro e -Umacro per eliminare la definizione. Le opzioni -D e -U vengono eseguite prima di cominciare l'attivita' di preprocessing sul sorgente. Uso ed esempi Usare ifndef per stampare un messaggio di debug #define DEBUG #ifdef DEBUG Printf(“MESSAGGIO DI DEBUG”); #endif Visto che DEBUG è diverso da 0 le la linea del printf viene compilata ed aiuta nel debug del programma. Successivamente omettendo #define DEBUG 1 le linee non verranno compilate e quindi eseguite. 4 Gcc e make Inclusione di file (#include) Le definizioni ricorrenti delle macro, le dichiarazioni dei prototype di funzione e delle variabili esterne, di solito vengono scritte, una volta per tutte, in files tradizionalmente chiamati header ed aventi normalmente estensione .h. Il preprocessore C, tramite la direttiva #include, puo' ricercare il file indicato in alcune directory standard o definite al momento della compilazione ed espanderlo testualmente in sostituzione della direttiva. Considerato che nel C ogni funzione, variabile, macro deve essere definita o dichiarata prima del suo utilizzo, risulta evidente che ha senso includere gli header file all'inizio, cioe' nella testata (e da qui deriva il nome di header), del file sorgente. La direttiva #include puo' essere impiegata in due forme: #include <nomefile> #include "nomefile" Nel 1° caso il nomefile viene ricercato in un insieme di directory standard definite dall'implementazione ed in altre che sono specificate al momento della compilazione. Nel caso della versione Unix del compilatore, le directory standard di ricerca degli header potrebbero essere /usr/include, /usr/local/include, ... Nel 2° caso il nomefile viene ricercato nella directory corrente e poi, se non e' stato trovato, la ricerca continua nelle directory standard e in quelle specificate al momento della compilazione come nel 1° caso. N.B. - Nel caso che un header venga modificato, e' necessario ricompilare tutti i sorgenti che lo includono. Compilazione condizionale Il preprocessore C al verificarsi di alcune condizioni puo' includere o escludere parti del codice sorgente alla compilazione. Le direttive che indicano al preprocessore la compilazione condizionata sono riportate di seguito: #if espressione_costante_intera #ifdef identificatore #ifndef identificatore #else #elif espressione_costante_intera #endif dove #if, #ifdef, #ifndef testano la condizione. Se risulta verificata, viene incluso per la compilazione il codice dalla riga successiva alla direttiva, fino ad incontrare una delle direttive #else, #elif o #end. In particolare #if testa l'espressione_costante_intera e se risulta diversa da zero, la condizione e' considerata verificata positivamente. L'espressione_costante_intera non puo' comprendere costanti di tipo enumerativo, operatori di cast e sizeof. 5 Gcc e make #ifdef considera superata la condizione se e' definito identificatore. #ifdef identificatore equivale a #if defined (identificatore) o #if defined identificatore #ifndef considera superata la condizione se non e' definito identificatore. #ifndef identificatore equivale a #if !defined (identificatore) o #if !defined identificatore La parte di codice successiva a #else viene passata al compilatore nel caso cha la #if, #ifdef, #ifndef non sia stata soddisfatta. La direttiva #elif equivale ad #else #if tranne il fatto di non aumentare di un livello di annidamento l'intera #if. La direttiva #endif chiude la #if, #ifdef, #ifndef del corrispondente livello di annidamento. Esempio: #ifdef DEBUG fprintf(stderr, "Linea di Debug %d\n", (int)__LINE__); #endif Compilazione Come passo intermedio, gcc trasforma il vostro codice in Assembly. Per farlo deve capire cosa intendevate fare analizzando il vostro codice. Se avete commesso degli errori di sintassi ve lo dirà e la compilazione fallirà. Di solito la gente confonde questo passo con l'intero processo di compilazione. Ma c'è ancora molto lavoro da fare per gcc. Assembly as trasforma il codice Assembly in codice oggetto. Il codice oggetto non può ancora essere lanciato sulla CPU, ma ci si avvicina molto. L'opzione -c trasforma un file .c in un file oggetto con estensione .o Se lanciamo gcc -c game.c creiamo automaticamente un file che si chiama game.o Qui siamo incappati in un punto importante. Possiamo prendere un qualsiasi file .c e creare un file oggetto da esso. Come vedremo più avanti potremo combinare questi file oggetto in un eseguibile nella fase di Link. Andiamo avanti col nostro esempio. Poiché stiamo programmando un gioco di carte abbiamo definito un mazzo di carte come un deck_t, creeremo una funziona per mischiare il mazzo. Questa funzione prenderà un puntatore a un tipo deck e lo riempie con delle carte messe a caso. Tiene traccia di quali carte sono già state aggiunte con l'array 'drawn'. Questo arraydi DECKSIZE elementi ci impedisce di duplicare un valore di carta. #include <stdlib.h> #include <stdio.h> #include <time.h> #include "deck.h" static time_t seed = 0; 6 Gcc e make void shuffle(deck_t *pdeck) { /* Tiene traccia di che numeri sono stati usati */ int drawn[DECKSIZE] = {0}; int i; /* Inizializzazione da fare una volta di rand */ if(0 == seed) { seed = time(NULL); srand(seed); } for(i = 0; i < DECKSIZE; i++) { int value = -1; do { value = rand() % DECKSIZE; } while(drawn[value] != 0); /* segna il valore come usato */ drawn[value] = 1; /* codice di debug */ printf("%i\n", value); pdeck->card[i] = value; } pdeck->dealt = 0; return; } Salvate questo file come shuffle.c. Abbiamo inserito del codice di debug in questo codice in modo che, quando viene lanciato, scriva i valori delle carte che genera. Questo non aggiunge niente alle funzionalità del programma, ma è cruciale adesso che non possiamo vedere cosa succede. Visto che stiamo appena cominciando il nostro gioco non abbiamo altro modo di sapere se la nostra funzione fa quello che vogliamo. Con il comando printf potete vedere esattamente cosa sta succedendo in modo che quando passeremo alla prossima fase sapremo che il mazzo viene mescolato bene. Dopo che saremmo soddisfatti del buon funzionamento