Panoramica della pipeline di compilazione estesa di GCC, inclusi programmi specializzati come preprocessore, assemblatore e linker.
GCC segue l’architettura a 3 stadi tipica dei compilatori multi-lingua e multi-CPU. Tutti gli alberi del programma vengono convertiti in una rappresentazione astratta comune al “middle end”, consentendo l’ottimizzazione del codice e le strutture di generazione del codice binario da condividere con tutte le lingue.,
L’interfaccia esterna di GCC segue le convenzioni Unix. Gli utenti invocano un programma di driver specifico per la lingua (gcc
per C, g++
per C++, ecc.), che interpreta gli argomenti del comando, chiama il compilatore effettivo, esegue l’assemblatore sull’output e quindi opzionalmente esegue il linker per produrre un binario eseguibile completo.
Ciascuno dei compilatori di linguaggio è un programma separato che legge il codice sorgente e emette il codice macchina. Tutti hanno una struttura interna comune., Un front – end per lingua analizza il codice sorgente in quella lingua e produce un albero di sintassi astratto (“albero” in breve).
Questi vengono, se necessario, convertiti nella rappresentazione di input dell’estremità centrale, chiamata forma generica; l’estremità centrale trasforma gradualmente il programma verso la sua forma finale. Le ottimizzazioni del compilatore e le tecniche di analisi del codice statico (come FORTIFY_SOURCE, una direttiva del compilatore che tenta di rilevare alcuni overflow del buffer) vengono applicate al codice., Questi funzionano su più rappresentazioni, principalmente la rappresentazione GIMPLE indipendente dall’architettura e la rappresentazione RTL dipendente dall’architettura. Infine, il codice macchina viene prodotto utilizzando pattern matching specifici dell’architettura originariamente basati su un algoritmo di Jack Davidson e Chris Fraser.
GCC è stato scritto principalmente in C ad eccezione di parti del front-end Ada. La distribuzione include le librerie standard per Ada, C++ e Java il cui codice è scritto principalmente in quelle lingue., Su alcune piattaforme, la distribuzione include anche una libreria di runtime di basso livello, libgcc, scritta in una combinazione di C indipendente dalla macchina e codice macchina specifico del processore, progettato principalmente per gestire operazioni aritmetiche che il processore di destinazione non può eseguire direttamente.
GCC utilizza molti strumenti standard nella sua build, tra cui Perl, Flex, Bison e altri strumenti comuni. Inoltre, attualmente richiede tre librerie aggiuntive per essere presenti al fine di costruire: GMP, MPC e MPFR.
Nel maggio 2010, il comitato direttivo del GCC ha deciso di consentire l’uso di un compilatore C++ per compilare GCC., Il compilatore doveva essere scritto principalmente in C più un sottoinsieme di funzionalità da C++. In particolare, questo è stato deciso in modo che gli sviluppatori di GCC potessero utilizzare le funzionalità distruttori e generici di C++.
Nell’agosto 2012, il comitato direttivo del GCC ha annunciato che GCC ora utilizza il C++ come linguaggio di implementazione. Ciò significa che per creare GCC da fonti, è necessario un compilatore C++ che comprenda lo standard ISO/IEC C++03.,
Front endsEdit
I front end consistono in pre-elaborazione, analisi lessicale, analisi sintattica (parsing) e analisi semantica. Gli obiettivi dei front-end del compilatore sono di accettare o rifiutare i programmi candidati in base alla grammatica e alla semantica del linguaggio, identificare gli errori e gestire rappresentazioni di programma valide nelle fasi successive del compilatore. Questo esempio mostra i passaggi lexer e parser eseguiti per un semplice programma scritto in C.,
Ogni front-end utilizza un parser per produrre l’albero della sintassi astratta di un dato file sorgente. A causa dell’astrazione dell’albero della sintassi, i file sorgente di una qualsiasi delle diverse lingue supportate possono essere elaborati dallo stesso back-end. GCC ha iniziato a utilizzare parser LALR generati con Bison, ma gradualmente è passato a parser a discesa ricorsiva scritti a mano per C++ nel 2004 e per C e Objective-C nel 2006. A partire dal 2021 tutti i front end utilizzano parser a discesa ricorsiva scritti a mano.
Fino a GCC 4.,0 la rappresentazione ad albero del programma non era completamente indipendente dal processore preso di mira. Il significato di un albero era un po ‘ diverso per i front-end linguistici diversi, e i front-end potevano fornire i propri codici albero. Questo è stato semplificato con l’introduzione di GENERIC e GIMPLE, due nuove forme di alberi indipendenti dal linguaggio che sono stati introdotti con l’avvento di GCC 4.0. GENERICO è più complesso, basato sul GCC 3.x Rappresentazione intermedia del front-end Java. GIMPLE è un GENERICO semplificato, in cui vari costrutti vengono ridotti a più istruzioni di GIMPLE., I front-end C, C++ e Java producono GENERICI direttamente nel front-end. Altri front end invece hanno diverse rappresentazioni intermedie dopo l’analisi e li convertono in GENERICI.
In entrambi i casi, il cosiddetto “gimplifier” converte quindi questa forma più complessa nella più semplice forma GIMPLE basata su SSA che è il linguaggio comune per un gran numero di potenti ottimizzazioni globali (function scope) indipendenti dal linguaggio e dall’architettura.,
GENERIC e GIMPLEEdit
GENERIC è un linguaggio di rappresentazione intermedio utilizzato come “middle end” durante la compilazione del codice sorgente in binari eseguibili. Un sottoinsieme, chiamato GIMPLE, è preso di mira da tutti i front-end di GCC.
Lo stadio intermedio di GCC esegue tutta l’analisi e l’ottimizzazione del codice, lavorando indipendentemente sia dal linguaggio compilato che dall’architettura di destinazione, partendo dalla rappresentazione GENERICA ed espandendola al register Transfer language (RTL)., La rappresentazione generica contiene solo il sottoinsieme dei costrutti di programmazione imperativi ottimizzati dall’estremità centrale.
Nel trasformare il codice sorgente in GIMPLE, le espressioni complesse vengono suddivise in un codice a tre indirizzi usando variabili temporanee. Questa rappresentazione è stata ispirata dalla SEMPLICE rappresentazione proposta nel compilatore McCAT da Laurie J. Hendren per semplificare l’analisi e l’ottimizzazione dei programmi imperativi.,
OptimizationEdit
L’ottimizzazione può verificarsi durante qualsiasi fase di compilazione; tuttavia, la maggior parte delle ottimizzazioni viene eseguita dopo l’analisi sintattica e semantica del front-end e prima della generazione del codice del back-end; quindi un nome comune, anche se alquanto contraddittorio, per questa parte del compilatore è “middle end.”
L’esatto insieme di ottimizzazioni GCC varia da rilascio a rilascio man mano che si sviluppa, ma include gli algoritmi standard, come l’ottimizzazione del ciclo, il threading del salto, l’eliminazione della sottoespressione comune, la pianificazione delle istruzioni e così via., Le ottimizzazioni RTL sono meno importanti con l’aggiunta di ottimizzazioni globali basate su SSA sugli alberi di GIMPLE, poiché le ottimizzazioni RTL hanno un ambito molto più limitato e hanno meno informazioni di alto livello.
Alcune di queste ottimizzazioni eseguite a questo livello includono l’eliminazione del codice morto, l’eliminazione della ridondanza parziale, la numerazione dei valori globali, la propagazione costante condizionale sparsa e la sostituzione scalare degli aggregati. Vengono eseguite anche ottimizzazioni basate sulla dipendenza dell’array come la vettorizzazione automatica e la parallelizzazione automatica. È possibile anche l’ottimizzazione guidata dal profilo.,
Back endEdit
Il back-end di GCC è in parte specificato da macro e funzioni del preprocessore specifiche di un’architettura di destinazione, ad esempio per definire la sua endianità, la dimensione delle parole e le convenzioni di chiamata., La parte anteriore del back-end li usa per aiutare a decidere la generazione RTL, quindi sebbene RTL di GCC sia nominalmente indipendente dal processore, la sequenza iniziale di istruzioni astratte è già adattata al target. In qualsiasi momento, le istruzioni RTL effettive che formano la rappresentazione del programma devono essere conformi alla descrizione della macchina dell’architettura di destinazione.
Il file di descrizione della macchina contiene pattern RTL, insieme a vincoli di operando e frammenti di codice per l’output dell’assembly finale., I vincoli indicano che un particolare pattern RTL potrebbe applicarsi solo (ad esempio) a determinati registri hardware o (ad esempio) consentire offset di operando immediati di dimensioni limitate (ad esempio 12, 16, 24,… offset di bit, ecc.). Durante la generazione di RTL, vengono controllati i vincoli per l’architettura di destinazione specificata. Per emettere un determinato frammento di RTL, deve corrispondere a uno (o più) dei pattern RTL nel file di descrizione della macchina e soddisfare i vincoli per quel pattern; altrimenti, sarebbe impossibile convertire l’RTL finale in codice macchina.,
Verso la fine della compilazione, RTL valido viene ridotto a una forma rigorosa in cui ogni istruzione si riferisce a registri macchina reali e un modello dal file di descrizione della macchina del target. Formare RTL rigoroso è un compito complicato; un passo importante è l’allocazione del registro, in cui vengono scelti registri hardware reali per sostituire gli pseudo-registri inizialmente assegnati. Questo è seguito da una fase di” ricarica”; qualsiasi pseudo-registro a cui non è stato assegnato un vero registro hardware viene “versato” nello stack e RTL per eseguire questo versamento viene generato., Allo stesso modo, gli offset troppo grandi per essere inseriti in un’istruzione effettiva devono essere suddivisi e sostituiti da sequenze RTL che obbediranno ai vincoli di offset.
Nella fase finale, il codice macchina viene creato chiamando un piccolo frammento di codice, associato a ciascun modello, per generare le istruzioni reali dal set di istruzioni del target, utilizzando i registri finali, gli offset e gli indirizzi scelti durante la fase di ricarica. Lo snippet di generazione dell’assembly può essere solo una stringa, nel qual caso viene eseguita una semplice sostituzione di stringa dei registri, degli offset e/o degli indirizzi nella stringa., Lo snippet di generazione dell’assembly può anche essere un breve blocco di codice C, che esegue alcuni lavori aggiuntivi, ma alla fine restituisce una stringa contenente il codice assembly valido.
Altre featuresEdit
Alcune caratteristiche di GCC includono:
- Link-time optimization ottimizza attraverso i confini dei file oggetto per migliorare direttamente il binario collegato. L’ottimizzazione del tempo di collegamento si basa su un file intermedio contenente la serializzazione di alcune rappresentazioni di Gimple incluse nel file oggetto. Il file viene generato insieme al file oggetto durante la compilazione dell’origine., Ogni compilazione sorgente genera un file oggetto separato e un file helper link-time. Quando i file oggetto sono collegati, il compilatore viene eseguito di nuovo e utilizza i file di supporto per ottimizzare il codice tra i file oggetto compilati separatamente.
- I plugin possono estendere direttamente il compilatore GCC. I plugin consentono a un compilatore stock di essere adattato alle esigenze specifiche dal codice esterno caricato come plugin. Ad esempio, i plugin possono aggiungere, sostituire o persino rimuovere passaggi di fascia media che operano sulle rappresentazioni di Gimple., Diversi plugin GCC sono già stati pubblicati, in particolare il plugin GCC Python, che si collega a libpython e consente di richiamare script Python arbitrari dall’interno del compilatore. L’obiettivo è consentire ai plugin GCC di essere scritti in Python. Il plugin MELT fornisce un linguaggio Lisp di alto livello per estendere GCC.
- C++ memoria transazionale durante la compilazione con-fgnu-tm.
- A partire da GCC 10, gli identificatori consentono la codifica UTF-8 (Unicode), cioè il codice sorgente C utilizza la codifica UTF-8 per impostazione predefinita.