Compiler, Linker

Die Anweisungen, die in den Sprachen C oder C++ geschrieben werden, können von dem Prozessor eines Computers nicht direkt verstanden werden. Bevor die Anweisungen ausgeführt werden können, muss der gesamte Code (der sogenannte Sourcecode) in ein lauffähiges Programm (das sogenannte Binary) übersetzt werden. Die Sprachen C und C++ verwenden hierfür einen Compiler, welcher alle Source-Files einzeln übersetzt, welche daraufhin von einem Linker zu einem vollständigen, lauffähigen Programm zusammengesetzt werden, welches direkt auf dem Computer abgearbeitet werden kann.

Details

Ein Prozessor versteht nur eine Sprache, die sogenannte Maschinensprache, welche für jeden Prozessor unterschiedlich sein kann. Die Programmiersprache Assembler liefert für jede Maschinensprache (welche grundsätzlich für den Menschen unverständlich ist) eine mehr oder weniger lesbare Umsetzung der einzelnen Befehle, doch für jeden Prozessortyp existiert somit ein eigener Assembler.

Da nicht für jeden neuen Prozessortyp ein Programm umgeschrieben werden sollte, wurden sogenannte Hochsprachen entwickelt, welche über allen Assemblern stehen und den Programmcode auf verschiedene Art und Weise automatisch in den gewünschten Assembler und schlussendlich in die Maschinensprache umwandeln. Es wird grob zwischen zwei Hochsprachentypen unterschieden: Interpreter und Compiler. Ein in den letzten Jahren zusätzlich in Mode gekommener Typ ist die virtuelle Maschine, welche jedoch genau genommen nur eine Kombination der beiden erstgenannten ist. Alle drei Typen sind heute in verschiedenen Sprachen in Gebrauch.

Bei einer Interpreter-Sprache werden die einzelnen Anweisungen eine nach der anderen während der Laufzeit interpretiert und direkt umgewandelt. Der Vorteil hiervon ist, dass das Programm direkt gestartet werden kann, ohne dass der original-Programmtext vom Anwender in irgendeiner Weise verändert oder umgewandelt werden muss. Der Nachteil jedoch ist eine verminderte Geschwindigkeit sowie das wiederholte Übersetzen des Programmcodes bei jedem Start des Programmes. Wichtige Beispiele für Interpretersprachen sind BASIC, Perl, PHP, Javascript sowie grundsätzlich sämtliche Skriptsprachen.

Eine Compiler-Sprache übersetzt den kompletten Programmcode ein Mal und kann danach beliebig oft ohne erneute Compilierung gestartet werden. Der Vorteil hiervon ist, dass das Programm sehr schnell ist und zudem eigenständig arbeitet. Der Nachteil ist jedoch, dass die Compilierung viel Zeit kostet und das schlussendliche Resultat ohne neu-Compilierung grundsätzlich nur auf dem Prozessortyp läuft, für den compiliert wurde. Wichtige Beispiele sind C, C++, Objective-C und Pascal.

Eine virtuelle Maschine schlussendlich ist eine Mischform der obengenannten Typen. Eine solche Maschine bezeichnet einen künstlichen Prozessor, der nicht als Hardware vorhanden, sondern nur softwaremässig ausprogrammiert ist. Der Vorteil hierbei ist, dass dieser künstliche Prozessor auf jedem Computer ausprogrammiert werden kann und somit sämtliche Programme dieser virtuellen Maschine auf jedem beliebigen Computer direkt abgearbeitet werden können. Die Programmcodes werden hierbei mittels eines Compilers in den sogenannten Bytecode umgewandelt, welcher das direkte Pendant zum Maschinencode eines Prozessors ist. Bei der Abarbeitung des Programmes schlussendlich läuft der virtuelle Prozessor im Hintergrund und interpretiert diesen Bytecode Stück für Stück und wandelt ihn in die eigentliche Maschinensprache um. Wichtige Beispiele für virtuelle Maschinen sind Java und C# (sprich: C-Sharp).

Die Sprachen C und C++ werden mit Compilern in Maschinencode umgewandelt. Während für spezifische Projekte teilweise noch ältere Compiler aus früheren Jahren verwendet werden, sind heutzutage folgende Compiler in der breiten Masse vertreten: Der VisualC Compiler des Visual Studios von Microsoft, der GNU C Compiler GCC sowie der LLVM-Compiler Clang. Alle drei Compiler sind nach heutigem Stand gratis verfügbar. GCC sowie Clang existieren auf allen gängigen Systemen und sind zudem OpenSource, wohingegen der Compiler von Microsoft nicht OpenSource ist und hauptsächlich auf Windows in Kombination mit dem Visual Studio benutzt wird und bis vor Kurzem auch nur in einer käuflichen Version erworben werden konnte.

Nebst dem Compiler wird für eine vollständige Übersetzung auch noch ein Linker benötigt. Diese sind jedoch heutzutage direkt in die Compiler integriert und moderne Programmierumgebungen konfigurieren den korrekten Aufruf automatisch, sodass heutzutage eigentlich nur noch vom Compilieren gesprochen wird, auch wenn es Compilieren und Linken bedeutet.

Wenngleich alle existierenden Compiler grundsätzlich die Aufgabe haben, Sourcecode in ein lauffähiges Programm umzuwandeln, unterscheiden sie sich doch teils markant in den Details. Beim Umsteigen von dem einen auf den anderen Compiler können Fehler angezeigt werden, die ursprünglich nicht erkannt wurden. In einigen Fällen ist es sogar möglich, das ein Programm nicht mehr richtig läuft. Insbesondere, wenn ein Programm auf verschiedenen Systemen zum Laufen gebracht werden soll, können bislang nie gesehene Warnungen und Fehler auftreten. Die Fülle an Unterschieden kann hier nicht aufgezählt werden, doch jeder, der sich an ein sogenanntes Cross-Compiling heranwagt, wird einige Überraschungen erleben und vieles über die Sprachen C und C++ lernen.

