Was ist eine Binary?

Ein Binary ist eine ausführbare Datei, die aus Maschinencode besteht, also Code, der direkt von der CPU des Computers verstanden und ausgeführt werden kann. Sie ist das Ergebnis der Kompilierung von Quellcode durch einen Compiler in eine binäre und damit ausführbare Datei.

Im IoT-Bereich werden häufig die Programmiersprachen C und C++ eingesetzt. Die wichtigsten Gründe dafür sind:

  • C und C++ arbeiten direkt mit der Hardware und ermöglichen Systemressourcen wie Speicher oder CPU direkt zu kontrollieren.
  • Mit C und C++ kann man effizientere Programme schreiben, die relativ wenig Speicher benötigen und relativ wenig Rechenleistung verbrauchen. In IoT-Geräten, die oft sehr beschränkte Ressourcen haben (wenig Speicher, geringe Rechenleistung, geringer Stromverbrauch), ist dies von entscheidender Bedeutung.

Kompilieren von Quellcode

Der Kompilierungsprozess im Überblick

Der Kompilierungsprozess eines Quellcodes in C zu einer ausführbaren Datei durchläuft mehrere Schritte:

Compilation and executable file
Kompilieren eines Quellcodes zu einer ausführbaren Datei
  1. Der Preprocessor bereitet den Quellcode vor, indem er unter anderem Kommentare entfernt und den Inhalt von Header-Dateien einfügt, die laut Quellcode eingebunden werden sollen.
  2. Der Compiler übersetzt den erweiterten Quellcode in Assemblercode. Assembly ist eine Zwischensprache, die menschenlesbar ist, jedoch bereits stark dem Maschinencode ähnelt.
  3. Der Assembler wandelt den Assemblercode in Maschinencode um, der von der CPU direkt ausgeführt werden kann. Er besteht nur noch aus 0 und 1 und ist damit nicht mehr menschenlesbar.
  4. Der Linker fügt alle benötigten Bibliotheken und Dateien, auf die sich der Code bezieht zusammen und sorgt dafür, dass der erstellte Maschinencode mit diesen zusätzlichen Dateien korrekt funktioniert.

Das Resultat ist eine ausführbare Binärdatei, die der Computer verstehen und ausführen kann. Typischerweise wird auf Linux basierenden Betriebssystemen die Endung weggelassen, gelegentlich sieht man aber .elf oder .bin. Diese Dateien sind das, was man unter Windows als Portable Executable mit der Endung .exe kennt.

Der Präprozessor

Was genau der Präprozessor macht, soll mit der folgenden Abbildung deutlich werden:

Preprocessing of C code
Vorverarbeitung eines Quellcodes durch den Präprozessor

Auf der linken Seite ist der Quellcode compile_me.c dargestellt. Das Ergebnis der Vorverarbeitung befindet sich auf der rechten Seite, in der temporären Datei compile_me.i. Welche Unterschiede gibt es?

  • Alles, was mit # beginnt, ist eine Präprozessordirektive und wird vom Präprozessor verarbeitet. Entsprechend wird der Inhalt der Header-Datei stdio.h, in der die Funktion puts deklariert ist, an der Stelle der #include-Direktive in den Quellcode eingefügt. Dadurch nimmt die Anzahl der Codezeilen zu.
  • Mit der #define-Direktive das Makro MY_NUMBER definiert, das den Wert 42 repräsentiert. Während der Preprocessing-Phase werden alle Vorkommen von MY_NUMBERdurch den Wert 42 ersetzt.
  • Alle Kommentare werden entfernt. Dies betrifft in unserem Beispiel die mit // eingeleitete Zeile.

Das Resultat ist ein bereinigter Code, der bereit ist für den nächsten Schritt der Kompilierung.

Der Compiler

Im zweiten Schritt des Kompilierungsprozesses wird der C-Quellcode in Assemblersprache übersetzt:

Compilation of preprocessed C code
Übersetzung des vorverarbeiteten Quellcodes in Assemblersprache

Wie du siehst, unterscheidet sich die Assemblersprache in compile_me.s deutlich vom C-Code.

