Come funziona il processo di compilazione / collegamento?


417

Come funziona il processo di compilazione e collegamento?

(Nota: questo dovrebbe essere una voce alle FAQ C ++ di Stack Overflow . Se vuoi criticare l'idea di fornire una FAQ in questo modulo, allora la pubblicazione su meta che ha iniziato tutto questo sarebbe il posto dove farlo. tale domanda viene monitorata nella chatroom di C ++ , dove l'idea FAQ è iniziata in primo luogo, quindi è molto probabile che la tua risposta venga letta da coloro che hanno avuto l'idea.)

Risposte:


555

La compilazione di un programma C ++ prevede tre passaggi:

  1. Preelaborazione: il preprocessore prende un file di codice sorgente C ++ e si occupa delle direttive #includes, se #definealtre preprocessori. L'output di questo passaggio è un file C ++ "puro" senza direttive pre-processore.

  2. Compilazione: il compilatore prende l'output del pre-processore e produce da esso un file oggetto.

  3. Collegamento: il linker prende i file oggetto prodotti dal compilatore e produce una libreria o un file eseguibile.

Pre-elaborazione

Il preprocessore gestisce le direttive del preprocessore , come #includee #define. È agnostico della sintassi del C ++, motivo per cui deve essere usato con cura.

Funziona su un file C ++ fonte alla volta sostituendo #includele direttive con il contenuto dei rispettivi file (che di solito è solo dichiarazioni), facendo la sostituzione di macro ( #define), e la selezione di diverse porzioni di testo a seconda di #if, #ifdefe #ifndefdirettive.

Il preprocessore funziona su un flusso di token di preelaborazione. La sostituzione macro è definita come la sostituzione di token con altri token (l'operatore ##consente di unire due token quando ha senso).

Dopotutto, il preprocessore produce un singolo output che è un flusso di token risultante dalle trasformazioni sopra descritte. Aggiunge inoltre alcuni marcatori speciali che indicano al compilatore da dove proviene ciascuna riga in modo che possa utilizzarli per produrre messaggi di errore sensibili.

Alcuni errori possono essere prodotti in questa fase con un uso intelligente delle direttive #ife #error.

Compilazione

La fase di compilazione viene eseguita su ciascun output del preprocessore. Il compilatore analizza il codice sorgente C ++ puro (ora senza direttive per il preprocessore) e lo converte in codice assembly. Quindi richiama il back-end sottostante (assemblatore in toolchain) che assembla quel codice nel codice macchina producendo il file binario effettivo in un formato (ELF, COFF, a.out, ...). Questo file oggetto contiene il codice compilato (in forma binaria) dei simboli definiti nell'input. I simboli nei file oggetto sono indicati per nome.

I file oggetto possono fare riferimento a simboli non definiti. Questo è il caso in cui usi una dichiarazione e non fornisci una definizione per essa. Al compilatore non importa, e produrrà felicemente il file oggetto fintanto che il codice sorgente è ben formato.

I compilatori di solito ti consentono di interrompere la compilazione a questo punto. Questo è molto utile perché con esso puoi compilare ogni file di codice sorgente separatamente. Il vantaggio offerto da questo è che non è necessario ricompilare tutto se si modifica solo un singolo file.

I file oggetto prodotti possono essere inseriti in archivi speciali chiamati librerie statiche, per riutilizzarli più facilmente in seguito.

È a questo punto che vengono segnalati errori "regolari" del compilatore, come errori di sintassi o errori di risoluzione del sovraccarico non riusciti.

Collegamento

Il linker è ciò che produce l'output della compilazione finale dai file oggetto generati dal compilatore. Questo output può essere una libreria condivisa (o dinamica) (e sebbene il nome sia simile, non hanno molto in comune con le librerie statiche menzionate in precedenza) o un eseguibile.

Collega tutti i file oggetto sostituendo i riferimenti a simboli indefiniti con gli indirizzi corretti. Ognuno di questi simboli può essere definito in altri file oggetto o in librerie. Se sono definiti in librerie diverse dalla libreria standard, è necessario informarne il linker.

In questa fase gli errori più comuni sono definizioni mancanti o definizioni duplicate. Il primo significa che le definizioni non esistono (cioè non sono scritte) o che i file oggetto o le librerie in cui risiedono non sono stati dati al linker. Quest'ultimo è ovvio: lo stesso simbolo è stato definito in due file o librerie di oggetti diversi.


39
La fase di compilazione chiama anche assemblatore prima di convertirlo in file oggetto.
manav mn

3
Dove vengono applicate le ottimizzazioni? A prima vista sembra che sarebbe stato fatto nella fase di compilazione, ma d'altra parte posso immaginare che l'ottimizzazione corretta possa essere fatta solo dopo il collegamento.
Bart van Heukelom,

6
@BartvanHeukelom è stato fatto tradizionalmente durante la compilazione, ma i compilatori moderni supportano la cosiddetta "ottimizzazione del tempo di collegamento" che ha il vantaggio di essere in grado di ottimizzare tra le unità di traduzione.
R. Martinho Fernandes,

3
C ha gli stessi passaggi?
Kevin Zhu,

6
Se il linker converte i simboli che fanno riferimento a classi / metodi nelle librerie in indirizzi, significa che i binari delle librerie sono memorizzati in indirizzi di memoria che il sistema operativo mantiene costanti? Sono solo confuso su come il linker conoscerebbe l'indirizzo esatto, per esempio, del binario stdio per tutti i sistemi di destinazione. Il percorso del file sarebbe sempre lo stesso, ma l'indirizzo esatto può cambiare, giusto?
Dan Carter,

42

Questo argomento è discusso su CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html

Ecco cosa ha scritto l'autore:

La compilazione non è la stessa della creazione di un file eseguibile! Invece, la creazione di un eseguibile è un processo a più fasi diviso in due componenti: compilazione e collegamento. In realtà, anche se un programma "compila bene" potrebbe non funzionare a causa di errori durante la fase di collegamento. Il processo totale di passaggio da file di codice sorgente a un eseguibile potrebbe essere meglio definito build.

Compilazione

La compilazione si riferisce all'elaborazione di file di codice sorgente (.c, .cc o .cpp) e alla creazione di un file 'oggetto'. Questo passaggio non crea nulla che l'utente possa effettivamente eseguire. Al contrario, il compilatore produce semplicemente le istruzioni del linguaggio macchina che corrispondono al file di codice sorgente che è stato compilato. Ad esempio, se compili (ma non colleghi) tre file separati, avrai tre file oggetto creati come output, ognuno con il nome .o o .obj (l'estensione dipenderà dal tuo compilatore). Ognuno di questi file contiene una traduzione del file del codice sorgente in un file in linguaggio macchina, ma non è ancora possibile eseguirli! È necessario trasformarli in file eseguibili utilizzabili dal sistema operativo. È qui che entra in gioco il linker.

Collegamento

Il collegamento si riferisce alla creazione di un singolo file eseguibile da più file oggetto. In questo passaggio, è comune che il linker si lamenti di funzioni indefinite (comunemente main stesso). Durante la compilazione, se il compilatore non è stato in grado di trovare la definizione per una particolare funzione, si presuppone che la funzione sia stata definita in un altro file. In caso contrario, non è possibile che il compilatore lo sappia: non esamina il contenuto di più di un file alla volta. Il linker, d'altra parte, può guardare più file e provare a trovare riferimenti per le funzioni che non sono state menzionate.

Potresti chiederti perché ci sono fasi di compilazione e collegamento separate. Innanzitutto, è probabilmente più facile implementare le cose in questo modo. Il compilatore fa la sua parte e il linker fa la sua cosa - mantenendo separate le funzioni, la complessità del programma viene ridotta. Un altro (più ovvio) vantaggio è che ciò consente la creazione di programmi di grandi dimensioni senza dover ripetere la fase di compilazione ogni volta che si modifica un file. Invece, usando la cosiddetta "compilazione condizionale", è necessario compilare solo i file di origine che sono stati modificati; per il resto, i file oggetto sono input sufficienti per il linker. Infine, ciò semplifica l'implementazione di librerie di codice precompilato: basta creare file oggetto e collegarli come qualsiasi altro file oggetto.

Per ottenere tutti i vantaggi della compilazione delle condizioni, è probabilmente più semplice ottenere un programma che ti aiuti piuttosto che provare a ricordare quali file sono stati modificati dall'ultima compilazione. (Naturalmente, potresti semplicemente ricompilare tutti i file che hanno un timestamp maggiore del timestamp del file oggetto corrispondente.) Se stai lavorando con un ambiente di sviluppo integrato (IDE), potresti già occupartene. Se stai usando gli strumenti da riga di comando, c'è un'utile utilità chiamata make che viene fornita con la maggior parte delle distribuzioni * nix. Insieme alla compilazione condizionale, ha molte altre belle funzioni per la programmazione, come consentire diverse compilazioni del programma, ad esempio se si dispone di una versione che produce output dettagliati per il debug.

Conoscere la differenza tra la fase di compilazione e la fase di collegamento può semplificare la ricerca di bug. Gli errori del compilatore sono generalmente di natura sintattica: un punto e virgola mancante, una parentesi aggiuntiva. Gli errori di collegamento hanno generalmente a che fare con definizioni mancanti o multiple. Se ricevi un errore che una funzione o una variabile viene definita più volte dal linker, questa è una buona indicazione che l'errore è che due dei tuoi file di codice sorgente hanno la stessa funzione o variabile.


1
Quello che non capisco è che se il preprocessore gestisce cose come #include per creare un super file, allora sicuramente non c'è nulla da collegare dopo?
binarysmacker,

@binarysmacer Vedi se ciò che ho scritto sotto ha senso per te. Ho provato a descrivere il problema dall'interno.
Vista ellittica il

3
@binarysmacker È troppo tardi per commentare questo, ma altri potrebbero trovarlo utile. youtu.be/D0TazQIkc8Q Fondamentalmente includi i file di intestazione e questi file di intestazione generalmente contengono solo le dichiarazioni di variabili / funzioni e non ci sono definizioni, le definizioni potrebbero essere presenti in un file di origine separato. Quindi il preprocessore include solo dichiarazioni e non definizioni, questo è dove linker aiuta. Collega il file sorgente che utilizza la variabile / funzione con il file sorgente che li definisce.
Karan Joisher,

24

Sul fronte standard:

  • un'unità di traduzione è la combinazione di un file di origine, intestazioni inclusi e file di origine al netto di eventuali righe di origine saltati dalla condizionale direttiva inclusione preprocessore.

  • lo standard definisce 9 fasi nella traduzione. I primi quattro corrispondono alla preelaborazione, i tre successivi sono la compilazione, il successivo è l'istanza di modelli (producendo unità di istanza ) e l'ultimo è il collegamento.

In pratica, l'ottava fase (la creazione di istanze di modelli) viene spesso eseguita durante il processo di compilazione, ma alcuni compilatori la ritardano alla fase di collegamento e alcuni la distribuiscono nei due.


14
Potresti elencare tutte e 9 le fasi? Penso che sarebbe una bella aggiunta alla risposta. :)
jalf


@jalf, aggiungi l'istanza del modello appena prima dell'ultima fase della risposta indicata da @sbi. IIRC ci sono sottili differenze nella formulazione precisa nella gestione di caratteri ampi, ma non credo che emergano nelle etichette dei diagrammi.
AProgrammer

2
@sbi sì, ma questa dovrebbe essere la domanda frequente, vero? Quindi queste informazioni non dovrebbero essere disponibili qui ? ;)
jalf

3
@AProgrammmer: semplicemente elencarli per nome sarebbe utile. Quindi le persone sanno cosa cercare se vogliono maggiori dettagli. Comunque, + ho dato la tua risposta in ogni caso :)
jalf

