Ricerca…


introduzione

Il linguaggio C è tradizionalmente un linguaggio compilato (al contrario di interpretato). Lo standard C definisce le fasi di traduzione e il prodotto di applicarle è un'immagine di programma (o un programma compilato). In , le fasi sono elencate in §5.1.1.2.

Osservazioni

Estensione del nome file Descrizione
.c File sorgente. Di solito contiene definizioni e codice.
.h File di intestazione. Di solito contiene dichiarazioni.
.o File oggetto Codice compilato nel linguaggio macchina.
.obj Estensione alternativa per i file oggetto.
.a File di libreria Pacchetto di file oggetto.
.dll Libreria di collegamento dinamico su Windows.
.so Oggetto condiviso (libreria) su molti sistemi simili a Unix.
.dylib Libreria di collegamento dinamico su OSX (variante Unix).
.exe , .com File eseguibile di Windows. Creato collegando file di oggetti e file di libreria. Nei sistemi di tipo Unix, non esiste un'estensione di nome file speciale per il file eseguibile.
Bandiere del compilatore POSIX c99 Descrizione
-o filename Nome del file di output es. ( bin/program.exe , program )
-I directory cercare intestazioni in direrctory .
-D name definire il name macro
-L directory cerca le biblioteche nella directory .
-l name link library libname .

I compilatori su piattaforme POSIX (Linux, mainframe, Mac) di solito accettano queste opzioni, anche se non sono chiamate c99 .

Bandiere GCC (GNU Compiler Collection) Descrizione
-Wall Abilita tutti i messaggi di avviso comunemente accettati come utili.
-Wextra Abilita più messaggi di avviso, può essere troppo rumoroso.
-pedantic Forza avvisi in cui il codice viola lo standard scelto.
-Wconversion Abilita gli avvisi sulla conversione implicita, usa con cautela.
-c Compila i file sorgente senza collegamento.
-v Stampa informazioni di compilazione.
  • gcc accetta i flag POSIX più molti altri.
  • Molti altri compilatori su piattaforme POSIX ( clang , compilatori specifici del fornitore) utilizzano anche i flag elencati sopra.
  • Vedi anche Invocazione di GCC per molte altre opzioni.
Bandiere TCC (Tiny C Compiler) Descrizione
-Wimplicit-function-declaration Avvisa sulla dichiarazione di funzione implicita.
-Wunsupported Avvisa sulle funzionalità GCC non supportate ignorate da TCC.
-Wwrite-strings Rendi costanti stringa di tipo const char * invece di char *.
-Werror Compilazione di interruzione se vengono emessi avvisi.
-Wall Attiva tutti gli avvisi, ad eccezione di -Werror , -Wunusupported e -Wwrite strings .

Il linker

Il lavoro del linker consiste nel collegare insieme un gruppo di file oggetto (file .o ) in un eseguibile binario. Il processo di collegamento coinvolge principalmente la risoluzione di indirizzi simbolici in indirizzi numerici . Il risultato del processo di collegamento è normalmente un programma eseguibile.

Durante il processo di collegamento, il linker raccoglierà tutti i moduli oggetto specificati sulla riga di comando, aggiungerà un codice di avvio specifico del sistema e tenterà di risolvere tutti i riferimenti esterni nel modulo oggetto con definizioni esterne in altri file oggetto (file oggetto) può essere specificato direttamente sulla riga di comando o può essere aggiunto implicitamente tramite le librerie). Assegna quindi gli indirizzi di carico per i file oggetto, cioè specifica dove il codice e i dati finiranno nello spazio degli indirizzi del programma finito. Una volta che ha gli indirizzi di carico, può sostituire tutti gli indirizzi simbolici nel codice oggetto con indirizzi "reali" numerici nello spazio degli indirizzi del target. Il programma è pronto per essere eseguito ora.

Ciò include sia i file oggetto che il compilatore ha creato dai file del codice sorgente sia i file oggetto che sono stati precompilati per te e raccolti in file di libreria. Questi file hanno nomi che terminano in .a o .so , e normalmente non è necessario conoscerli, poiché il linker sa dove si trova la maggior parte di essi e li collegherà automaticamente in base alle necessità.

Invocazione implicita del linker

Come il pre-processore, il linker è un programma separato, spesso chiamato ld (ma Linux usa collect2 , ad esempio). Analogamente al pre-processore, il linker viene richiamato automaticamente quando si utilizza il compilatore. Pertanto, il modo normale di utilizzare il linker è il seguente:

% gcc foo.o bar.o baz.o -o myprog

Questa riga indica al compilatore di collegare insieme tre file oggetto ( foo.o , bar.o e baz.o ) in un file eseguibile binario chiamato myprog . Ora hai un file chiamato myprog che puoi eseguire e che si spera possa fare qualcosa di interessante e / o utile.