Die Übersetzung von C und C++

Die Übersetzung des Sourcecodes in ein Binary geschieht grob über drei Schritte: Preprocessing, Compiling, Linking.

Es ist zu beachten, dass ein Programm in C oder C++ häufig nicht nur aus einer einzelnen Datei besteht, sondern aus mehreren. Die Vorverarbeitung (das Preprocessing) und das eigentliche Übersetzen (Compilieren) wird für jede Implementations-Datei (.c .cpp) einzeln eingeleitet. Header-Dateien (.h) werden normalerweise nicht als Startpunkt verwendet, sondern erst beim Preprocessing in die Implementations-Dateien eingebunden. Schlussendlich wird durch das Zusammenfügen (Linken) der einzelnen compilierten Teile zusammen mit allen benötigten Bibliotheken ein einziges lauffähiges Programm erstellt. Hier ein kurzer Durchgang durch eine vollständige Übersetzung eines Programmes:

Eine Datei, die in C oder C++ geschrieben ist, beinhaltet normalerweise Angaben wie beispielsweise Kommentare oder Leerzeichen, die in den Programmtext eingefügt wurden, die jedoch der Compiler nicht zu wissen braucht. Solche rein unterstützenden Angaben werden vom Preprozessor aus dem Quellcode herausgelöscht. Des weiteren gibt es mittels des Preprozessors die Möglichkeit, andere Dateien einzubinden, Makros zu definieren, sowie bedingte Compilierung und Fehlermeldungen zu steuern. Eine umfassende Erklärung der Möglichkeiten der Steuerung des Preprozessors kann in der Sektion Preprozessor nachgelesen werden.

Die daraus entstehende, vorverarbeitete (preprocessed) Datei wird sodann compiliert. Diese Übersetzung kann in mehrere Schritte aufgeteilt werden: Zuerst werden sämtliche Angaben über Typen, Variablen, Funktionen, Klassen... usw, sowie der gesamte Aufbau der Programmierung zusammengetragen und in einer internen Struktur des Compilers gespeichert. Diese Phase wird als das Parsen des Programmcodes bezeichnet. Während dieser ersten Phase wird der Code auf korrekte Syntax, also korrekte Anordnung der einzelnen Teile geprüft.

Danach erfolgt eine Konsistenzprüfung, welche überprüft, ob sämtliche angesprochenen Symbole definiert wurden, ob die Typen korrekt gesetzt sind, dass keine Schutzverletzungen auftreten... usw. Hat der Compiler bis hier keinen Fehler gefunden, so beginnt er mit der Übersetzung der einzelnen Anweisungen in Assemblercode, Stück für Stück. Danach folgt auf Wunsch eine Optimierung des Codes, womit sowohl Platzverbrauch und insbesondere auch Geschwindigkeit des schlussendlichen Programmes verbessert werden. Ganz zum Schluss übernimmt ein Assembler die Übersetzung in Maschinencode.

Das Resultat der Übersetzung einer einzelnen Datei wird in eine sogenannte Objektdatei gespeichert. Mittels des Linkers werden sämtliche Objektdateien nun zu einem kompletten Programm zusammengefügt. Der Linker überprüft hierbei für jede Objektdatei, welche Definitionen mit externer Bindung noch fehlen. Der Linker sucht Definitionen sowohl in den soeben übersetzten Objekt-Dateien, als auch in den vorcompilierten Standardbibliotheken. Findet der Linker die Definition in keiner vorhandenen Objektdatei, so entsteht ein Linker-Fehler und das Programm kann nicht fertiggestellt werden. Sind jedoch alle Angaben komplett vorhanden, so wird der Linker eine einzelne Datei herstellen, die sämtliche übersetzten Teile enthält, die korrekt miteinander verbunden wurden und somit ein lauffähiges Programm darstellen.

Es sei hier bemerkt, dass diese Auflistung je nach Compiler mehr oder weniger ausgeprägt sein kann. Des weiteren sei bemerkt, dass eine Mischung von C und C++ Objektdateien zu Linker-Problemen führen kann, die jedoch mit dem extern-Keyword gelöst werden können.

Wenn das Programm schlussendlich ausgeführt wird, lädt ein sogenannter Loader das Programm in den Speicher und initialisiert das Runtime-System. Dieses wiederum führt sodann das Programm aus und gewährleistet die sichere und erfolgreiche Abarbeitung des Programmes wie beispielsweise auch, dass sogenannt dynamische Bibliotheken noch nachgeladen werden. Dies alles gehört jedoch nicht mehr zur eigentlichen Übersetzung des Programmes, ist oftmals systemspezifisch und wird somit hier (vorerst) nicht weiter erläutert.

Schlussbemerkung

Der konkrete Ablauf der Kompilierung spielt beim Programmieren selten eine Rolle, kann jedoch bis ins kleinste Detail konfiguriert werden. Spezifische Einstellungen für die einzelnen Teile der Übersetzung sind jedoch für die eigentliche Programmierarbeit normalerweise sekundär. Im alltäglichen Programmieren werden höchstens einige Preprozessor-Anweisungen geschrieben, ein paar (viele) Compilerfehler ausgebügelt und ein paar wenige Linker-Fehler behoben werden.

Nächstes Kapitel: Runtime-System