14

Il problema è che una CPU carica i dati dagli indirizzi di memoria, li archivia negli indirizzi di memoria ed esegue le istruzioni in sequenza dagli indirizzi di memoria, con alcuni salti condizionali nella sequenza delle istruzioni elaborate. Ognuna di queste tre categorie di istruzioni comporta il calcolo di un indirizzo a una cella di memoria da utilizzare nell'istruzione della macchina. Poiché le istruzioni della macchina hanno una lunghezza variabile a seconda della specifica istruzione in questione e poiché le stringiamo insieme in una lunghezza variabile mentre costruiamo il nostro codice macchina, esiste un processo in due fasi coinvolto nel calcolo e nella costruzione di qualsiasi indirizzo.

Innanzitutto, disponiamo l'allocazione della memoria nel miglior modo possibile, prima di sapere esattamente cosa succede in ogni cella. Scopriamo i byte, o le parole, o qualunque cosa formi le istruzioni, i letterali e tutti i dati. Iniziamo solo ad allocare memoria e costruendo i valori che creeranno il programma mentre procediamo, e annotiamo da qualche parte che dobbiamo tornare indietro e correggere un indirizzo. In quel luogo mettiamo un manichino per riempire la posizione in modo da poter continuare a calcolare la dimensione della memoria. Ad esempio, il nostro primo codice macchina potrebbe richiedere una cella. Il prossimo codice macchina potrebbe richiedere 3 celle, coinvolgendo una cella di codice macchina e due celle di indirizzo. Ora il nostro puntatore di indirizzo è 4. Sappiamo cosa succede nella cella della macchina, che è il codice operativo, ma dobbiamo aspettare per calcolare ciò che va nelle celle dell'indirizzo fino a quando non sappiamo dove si troveranno quei dati, cioè