Invocazione esplicita del linker

È possibile richiamare direttamente il linker, ma questo è raramente consigliabile ed è tipicamente molto specifico della piattaforma. Ovvero, le opzioni che funzionano su Linux non funzioneranno necessariamente su Solaris, AIX, macOS, Windows e in modo simile per qualsiasi altra piattaforma. Se lavori con GCC, puoi usare gcc -v per vedere cosa viene eseguito per tuo conto.

Opzioni per il linker

Il linker accetta anche alcuni argomenti per modificarne il comportamento. Il seguente comando direbbe a gcc di collegare foo.o e bar.o , ma include anche la libreria ncurses .

% gcc foo.o bar.o -o foo -lncurses

Questo è in realtà (più o meno) equivalente a

% gcc foo.o bar.o /usr/lib/libncurses.so -o foo

(anche se libncurses.so potrebbe essere libncurses.a , che è solo un archivio creato con ar ). Si noti che è necessario elencare le librerie (tramite il nome del percorso o tramite -lname opzioni -lname ) dopo i file oggetto. Con le librerie statiche, l'ordine in cui vengono specificate è importante; spesso, con le librerie condivise, l'ordine non ha importanza.

Si noti che su molti sistemi, se si utilizzano funzioni matematiche (da <math.h> ), è necessario specificare -lm per caricare la libreria matematica, ma Mac OS X e macOS Sierra non richiedono questo. Ci sono altre librerie che sono librerie separate su Linux e altri sistemi Unix, ma non su macOS - thread POSIX e POSIX realtime, e le librerie di rete sono esempi. Di conseguenza, il processo di collegamento varia tra piattaforme.

Altre opzioni di compilazione

Questo è tutto ciò che devi sapere per iniziare a compilare i tuoi programmi C. In generale, ti consigliamo anche di utilizzare l' -Wall della -Wall comando -Wall :

% gcc -Wall -c foo.cc

L'opzione -Wall fa in modo che il compilatore ti avvisi su costrutti di codice legali ma dubbi e ti aiuterà a catturare molti bug molto presto.

Se vuoi che il compilatore lanci più avvertimenti su di te (comprese le variabili dichiarate ma non utilizzate, dimenticando di restituire un valore ecc.), Puoi usare questo insieme di opzioni, poiché -Wall , nonostante il nome, non gira tutti i possibili avvertimenti su:

% gcc -Wall -Wextra -Wfloat-equal -Wundef -Wcast-align -Wwrite-strings -Wlogical-op \
>     -Wmissing-declarations -Wredundant-decls -Wshadow …

Nota che clang ha un'opzione -Weverything che -Weverything realmente tutti gli avvertimenti in clang .

Tipi di file

