Suche…


Einführung

Die C-Sprache ist traditionell eine kompilierte Sprache (im Gegensatz zu interpretiert). Der C-Standard definiert Übersetzungsphasen , und das Produkt ihrer Anwendung ist ein Programmbild (oder ein kompiliertes Programm). In sind die Phasen in §5.1.1.2 aufgeführt.

Bemerkungen

Dateinamenerweiterung Beschreibung
.c Quelldatei Enthält normalerweise Definitionen und Code.
.h Header-Datei. Enthält normalerweise Deklarationen.
.o Objektdatei Kompilierter Code in Maschinensprache.
.obj Alternative Erweiterung für Objektdateien.
.a Bibliotheksdatei. Paket von Objektdateien.
.dll Dynamic Link Library unter Windows.
.so Shared Object (Bibliothek) auf vielen Unix-ähnlichen Systemen.
.dylib Dynamic-Link Library unter OSX (Unix-Variante).
.exe , .com Ausführbare Windows-Datei. Wird durch Verknüpfen von Objektdateien und Bibliotheksdateien gebildet. In Unix-ähnlichen Systemen gibt es keine spezielle Dateinamenerweiterung für ausführbare Dateien.
POSIX c99-Compiler-Flags Beschreibung
-o filename Name der Ausgabedatei, z. ( bin/program.exe , program )
-I directory Suche nach Headern im direrctory .
-D name definieren name
-L directory Suche nach Bibliotheken im directory .
-l name libname .

Compiler auf POSIX-Plattformen (Linux, Mainframes, Mac) akzeptieren diese Optionen normalerweise, auch wenn sie nicht als c99 .

GCC-Flags (GNU Compiler Collection) Beschreibung
-Wall Aktiviert alle Warnmeldungen, die allgemein akzeptiert werden, als nützlich.
-Wextra Aktiviert weitere Warnmeldungen, kann zu laut sein.
-pedantic Erzwingen Sie Warnungen, wenn der Code den gewählten Standard verletzt.
-Wconversion Aktivieren Sie Warnungen bei der impliziten Konvertierung.
-c Kompiliert Quelldateien ohne Verknüpfung.
-v Gibt Informationen zur Zusammenstellung aus.
  • gcc akzeptiert die POSIX-Flags und viele andere.
  • Viele andere Compiler auf POSIX-Plattformen ( clang , herstellerspezifische Compiler) verwenden ebenfalls die oben aufgeführten Flags.
  • Siehe auch GCC aufrufen für viele weitere Optionen.
TCC (Tiny C Compiler) -Flaggen Beschreibung
-Wimplicit-function-declaration Warnung vor impliziter Funktionsdeklaration
-Wunsupported Warnung vor nicht unterstützten GCC-Funktionen, die von TCC ignoriert werden.
-Wwrite-strings Machen Sie String-Konstanten vom Typ const char * anstelle von char *.
-Werror Kompilierung abbrechen, wenn Warnungen ausgegeben werden.
-Wall Aktivieren Sie alle Warnungen mit Ausnahme der -Werror , " -Wunusupported und " -Wwrite strings .

Der Linker

Die Aufgabe des Linkers besteht darin, eine Reihe von Objektdateien ( .o Dateien) zu einer binären ausführbaren Datei zu verknüpfen. Bei der Verknüpfung werden hauptsächlich symbolische Adressen in numerische Adressen aufgelöst . Das Ergebnis des Verknüpfungsprozesses ist normalerweise ein ausführbares Programm.

Während des Verbindungsvorgangs nimmt der Linker alle auf der Befehlszeile angegebenen Objektmodule auf, fügt systemspezifischen Startcode hinzu und versucht, alle externen Verweise im Objektmodul mit externen Definitionen in anderen Objektdateien (Objektdateien) aufzulösen kann direkt in der Befehlszeile angegeben werden oder implizit durch Bibliotheken hinzugefügt werden). Es wird dann Last - Adressen für die Objektdateien zugeordnet werden , das heißt, es gibt an, wo der Code und die Daten werden in dem Adressraum des fertigen Programms enden. Sobald sie die Ladeadressen erhalten hat, kann sie alle symbolischen Adressen im Objektcode durch "echte" numerische Adressen im Adressraum des Ziels ersetzen. Das Programm kann jetzt ausgeführt werden.