Se esistesse solo un file sorgente, un compilatore potrebbe teoricamente produrre codice macchina completamente eseguibile senza un linker. In un processo a due passaggi è possibile calcolare tutti gli indirizzi effettivi su tutte le celle di dati a cui fa riferimento qualsiasi carico della macchina o istruzioni di archiviazione. E potrebbe calcolare tutti gli indirizzi assoluti a cui fa riferimento qualsiasi istruzione di salto assoluto. Ecco come funzionano i compilatori più semplici, come quello di Forth, senza linker.

Un linker è qualcosa che consente di compilare separatamente blocchi di codice. Ciò può accelerare il processo generale di costruzione del codice e consentire una certa flessibilità nel modo in cui i blocchi vengono successivamente utilizzati, in altre parole possono essere trasferiti in memoria, ad esempio aggiungendo 1000 a ogni indirizzo per raccogliere il blocco di 1000 celle di indirizzi.

Quindi ciò che l'output del compilatore è un codice macchina approssimativo che non è ancora completamente costruito, ma è strutturato in modo da conoscere la dimensione di tutto, in altre parole in modo da poter iniziare a calcolare dove saranno situati tutti gli indirizzi assoluti. il compilatore genera anche un elenco di simboli che sono coppie nome / indirizzo. I simboli si riferiscono a un offset di memoria nel codice macchina nel modulo con un nome. L'offset è la distanza assoluta dalla posizione di memoria del simbolo nel modulo.