Compilare i programmi C richiede di lavorare con cinque tipi di file:

  1. File di origine : questi file contengono definizioni di funzioni e hanno nomi che terminano in .c per convenzione. Nota: .cc e .cpp sono file C ++; non i file C.
    ad esempio, foo.c

  2. File di intestazione : questi file contengono prototipi di funzioni e varie istruzioni di pre-processore (vedi sotto). Vengono utilizzati per consentire ai file del codice sorgente di accedere a funzioni definite esternamente. I file di intestazione terminano in .h per convenzione.
    ad esempio, foo.h

  3. File oggetto : questi file sono prodotti come output del compilatore. Sono costituiti da definizioni di funzioni in formato binario, ma non sono eseguibili da soli. I file oggetto terminano in .o per convenzione, sebbene su alcuni sistemi operativi (es. Windows, MS-DOS), spesso finiscono in .obj .
    ad esempio, foo.o foo.obj

  4. Eseguibili binari : sono prodotti come output di un programma chiamato "linker". Il linker collega insieme un numero di file oggetto per produrre un file binario che può essere eseguito direttamente. Gli eseguibili binari non hanno suffisso speciale sui sistemi operativi Unix, sebbene generalmente finiscano in .exe su Windows.
    ad esempio, foo foo.exe

  5. Librerie : una libreria è un binario compilato ma non è di per sé un eseguibile (cioè, non esiste una funzione main() in una libreria). Una libreria contiene funzioni che possono essere utilizzate da più di un programma. Una libreria dovrebbe essere spedita con i file header che contengono prototipi per tutte le funzioni nella libreria; questi file di intestazione dovrebbero essere referenziati (es .: #include <library.h> ) in qualsiasi file sorgente che usi la libreria. Il linker deve quindi essere indirizzato alla libreria in modo che il programma possa essere compilato correttamente. Esistono due tipi di librerie: statiche e dinamiche.

    • Libreria statica : una libreria statica (file .a per sistemi POSIX e file .lib per Windows - da non confondere con i file di libreria di importazione DLL , che utilizzano anche l'estensione .lib ) è integrata staticamente nel programma. Le librerie statiche hanno il vantaggio che il programma sa esattamente quale versione di una libreria viene utilizzata. D'altra parte, le dimensioni dei file eseguibili sono maggiori in quanto sono incluse tutte le funzioni di libreria utilizzate.
      ad esempio, libfoo.a foo.lib
    • Libreria dinamica : una libreria dinamica (file .so per la maggior parte dei sistemi POSIX, .dylib per OSX e file .dll per Windows) è collegata dinamicamente in fase di esecuzione dal programma. A volte vengono anche chiamate librerie condivise perché una sola immagine di libreria può essere condivisa da molti programmi. Le librerie dinamiche hanno il vantaggio di occupare meno spazio su disco se più di un'applicazione utilizza la libreria. Inoltre, consentono aggiornamenti della libreria (correzioni di bug) senza dover ricostruire eseguibili.
      ad esempio, foo.so foo.dylib foo.dll

Il preprocessore

Prima che il compilatore C inizi a compilare un file di codice sorgente, il file viene elaborato in una fase di pre-elaborazione. Questa fase può essere eseguita da un programma separato o essere completamente integrata in un eseguibile. In ogni caso, viene richiamato automaticamente dal compilatore prima che inizi la compilazione corretta. La fase di pre-elaborazione converte il codice sorgente in un altro codice sorgente o unità di traduzione applicando sostituzioni testuali. Puoi considerarlo come un codice sorgente "modificato" o "espanso". Questa fonte espansa può esistere come file reale nel file system, oppure può essere memorizzata in memoria per un breve periodo prima di essere ulteriormente elaborata.

I comandi del preprocessore iniziano con il cancelletto ("#"). Esistono diversi comandi per il preprocessore; due dei più importanti sono:

  1. Definisce :

    #define è utilizzato principalmente per definire le costanti. Per esempio,

    #define BIGNUM 1000000
    int a = BIGNUM; 
    

    diventa

    int a = 1000000;
    

    #define viene utilizzato in questo modo in modo da evitare di dover scrivere esplicitamente un valore costante in molte posizioni diverse in un file di codice sorgente. Questo è importante nel caso in cui sia necessario modificare il valore costante in seguito; è molto meno incline a cambiarlo una volta, in #define , piuttosto che #define modificare in più punti sparsi su tutto il codice.

    Poiché #define esegue solo la ricerca avanzata e la sostituzione, puoi anche dichiarare macro. Per esempio:

    #define ISTRUE(stm) do{stm = stm ? 1 : 0;}while(0)
    // in the function:
    a = x;
    ISTRUE(a);
    

    diventa:

    // in the function:
    a = x;
    do {
        a = a ? 1 : 0;
    } while(0);
    

    In prima approssimazione, questo effetto è più o meno lo stesso delle funzioni inline, ma il preprocessore non fornisce il controllo di tipo per le macro #define . Questo è ben noto per essere soggetto a errori e il loro uso richiede molta cautela.

    Si noti inoltre che il preprocessore dovrebbe anche sostituire i commenti con uno spazio vuoto come spiegato di seguito.

  2. Include :

    #include viene utilizzato per accedere alle definizioni di funzioni definite al di fuori di un file di codice sorgente. Per esempio:

     #include <stdio.h> 
    

    causa al preprocessore di incollare il contenuto di <stdio.h> nel file di codice sorgente nel punto #include prima che venga compilato. #include è quasi sempre usato per includere i file header, che sono i file che contengono principalmente dichiarazioni di funzioni e dichiarazioni #define . In questo caso, usiamo #include per poter utilizzare funzioni come printf e scanf , le cui dichiarazioni si trovano nel file stdio.h . I compilatori C non ti permettono di usare una funzione a meno che non sia stata precedentemente dichiarata o definita in quel file; #include affermazioni sono quindi il modo di riutilizzare il codice scritto in precedenza nei vostri programmi C.

  3. Operazioni logiche :

    #if defined A || defined B
    variable = another_variable + 1;
    #else
    variable = another_variable * 2;
    #endif
    

    sarà cambiato in:

    variable = another_variable + 1;
    

    se A o B sono stati definiti da qualche parte nel progetto prima. Se questo non è il caso, ovviamente il preprocessore farà questo:

    variable = another_variable * 2;
    

    Questo è spesso usato per il codice, che funziona su diversi sistemi o compila su diversi compilatori. Dato che ci sono definizioni globali, che sono specifiche del compilatore / sistema, è possibile testare quelle che definiscono e lasciare sempre che il compilatore usi il codice che compilerà di sicuro.

  4. Commenti

    Il preprocessore sostituisce tutti i commenti nel file di origine per singoli spazi. I commenti sono indicati da // fino alla fine della riga, o una combinazione di parentesi di apertura /* e chiusura */ commento.

Il compilatore

Dopo che il pre-processore C ha incluso tutti i file header e espanso tutte le macro, il compilatore può compilare il programma. Lo fa girando il codice sorgente C in un file di codice oggetto, che è un file che termina in .o che contiene la versione binaria del codice sorgente. Il codice oggetto non è direttamente eseguibile, però. Per rendere un eseguibile, devi anche aggiungere il codice per tutte le funzioni della libreria che erano #include d nel file (non è lo stesso che includere le dichiarazioni, che è ciò che #include ). Questo è il lavoro del linker .

In generale, la sequenza esatta su come richiamare un compilatore C dipende molto dal sistema che si sta utilizzando. Qui stiamo usando il compilatore GCC, sebbene si noti che esistono molti altri compilatori:

% gcc -Wall -c foo.c

% è il prompt dei comandi del sistema operativo. Questo dice al compilatore di eseguire il pre-processore sul file foo.c e quindi di compilarlo nel file di codice oggetto foo.o L'opzione -c significa compilare il file del codice sorgente in un file oggetto ma non invocare il linker. Questa opzione -c è disponibile su sistemi POSIX, come Linux o macOS; altri sistemi possono utilizzare una sintassi diversa.

Se l'intero programma si trova in un file di codice sorgente, puoi farlo invece:

% gcc -Wall foo.c -o foo

Questo dice al compilatore di eseguire il pre-processore su foo.c , compilarlo e quindi collegarlo per creare un eseguibile chiamato foo . L'opzione -o indica che la parola successiva sulla riga è il nome del file eseguibile binario (programma). Se non si specifica -o , (se si digita semplicemente gcc foo.c ), l'eseguibile verrà denominato a.out per ragioni storiche.

In generale, il compilatore richiede quattro passaggi durante la conversione di un file .c in un eseguibile:

  1. pre-elaborazione - espande testualmente le direttive #include e #define nel tuo file .c
  2. compilation - converte il programma in assembly (puoi fermare il compilatore in questa fase aggiungendo l'opzione -S )
  3. assembly - converte l'assembly in codice macchina
  4. linkage : collega il codice oggetto alle librerie esterne per creare un eseguibile

Nota anche che il nome del compilatore che stiamo usando è GCC, che sta per "GNU C compiler" e "GNU compiler collection", a seconda del contesto. Esistono altri compilatori C. Per i sistemi operativi di tipo Unix, molti di essi hanno il nome cc , per "C compilatore", che è spesso un collegamento simbolico con qualche altro compilatore. Sui sistemi Linux, cc è spesso un alias per GCC. Su macOS o OS-X, punta al clang.

Gli standard POSIX attualmente c99 come nome di un compilatore C - per impostazione predefinita supporta lo standard C99. Versioni precedenti di POSIX con mandato c89 come compilatore. POSIX impone anche che questo compilatore comprenda le opzioni -c -o che abbiamo usato sopra.


Nota: l'opzione -Wall presente in entrambi gli esempi di gcc indica al compilatore di stampare avvisi su costruzioni discutibili, che è fortemente raccomandato. È anche una buona idea aggiungere altre opzioni di avviso , ad esempio -Wextra .

Le fasi di traduzione

A partire dallo Standard C 2011, elencato in §5.1.1.2 Fasi di traduzione , la traduzione del codice sorgente nell'immagine del programma (ad esempio, l'eseguibile) è elencata per essere eseguita in 8 passaggi ordinati.

  1. L'input del file sorgente viene mappato sul set di caratteri sorgente (se necessario). Trigrams sono sostituiti in questo passaggio.
  2. Le linee di continuazione (le righe che terminano con \ ) sono giuntate con la riga successiva.
  3. Il codice sorgente viene analizzato in spazi bianchi e token di preelaborazione.
  4. Viene applicato il preprocessore, che esegue le direttive, espande i macro e applica i pragma. Ogni file sorgente inserito da #include viene sottoposto alle fasi di conversione da 1 a 4 (se necessario ricorsivo). Tutte le direttive relative al preprocessore vengono quindi eliminate.
  5. I valori del set di caratteri di origine in costanti di caratteri e valori letterali stringa vengono associati al set di caratteri di esecuzione.
  6. I letterali delle stringhe adiacenti l'uno all'altro sono concatenati.
  7. Il codice sorgente viene analizzato in token, che comprendono l'unità di traduzione.
  8. I riferimenti esterni vengono risolti e viene creata l'immagine del programma.

Un'implementazione di un compilatore C può combinare più passaggi insieme, ma l'immagine risultante deve comunque comportarsi come se i passaggi precedenti si fossero verificati separatamente nell'ordine sopra elencato.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow