|
| 1 | +\lesson{Preprocessing, Compiler, Assembler, Linker} |
| 2 | + |
| 3 | +In der letzten Lektion klang es bereits an -- was der Befehl \texttt{g++} |
| 4 | +eigentlich tut, ist mehr, als nur Kompilieren im strengen Sinne des Wortes. Wir |
| 5 | +wollen jetzt kurz erkunden, welche anderen Schritte in den Prozess vom |
| 6 | +Quellcode in die ausführbare Datei notwendig sind und wie sie geschehen. Das |
| 7 | +ist im Alltag nicht sehr wichtig, kann uns aber helfen, einige Fehlermeldungen |
| 8 | +besser zu verstehen. Von daher müsst ihr auch nicht alles hier beschriebene |
| 9 | +vollständig verstehen. |
| 10 | + |
| 11 | +In Lektion 1 haben wir vereinfacht dargestellt, dass der Compiler eine |
| 12 | +Quelltextdatei mit der Endung \texttt{.cpp} nimmt und daraus direkt eine |
| 13 | +ausführbare Datei erstellt. Die Schritte, die hier eigentlich in einem Befehl |
| 14 | +durchgeführt werden, aber zu trennen sind, sind das \emph{Preprocessing}, das \emph{Kompilieren}, das |
| 15 | +\emph{Assemblieren} und das \emph{Linken}. |
| 16 | + |
| 17 | +Beim Preprocessing werden alle \texttt{\#include}-Anweisungen aufgelöst und so etwas wie Makros ersetzt. Das ist der erste Schritt, der passiert, wenn wir \texttt{g++} aufrufen. Das Ergebnis des Preprocessings ist ein \Cpp-Programm, das nur noch die \Cpp-Features enthält, die wir auch wirklich benutzen. Das ist der Grund, warum wir in den bisherigen Lektionen immer \texttt{g++ -o helloworld helloworld.cpp} benutzt haben, obwohl wir in \texttt{helloworld.cpp} auch \texttt{\#include <iostream>} stehen haben -- der Compiler hat das schon für uns erledigt. |
| 18 | + |
| 19 | +Das Kompilieren übersetzt unseren \Cpp-Code in eine Zwischenform, so genannten |
| 20 | +\emph{Assembler}. In Lektion 1 haben wir den Maschinencode angesprochen, in der |
| 21 | +Befehle und Parameter an Befehle in 1en und 0en dargestellt werden. Assembler |
| 22 | +ist quasi die nächst höhere Evolutionsstufe -- statt die Befehle binär zu |
| 23 | +kodieren, gibt es für jeden Befehl ein so genanten \emph{mnemonic}, also ein |
| 24 | +merkbares kurzes Wort. Ein Befehl ist allerdings deutlich weniger mächtig, als |
| 25 | +z.B. eine Anweisung in \Cpp. Früher wurden ganze Betriebssysteme in Assembler |
| 26 | +geschrieben, da es einfach nichts Besseres gab, heutzutage ist Assembler bis |
| 27 | +auf die exotischsten Anwendungsfelder eigentlich ausgestorben, da es viel zu |
| 28 | +anstrengend und Fehleranfällig ist. Der Compiler tut aber noch mehr, als |
| 29 | +einfach nur in diese Zwischensprache zu übersetzen -- er führt auch |
| 30 | +\emph{Optimierungen} durch, d.h. er sortiert Anweisungen um, damit der Code |
| 31 | +schneller läuft, aber das Gleiche tut. Dieser Prozess ist sehr umständlich, |
| 32 | +aber heutige Compiler sind tatsächlich so gut im Optimieren, dass sie meistens |
| 33 | +deutlich schnelleren Code erzeugen, als ein Mensch es je könnte. |
| 34 | + |
| 35 | +Der nächste Schritt ist dann das Assemblieren. Das übersetzt den Assembler des |
| 36 | +ersten Schrittes tatsächlich in Maschinensprache (genauer: In ein Dateiformat, |
| 37 | +welches ELF heißt, welches dann die Maschinensprache plus einiger |
| 38 | +Meta-Informationen enthält). Der Assembler erzeugt so genannte |
| 39 | +\emph{Objectfiles}, die meistens die Endung \texttt{.o} haben (und im |
| 40 | +ELF-Format sind). Ein Objectfile enthält dann mehrere Funktionen (in |
| 41 | +Maschinencode) und Variablen, die es \emph{exportieren} kann, d.h. Funktionen |
| 42 | +anderer Objectfiles die dagegen (im nächsten Schritt) gelinkt werden, können |
| 43 | +diese Variablen und Funktionen sehen. Der Vorteil, diesen Schritt vom |
| 44 | +vorhergehenden zu trennen ist, dass wir wenn wir wollen auch nur kompilieren |
| 45 | +können und den resultierenden Assembler betrachten -- das kann uns helfen, |
| 46 | +Engpässe in unserem Code, an denen der Compiler nicht hinreichend gut optimiert |
| 47 | +zu erkennen und möglicherweise zu verbessern. z.B. in der Spielentwicklung ist |
| 48 | +sehr schnell laufender Code wichtig. |
| 49 | + |
| 50 | +Der letzte Schritt ist das Linken. Hier werden mehrere Objectfiles genommen und |
| 51 | +miteinander verbunden, zu einer ausführbaren Datei. Wenn in einer der |
| 52 | +Objectfiles eine \texttt{main}-Funktion existiert, wird diese als |
| 53 | +Eintrittspunkt für das Programm genommen, sonst gibt es einen |
| 54 | +\emph{Linkerfehler}. Ein Linkerfehler tritt auch auf, wenn wir versuchen, eine |
| 55 | +Funktion zu verwenden, die es nicht gibt (z.B. indem wir sie mittels |
| 56 | +\texttt{extern} deklarieren, ohne später das relevante Objectfile mit |
| 57 | +anzugeben). Linkerfehler deuten also darauf hin, dass wir vergessen haben, alle |
| 58 | +relevanten Dateien auf der Kommandozeile anzugeben, oder dass eine |
| 59 | +\texttt{main}-Funktion fehlt, oder dass wir in mehren Dateien eine Funktion |
| 60 | +gleichen Namens haben, oder\dots |
| 61 | + |
| 62 | +Um das Diagramm aus der ersten Lektion zu ergänzen, dies ist der Weg, den euer |
| 63 | +Programm durch die verschiedenen Phasen geht: |
| 64 | + |
| 65 | +\begin{tikzpicture} |
| 66 | + \tikzstyle{block} = [ shape=rectangle, rounded corners = 0.1cm, draw=black, inner xsep=0.5cm, inner ysep = 0.3cm ]; |
| 67 | + \tikzstyle{arr} = [ ->, thick, shorten >= 2pt, shorten <= 2pt ]; |
| 68 | + |
| 69 | + \node (nHelloWorldCpp) [block] {\texttt{helloworld.cpp}}; |
| 70 | + \node (nHelloWorldS) [block, right of = nHelloWorldCpp, node distance = 12cm] {\texttt{helloworld.S}}; |
| 71 | + \draw [arr] (nHelloWorldCpp) -- (nHelloWorldS) node [midway,above,font=\small] {\texttt{g++ -S -o helloworld.S helloworld.cpp}}; |
| 72 | + \node (nHelloWorldO) [block, below of = nHelloWorldCpp, node distance = 2cm] {\texttt{helloworld.o}}; |
| 73 | + \draw [arr] (nHelloWorldS) -- (nHelloWorldO) node [pos=0.56,above,sloped,font=\small] {\texttt{g++ -c -o helloworld.o helloworld.S}}; |
| 74 | + \node (nAnderesO) [block, below of = nHelloWorldO, node distance = 1cm] {\texttt{anderes.o}}; |
| 75 | + \node (nHelloWorld) [block, below of = nHelloWorldS, node distance = 2cm] {\texttt{helloworld}}; |
| 76 | + \draw [arr] (nHelloWorldO) -- (nHelloWorld) node [midway,below,font=\small] {\texttt{g++ -o helloworld helloworld.o anderes.o}}; |
| 77 | + \draw [arr] (nAnderesO) -| (nHelloWorld) node {}; |
| 78 | +\end{tikzpicture} |
| 79 | + |
| 80 | +Der bisherige Befehl, den wir zum Kompilieren benutzt haben, ist tatsächlich |
| 81 | +nur ein Spezialfall von diesem: Geben wir nämlich auf der Kommandozeile eine |
| 82 | +input-Datei an, so rät \texttt{g++} anhand der Dateierweiterung und der |
| 83 | +Parameter, was wir damit tun wollen. Er führt dann alle Schritte, um von |
| 84 | +unserer input-Datei zu der gewünschten zu kommen automatisch aus, wenn wir also |
| 85 | +\texttt{g++ -o helloworld helloworld.cpp} eingeben, dann weiß der Compiler, |
| 86 | +dass wir eine ausführbare Datei wünschen (da wir weder \texttt{-c} noch |
| 87 | +\texttt{-S} angegeben haben) und dass er dafür kompilieren, assemblieren und |
| 88 | +linken muss (da wir ihm eine \texttt{.cpp} Datei gegeben haben). Genauso konnte |
| 89 | +er in der vorigen Lektion raten, dass \texttt{g++ -o tictactoe tictactoe.cpp |
| 90 | + tictactoe.o} heißt, dass wir eine ausführbare Datei wollen, die aus einem |
| 91 | +kompilierten und assemblierten \texttt{tictactoe.cpp} zusammen gelinkt mit |
| 92 | +\texttt{tictactoe.o} bestehen soll. |
| 93 | + |
| 94 | +\begin{praxis} |
| 95 | + \begin{enumerate} |
| 96 | + \item \texttt{assemble.cpp} enthält ein kleines (ziemlich nutzloses) |
| 97 | + Programm, welches zwei Zahlen addiert und das Ergebnis ausgibt. |
| 98 | + Kompiliert es (nun nur der erste Schritt in dem Diagramm, nicht so, wie |
| 99 | + in den vergangenen Lektionen) und schaut euch das resultierende |
| 100 | + \texttt{.S}-file in einem Editor an. Ihr müsst nicht verstehen, |
| 101 | + was genau hier überall passiert, aber vielleicht findet ihr ja die |
| 102 | + \texttt{main}-Funktion, die Definition der Variablen und die Addition? |
| 103 | + |
| 104 | + Wir können nun mal Optimierung anschalten -- gebt dazu zusätzlich den |
| 105 | + Parameter \texttt{-O3} direkt nach dem \texttt{g++} an. Schaut euch das |
| 106 | + \texttt{.S}-file nun wieder im Editor an. Was fällt euch |
| 107 | + (im Vergleich zu vorher) auf? |
| 108 | + \item Assembliert eines der im vorigen Schritt erzeugten \texttt{.S} files |
| 109 | + in ein \texttt{.o}-File. |
| 110 | + \item Benennt in einem eurer bisherigen Programme die |
| 111 | + \texttt{main}-Funktion um und versucht, es zu kompilieren (wie in den |
| 112 | + bisherigen Lektionen, also alle Schritte auf einmal). Schaut euch die |
| 113 | + resultierenden Fehlermeldungen an. Wo wird euch der Linkerfehler |
| 114 | + ausgegeben? |
| 115 | + \item Macht die Umbenennung wieder rückgängig und kompiliert das Programm |
| 116 | + erneut -- übergebt aber dieses mal den Quellcode doppelt (also z.B. |
| 117 | + \texttt{g++ -o helloworld helloworld.cpp helloworld.cpp}). Was |
| 118 | + beobachtet ihr? Könnt ihr die Beobachtung erklären? |
| 119 | + \end{enumerate} |
| 120 | + |
| 121 | + \inputcpp{assemble.cpp} |
| 122 | +\end{praxis} |
0 commit comments