Dies umfasst sowohl die Objektdateien, die der Compiler aus Ihren Quellcodedateien erstellt hat, als auch Objektdateien, die für Sie vorkompiliert und in Bibliotheksdateien gesammelt wurden. Diese Dateien haben Namen, die auf .a oder .so .a .so müssen Sie sie nicht kennen, da der Linker weiß, wo sich die meisten befinden, und er wird sie bei Bedarf automatisch einbinden.

Implizites Aufrufen des Linkers

Wie der Vorprozessor ist der Linker ein separates Programm, das häufig als ld (Linux verwendet collect2 zum Beispiel collect2 ). Ebenso wie der Vorprozessor wird der Linker automatisch aufgerufen, wenn Sie den Compiler verwenden. Die normale Art, den Linker zu verwenden, lautet daher wie folgt:

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

Diese Zeile weist den Compiler an, drei Objektdateien ( foo.o , bar.o und baz.o ) zu einer binären ausführbaren Datei namens myprog zu myprog . Jetzt haben Sie eine Datei namens myprog , die Sie ausführen können und die hoffentlich etwas myprog und / oder Nützliches tun wird.

Expliziter Aufruf des Linkers

Es ist möglich, den Linker direkt aufzurufen, dies ist jedoch selten ratsam und in der Regel sehr plattformspezifisch. Das heißt, Optionen, die unter Linux funktionieren, funktionieren unter Solaris, AIX, MacOS, Windows und anderen Plattformen nicht unbedingt. Wenn Sie mit GCC arbeiten, können Sie mit gcc -v sehen, was in Ihrem Namen ausgeführt wird.

Optionen für den Linker

Der Linker benötigt auch einige Argumente, um sein Verhalten zu ändern. Der folgende Befehl würde gcc bar.o , foo.o und bar.o zu verknüpfen, aber auch die Bibliothek ncurses .

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

Dies ist eigentlich (mehr oder weniger) äquivalent zu

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

(obwohl libncurses.so sein libncurses.a , was nur ein Archiv ist, das mit ar ). Beachten Sie, dass Sie die Bibliotheken (entweder über Pfadnamen oder über -lname Optionen -lname ) nach den Objektdateien -lname . Bei statischen Bibliotheken spielt die Reihenfolge, in der sie angegeben werden, eine Rolle. Bei gemeinsam genutzten Bibliotheken spielt die Reihenfolge oft keine Rolle.

Beachten Sie, dass auf vielen Systemen, wenn Sie mathematische Funktionen verwenden (von <math.h> ), müssen Sie angeben , -lm die Mathematik - Bibliothek zu laden - aber Mac OS X und Mac OS Sierra dies nicht erforderlich ist . Es gibt andere Bibliotheken, die separate Bibliotheken auf Linux und anderen Unix-Systemen sind, jedoch nicht auf macOS - POSIX-Threads und POSIX-Echtzeit und Netzwerkbibliotheken. Folglich variiert der Verknüpfungsprozess zwischen den Plattformen.

Andere Zusammenstellungsoptionen

Dies ist alles, was Sie wissen müssen, um Ihre eigenen C-Programme zu erstellen. Im Allgemeinen empfehlen wir auch die Verwendung der -Wall :

% gcc -Wall -c foo.cc

Die Option -Wall bewirkt, dass der Compiler Sie vor legalen, aber zweifelhaften Code-Konstrukten warnt, und hilft Ihnen, sehr früh Fehler zu finden.

Wenn Sie möchten, dass der Compiler weitere Warnungen ausgibt (einschließlich deklarierter, aber nicht verwendeter Variablen, vergessen Sie die Rückgabe eines Werts usw.), können Sie diese Optionen verwenden, da sich -Wall trotz des Namens nicht dreht alle möglichen Warnungen zu:

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

Beachten Sie, dass clang eine Option hat - -Weverything was wirklich alle Warnungen in clang -Weverything .

Datentypen