Assemblersprache ist eine niedrigstufige Programmiersprache, die die Maschinenbefehle textuell darstellt und damit menschenlesbar ist. Sie ist von der zugrunde liegenden Hardware-Architektur abhängig. So hat jede CPU-Architektur ihre eigenen Befehlsätze und eine andere Anzahl und Art von Registern, die in der Assemblersprache verwendet werden.

Assemblersprache besteht aus:

  • Mnemonics sind leicht merkbare, textuelle Darstellungen der Maschinenbefehle. Sie stehen für spezifische Operationen, wie z. B. push, add oder sub, die bestimmte Anweisungen an die CPU ausführen.
  • Operanden sind die Werte oder Adressen, die von den Mnemonics verarbeitet werden. Sie können Register wie fp, lr oder sp, Speicheradressen oder unmittelbare Werte (konstante Zahlen) sein.

Ein wichtiger Punkt zum Schluss: Während die Übersetzung von C in Assemblersprache relativ einfach und direkt ist, gestaltet sich der umgekehrte Weg – also von Assemblersprache zurück zu C – insbesondere bei komplexeren Programmen oft weniger eindeutig. Das bedeutet, dass es mehrere verschiedene C-Programme geben kann, die denselben Assembly-Code erzeugen. Dieser Punkt ist besonders relevant im Zusammenhang mit Reverse Engineering, worauf wir später noch näher eingehen werden.

Der Assembler

Im dritten Schritt des Kompilierungsprozesses wird die Assemblersprache aus der Datei compile_me.s in Maschinensprache übersetzt. Das Resultat ist die Objektdatei compile_me.o:

Translating assembler language into machine code
Übersetzung von Assemblersprache in Maschinensprache

Eine Objektdatei ist das Ergebnis der Kompilierung von Quellcode in Maschinencode. Diese Datei ist jedoch nicht ausführbar, da sie noch nicht vollständig verlinkt wurde. Das bedeutet, dass sie weder die endgültigen Speicheradressen noch die vollständigen Referenzen auf externe Funktionen, wie etwa Bibliotheken, enthält.

Die Datei compile_me.o beispielsweise enthält zwar den Maschinencode für die Funktionen print_test und main, aber sie enthält nur einen Verweis auf die Funktion puts, ohne die tatsächliche Speicheradresse der Funktion zu kennen, die wie die Header-Datei stdio.h Bestandteil der C-Standardbibliothek libc ist. Daher kann die Objektdatei noch nicht ausgeführt werden.

Bei Maschinencode handelt es sich um Binärcode. Wird dieser wie in der Abbildung rechts als Text interpretiert, dann besteht er aus einigen scheinbar zufälligen Buchstaben oder Zeichen. Zum Großteil gibt es für die binären Daten aber keine Entsprechung im Textformat, sodass viele unverständliche Zeichen erscheinen.

Der Linker

Im letzten Schritt des Kompilierungsprozesses wird schließlich aus der Objektdatei compile_me.o die ausführbare Datei compile_me:

Linking object files
Zusammenfügen einer Objektdatei mit Bibliotheken zu einer ausführbaren Datei

Linking ist der Prozess, bei dem der Linker mehrere Objektdateien und Bibliotheken zu einem vollständigen, ausführbaren Programm zusammenfügt. Dabei werden Platzhalter für Funktionsaufrufe und Variablenreferenzen durch ihre tatsächlichen Speicheradressen ersetzt.

In unserem Beispiel bedeutet dies, dass der Linker die Objektdatei compile_me.o mit der C-Standardbibliothek libc verknüpft, in der die Funktion puts implementiert ist.

Erst dann wird die exakte Speicheradresse von puts in den finalen Code eingefügt, sodass die gesamte Datei ausführbar ist. Beim statischem Linking wird die Speicheradresse von puts bereits während der Linkerphase in den finalen Code eingefügt, sodass die ausführbare Datei alle notwendigen Funktionen enthält. Beim dynamischen Linking hingegen wird die Adresse von puts erst zur Laufzeit festgelegt, wenn die Bibliothek geladen wird.

Das Resultat ist eine ausführbare Datei, die auf Linux-basierten Systemen häufig im Executable and Linkable Format (ELF) gespeichert wird.