Ecco dove arriviamo al linker. Il linker dapprima schiaffeggia tutti questi blocchi di codice macchina da un capo all'altro e annota dove inizia ciascuno di essi. Quindi calcola gli indirizzi da correggere sommando l'offset relativo all'interno di un modulo e la posizione assoluta del modulo nel layout più grande.

Ovviamente l'ho semplificato troppo in modo da poter provare ad afferrarlo e non ho deliberatamente usato il gergo di file oggetto, tabelle di simboli, ecc. Che per me fa parte della confusione.


13

GCC compila un programma C / C ++ in eseguibile in 4 passaggi.

Ad esempio, gcc -o hello hello.cviene eseguito come segue:

1. Pre-elaborazione

Preelaborazione tramite GNU C Preprocessor ( cpp.exe), che include le intestazioni ( #include) ed espande le macro ( #define).

cpp hello.c > hello.i

Il file intermedio risultante "ciao.i" contiene il codice sorgente espanso.

2. Compilazione

Il compilatore compila il codice sorgente pre-elaborato nel codice assembly per un processore specifico.

gcc -S hello.i

L'opzione -S specifica di produrre il codice assembly, anziché il codice oggetto. Il file di assieme risultante è "hello.s".

3. Assemblaggio

as.exeAssembler ( ) converte il codice assembly in codice macchina nel file oggetto "hello.o".

as -o hello.o hello.s

4. Linker

Infine, il linker ( ld.exe) collega il codice oggetto con il codice della libreria per produrre un file eseguibile "ciao".

    ld -o ciao ciao.o ... librerie ...

9

Guarda l'URL: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
Il processo completo di conformità di C ++ è chiaramente introdotto in questo URL.


2
Grazie per averlo condiviso, è così semplice e diretto da capire.
Segna il

Bene, risorsa, puoi mettere qui una spiegazione di base del processo, la risposta è contrassegnata dall'algoritmo come b / c di bassa qualità è breve e solo l'URL.
JasonB,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.