Zum Kompilieren von C-Programmen müssen Sie mit fünf Arten von Dateien arbeiten:

  1. Quelldateien : Diese Dateien enthalten Funktionsdefinitionen und haben Namen, die nach Konvention auf .c enden. Hinweis: .cc und .cpp sind C ++ - Dateien. keine C-Dateien.
    zB foo.c

  2. Header-Dateien : Diese Dateien enthalten Funktionsprototypen und verschiedene Präprozessoranweisungen (siehe unten). Sie werden verwendet, um Quellcodedateien den Zugriff auf extern definierte Funktionen zu ermöglichen. Header-Dateien enden nach Konvention in .h .
    zB foo.h

  3. Objektdateien : Diese Dateien werden als Ausgabe des Compilers erzeugt. Sie bestehen aus Funktionsdefinitionen in binärer Form, sind jedoch nicht selbst ausführbar. Objektdateien enden nach der Konvention .o , obwohl sie bei einigen Betriebssystemen (z. B. Windows, MS-DOS) häufig auf .obj .
    zB foo.o foo.obj

  4. Binäre ausführbare Dateien : Diese werden als Ausgabe eines Programms erzeugt, das als "Linker" bezeichnet wird. Der Linker verknüpft mehrere Objektdateien miteinander, um eine Binärdatei zu erzeugen, die direkt ausgeführt werden kann. Binäre ausführbare Dateien haben unter Unix-Betriebssystemen kein spezielles Suffix, obwohl sie unter Windows .exe auf .exe enden.
    zB foo foo.exe

  5. Bibliotheken : Eine Bibliothek ist eine kompilierte Binärdatei, ist jedoch keine ausführbare Datei (dh es gibt keine main() Funktion in einer Bibliothek). Eine Bibliothek enthält Funktionen, die von mehr als einem Programm verwendet werden können. Eine Bibliothek sollte mit Header-Dateien geliefert werden, die Prototypen für alle Funktionen in der Bibliothek enthalten. Diese Header-Dateien sollten in jeder Quelldatei, die die Bibliothek verwendet, referenziert werden (z. B. #include <library.h> ). Der Linker muss dann auf die Bibliothek verwiesen werden, damit das Programm erfolgreich kompiliert werden kann. Es gibt zwei Arten von Bibliotheken: statisch und dynamisch.

    • Statische Bibliothek : Eine statische Bibliothek ( .a Dateien für POSIX-Systeme und .lib Dateien für Windows - nicht zu verwechseln mit DLL-Import-Bibliotheksdateien , die ebenfalls die Erweiterung .lib ) ist statisch in das Programm integriert. Statische Bibliotheken haben den Vorteil, dass das Programm genau weiß, welche Version einer Bibliothek verwendet wird. Andererseits sind die ausführbaren Dateien größer, da alle verwendeten Bibliotheksfunktionen enthalten sind.
      zB libfoo.a foo.lib
    • Dynamische Bibliothek : Eine dynamische Bibliothek ( .so Dateien für die meisten POSIX-Systeme, .dylib für .dylib und .dll Dateien für Windows) wird vom Programm zur Laufzeit dynamisch verknüpft. Diese werden manchmal auch als gemeinsam genutzte Bibliotheken bezeichnet, da ein Bibliotheksbild von vielen Programmen gemeinsam genutzt werden kann. Dynamische Bibliotheken haben den Vorteil, dass weniger Speicherplatz beansprucht wird, wenn die Bibliothek von mehr als einer Anwendung verwendet wird. Sie ermöglichen auch Bibliotheksaktualisierungen (Fehlerbehebungen), ohne dass ausführbare Dateien neu erstellt werden müssen.
      zB foo.so foo.dylib foo.dll

Der Präprozessor

Bevor der C-Compiler mit dem Kompilieren einer Quellcodedatei beginnt, wird die Datei in einer Vorverarbeitungsphase verarbeitet. Diese Phase kann durch ein separates Programm erfolgen oder vollständig in eine ausführbare Datei integriert werden. In jedem Fall wird es automatisch vom Compiler aufgerufen, bevor die eigentliche Kompilierung beginnt. Die Vorverarbeitungsphase konvertiert Ihren Quellcode in einen anderen Quellcode oder eine andere Übersetzungseinheit, indem Sie Text ersetzen. Sie können es sich als "modifizierten" oder "erweiterten" Quellcode vorstellen. Diese erweiterte Quelle kann als echte Datei im Dateisystem vorhanden sein oder nur für kurze Zeit im Speicher gespeichert werden, bevor sie weiter verarbeitet wird.

Präprozessor-Befehle beginnen mit dem Nummernzeichen ("#"). Es gibt mehrere Präprozessor-Befehle. zwei der wichtigsten sind:

  1. Definiert :

    #define wird hauptsächlich zur Definition von Konstanten verwendet. Zum Beispiel,

    #define BIGNUM 1000000
    int a = BIGNUM; 
    

    wird

    int a = 1000000;
    

    #define wird auf diese Weise verwendet, um nicht explizit einen konstanten Wert an vielen verschiedenen Stellen in einer Quellcodedatei schreiben zu müssen. Dies ist wichtig, wenn Sie den konstanten Wert später ändern müssen. Es ist viel weniger fehleranfällig, es einmal in #define zu ändern, als es an mehreren Stellen im gesamten Code ändern zu müssen.

    Da #define nur erweiterte Such- und Ersetzungsvorgänge durchführt, können Sie auch Makros deklarieren. Zum Beispiel:

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

    wird:

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

    Auf den ersten Näherungswert ist dieser Effekt ungefähr derselbe wie bei Inline-Funktionen, aber der Präprozessor bietet keine Typüberprüfung für #define Makros. Dies ist bekanntermaßen fehleranfällig, und ihre Verwendung erfordert große Vorsicht.

    Beachten Sie auch hier, dass der Präprozessor auch Kommentare durch Leerzeichen ersetzt, wie unten erläutert.

  2. Beinhaltet :

    #include wird verwendet, um auf Funktionsdefinitionen zuzugreifen, die außerhalb einer Quellcodedatei definiert sind. Zum Beispiel:

     #include <stdio.h> 
    

    bewirkt, dass der Präprozessor den Inhalt von <stdio.h> in die Quellcodedatei an der Position der #include Anweisung #include , bevor er kompiliert wird. #include wird fast immer verwendet, um Header-Dateien #define . #define sind Dateien, die hauptsächlich Funktionsdeklarationen und #include #define Anweisungen enthalten. In diesem Fall verwenden wir #include , um Funktionen wie printf und scanf , deren Deklarationen sich in der Datei stdio.h . C-Compiler erlauben keine Verwendung einer Funktion, es sei denn, sie wurde zuvor in dieser Datei deklariert oder definiert. #include Anweisungen sind daher die Möglichkeit, zuvor geschriebenen Code in Ihren C-Programmen wiederzuverwenden.

  3. Logikoperationen :

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

    wird geändert in:

    variable = another_variable + 1;
    

    wenn A oder B zuvor im Projekt definiert wurden. Wenn dies nicht der Fall ist, führt der Präprozessor dies natürlich aus:

    variable = another_variable * 2;
    

    Dies wird häufig für Code verwendet, der auf verschiedenen Systemen läuft oder auf verschiedenen Compilern kompiliert wird. Da es globale Definitionen gibt, die compiler- / systemspezifisch sind, können Sie diese Definitionen testen und den Compiler immer nur den Code verwenden lassen, den er sicher kompiliert.

  4. Bemerkungen

    Der Präprozessor ersetzt alle Kommentare in der Quelldatei durch einzelne Leerzeichen. Kommentare werden durch // bis zum Ende der Zeile oder durch eine Kombination aus öffnendem /* und schließendem */ -Kommentar angegeben.

Der Compiler

Nachdem der C-Vorprozessor alle Header-Dateien enthalten und alle Makros erweitert hat, kann der Compiler das Programm kompilieren. Dazu wird der C-Quellcode in eine Objektcodedatei umgewandelt, die auf .o endet und die binäre Version des Quellcodes enthält. Der Objektcode ist jedoch nicht direkt ausführbar. Um eine ausführbare Datei zu machen, müssen Sie auch Code hinzufügen , für alle Bibliotheksfunktionen , die waren #include d in die Datei (dies ist nicht das gleiche wie die Erklärungen , einschließlich, das ist , was #include tut). Dies ist die Aufgabe des Linkers .

Im Allgemeinen hängt die genaue Reihenfolge, in der ein C-Compiler aufgerufen wird, stark von dem System ab, das Sie verwenden. Hier verwenden wir den GCC-Compiler, wobei zu beachten ist, dass viele weitere Compiler existieren:

% gcc -Wall -c foo.c

% ist die Eingabeaufforderung des Betriebssystems. Dies weist den Compiler an, den Vorprozessor für die Datei foo.c und ihn dann in die Objektcodedatei foo.o . Die Option -c bedeutet, die Quellcodedatei in eine Objektdatei zu kompilieren, den Linker jedoch nicht aufzurufen. Diese Option -c ist auf POSIX-Systemen wie Linux oder macOS verfügbar. andere Systeme verwenden möglicherweise eine andere Syntax.

Wenn sich Ihr gesamtes Programm in einer Quellcode-Datei befindet, können Sie stattdessen Folgendes tun:

% gcc -Wall foo.c -o foo

Dadurch wird der Compiler foo.c , den Vorprozessor auf foo.c , zu kompilieren und dann zu verknüpfen, um eine ausführbare Datei namens foo zu erstellen. Die Option -o gibt an, dass das nächste Wort in der Zeile der Name der ausführbaren binären Datei (Programm) ist. Wenn Sie nicht -o angeben (wenn Sie nur gcc foo.c ), wird die ausführbare Datei aus historischen Gründen als a.out bezeichnet.

Im Allgemeinen führt der Compiler beim Konvertieren einer .c Datei in eine ausführbare Datei vier Schritte aus:

  1. Vorverarbeitung - erweitert textlich die Anweisungen #include und #include #define Makros in Ihrer .c Datei
  2. Kompilierung - wandelt das Programm in eine Assembly um (Sie können den Compiler bei diesem Schritt durch Hinzufügen der Option -S stoppen)
  3. Assembly - wandelt die Assembly in Maschinencode um
  4. Verknüpfung - Verknüpft den Objektcode mit externen Bibliotheken, um eine ausführbare Datei zu erstellen

Beachten Sie auch, dass der Name des Compilers, den wir verwenden, GCC ist, der je nach Kontext sowohl für "GNU C-Compiler" als auch für "GNU-Compiler-Collection" steht. Andere C-Compiler existieren. Bei Unix-ähnlichen Betriebssystemen haben viele von ihnen den Namen cc für "C-Compiler", der häufig eine symbolische Verbindung zu einem anderen Compiler darstellt. Auf Linux-Systemen ist cc häufig ein Alias ​​für GCC. Unter Mac OS oder OS-X deutet es auf Lärm.

Der POSIX-Standard c99 derzeit c99 als Namen eines C-Compilers - er unterstützt standardmäßig den C99-Standard. In früheren Versionen von POSIX war c89 als Compiler vorgeschrieben. POSIX verlangt auch, dass dieser Compiler die oben genannten Optionen -c und -o versteht.


Hinweis: Die Option -Wall in beiden gcc Beispielen weist den Compiler an, Warnungen zu fragwürdigen Konstruktionen zu drucken. -Wall wird dringend empfohlen. Es ist eine gute Idee , auch andere hinzufügen Warnoptionen , zB -Wextra .

Die Übersetzungsphasen

Seit dem C 2011-Standard, aufgeführt in §5.1.1.2 Übersetzungsphasen , wird die Übersetzung des Quellcodes in ein Programmabbild (z. B. die ausführbare Datei) so aufgelistet, dass sie in 8 geordneten Schritten erfolgt.

  1. Die Eingabe der Quelldatei wird (falls erforderlich) dem Quellzeichensatz zugeordnet. Trigraphs werden in diesem Schritt ersetzt.
  2. Fortsetzungszeilen (Zeilen, die mit \ enden) werden mit der nächsten Zeile verbunden.
  3. Der Quellcode wird in Whitespace- und Vorverarbeitungstoken geparst.
  4. Der Präprozessor wird angewendet, der Anweisungen ausführt, Makros erweitert und Pragmas anwendet. Jede durch #include eingezogene Quelldatei durchläuft die Übersetzungsphasen 1 bis 4 (wenn nötig rekursiv). Alle Anweisungen für den Präprozessor werden dann gelöscht.
  5. Quellzeichensatzwerte in Zeichenkonstanten und String-Literalen werden dem Ausführungszeichensatz zugeordnet.
  6. Nebeneinander liegende String-Literale werden verkettet.
  7. Der Quellcode wird in Token analysiert, aus denen die Übersetzungseinheit besteht.
  8. Externe Referenzen werden aufgelöst und das Programmbild wird gebildet.

Eine Implementierung eines C-Compilers kann mehrere Schritte kombinieren, das resultierende Image muss sich jedoch so verhalten, als ob die oben genannten Schritte in der oben aufgelisteten Reihenfolge separat aufgetreten wären.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow