Bytecode

Bytecode ist in der Informatik die Bezeichnung für eine Sammlung von Befehlen in binärer Form für eine virtuelle Maschine. Es kann sowohl ein Programm als auch die Vorgabe bezeichnen, mit der die Programme codiert sind. Bytecode ist im Gegensatz zu Quelltexten oder Skripten nicht „menschenlesbar“, Programmanweisungen und Werte sind wie in Maschinencode als „Bytes“ codiert. Allerdings mit dem Unterschied, dass Maschinencode direkt von einem Prozessor ausgeführt wird. Wenn man von „Bytecode“ spricht, betont es normalerweise, dass es sich nicht um Maschinencode handelt, und dieser von einem speziellen Programm „interpretiert“ werden soll bzw. allgemein: in Maschinencode übersetzt wird. Es schließt aber nicht aus, dass ein Bytecode durch passende Prozessoren ausführbar wäre.

Bei Kompilierung eines Quelltextes mancher Programmiersprachen oder Umgebungen – wie beispielsweise Java – wird nicht direkt Maschinencode, sondern ein Zwischencode, der Bytecode, erstellt.[1] Dieser Code ist in der Regel unabhängig von realer Hardware. Er entsteht als Resultat einer semantischen Analyse des Quelltexts und ist im Vergleich zu diesem deutlich kompakter und zumeist wesentlich effizienter interpretierbar als der originale Quelltext.

Technische Details

Die virtuelle Maschine – im Fall von Java die Java Virtual Machine (JVM) – führt dann dieses Zwischenergebnis aus, indem sie zur Laufzeit den Bytecode entweder in Maschinensprache für den jeweiligen Prozessor übersetzt (Just-in-time-Kompilierung) oder entsprechende Maschinencode-Routinen ausführt (Interpreter). Dabei muss die virtuelle Maschine für jede Rechnerplattform, auf der das Kompilat ausgeführt werden soll, vorhanden sein.[2]

Java ist nur eines der prominenteren Beispiele für eine Bytecode-basierte Programmiersprache. Andere Sprachen, die Bytecodes verwenden, sind P-Code, Lua, die .Net-Framework-Sprachen C#, F# und Visual Basic, Python, Ruby, Perl, PHP, Prolog, Limbo, Gambas sowie Tcl.[1] Während bei Java, Python und .NET der Bytecode als „Kompilat“ gespeichert und dann unabhängig vom Quellcode ausgeführt wird, wird bei den Skriptsprachen Perl (bis Version 5) und Tcl beim Programmstart der Quellcode in Bytecode übersetzt und nur im Arbeitsspeicher vorgehalten.

Die Verwendung von Bytecode macht es möglich, dieselbe virtuelle Maschine für mehrere Sprachen zu verwenden, beispielsweise die JVM für Java, Scala, Groovy und viele andere. Der Aufwand nur einen Bytecode-Compiler für eine höhere Programmiersprache zu entwickeln ist dann deutlich reduziert. Dagegen wäre der Aufwand, wenn ein Compiler eine direkte Übersetzung in Maschinencode für mehrere Betriebssysteme und Architekturen bereitstellen soll, sehr viel höher. Ein Bytecode kann auch unabhängig von einer speziellen Sprache für einen bestimmten Einsatzzweck entwickelt worden sein, zum Beispiel WebAssembly.

Die Ausführung von Bytecode durch das Programm, das die virtuelle Maschine repräsentiert, geht auf Kosten von dessen Startzeit, welche im Allgemeinen aber eher nur messbar als wahrnehmbar beeinträchtigt wird. Spezielle Just-in-time-Compiler (JIT-Compiler) übersetzen Bytecode-Stücke einmal während der Programmausführung in entsprechende Maschinencodestücke und führen diese dann aus. Dadurch lassen sich die Ausführungszeiten, jedoch nicht die Startzeiten, oft in den Bereich von vorübersetztem Maschinencode reduzieren.[3]

Hintergründe

Viele interpretierte Sprachen verwenden auch intern Bytecode. Das bedeutet, dass der Bytecode an sich unsichtbar für den Programmierer und Endbenutzer gehalten wird und automatisch als Zwischenschritt der Interpretation des Programmes erzeugt wird. Beispiele für aktuelle Sprachen, die diese Technik verwenden, sind: Perl, PHP, Prolog, Tcl und Python. In Python wird der Bytecode nach dem ersten Parsen in .pyc-Dateien (welche den Bytecode enthalten) abgelegt; das Vorgehen ähnelt damit prinzipiell dem bei Java. Dieser Schritt ist jedoch optional.

Auch dieses Verfahren ist sehr alt: Bereits für Lisp wurde in den 1960er Jahren Bytecompiling eingesetzt: Die 256 atomare Funktionen wurden in einem Byte kodiert. Frühe BASIC-Versionen der 1970er und 1980er Jahre verwendeten anstelle der Schlüsselwörter bestimmte Byte-Werte, sogenannte Tokens, um die Ausführung ihrer Programme zu beschleunigen und den Programmtext in einer kompakteren Form abzuspeichern. Der restliche Text – zum Beispiel Variablen, mathematische Ausdrücke und Zeichenketten – wurde jedoch unverändert gespeichert. Bei Ausgabe durch den LIST-Befehl wurden die Tokens wieder in lesbare Schlüsselwörter umgewandelt. Ein bekannter früher Heimcomputer, der Bytecode verwendet, ist der TI 99/4a von Texas Instruments.

Aufgrund des stark begrenzten Speicherplatzes der ersten Homecomputer war es unumgänglich, beispielsweise BASIC-Programme als Bytecode in den RAM zu überführen. Alternativ konnte man nur mit direkt eingegebenen Maschinencode programmieren. Reiner Text wäre sehr schnell an die Speichergrenze gestoßen. Die Platzersparnis von Quelltexten in Bytecodeform war aber auch später noch angesichts langsamer und begrenzter Massenspeicher von Vorteil. Hinzu kommt der große Vorteil, dass Bytecode deutlich schneller ausgeführt werden kann als ein normaler Quelltext. Statt jeden Buchstaben eines Befehls zu vergleichen, um erst mal herauszufinden, um welchen Befehl es sich handelt, muss man nur ein oder wenige Bytes vergleichen, die diesen repräsentieren.

Rückgewinnung von Quelltext aus Kompilaten

Für Programmierer, die an Programmen arbeiten, deren Quellcode nicht offengelegt werden soll, gibt es einen wichtigen Aspekt zu beachten: Bei Programmiersprachen, wie beispielsweise C, die direkt zu Maschinencode kompiliert werden, ist es in der Regel nicht möglich, aus dem Maschinencode den ursprünglichen Quellcode zurückzugewinnen.[4] Bei Kompilierung zu Bytecode ist dies hingegen oftmals kein allzu großes Problem. Hierbei kann zwar nicht der exakte Quellcode rekonstruiert werden, allerdings ist es häufig möglich, zumindest äquivalenten Code in der Quellsprache zurückzugewinnen, teilweise mit verblüffenden Ähnlichkeiten. Beispielsweise bei Java und .NET ist dies in den meisten Fällen sehr gut möglich; in Prolog mit WAM-Bytecode ist eine Rückgewinnung immer möglich;[5]

Mithilfe eines sogenannten Obfuskators kann die Nutzbarkeit des durch das Dekompilieren erhaltenen Quelltextes sehr stark eingeschränkt werden; mitunter ist ein Dekompilieren in die Quellsprache gar nicht mehr möglich.

Für eigene .NET-Projekte kann der .NET Reflector verwendet werden um den Quellcode wiederherzustellen. Dieser unterstützt die drei .NET-Sprachen C#, Visual Basic .NET und Delphi.NET. Alternativ bietet sich das quelloffene dnSpy an.

Weblinks

Einzelnachweise

  1. a b Bytecode. In: itwissen.info. 2. November 2013, abgerufen am 19. Oktober 2018.
  2. Bytecode. In: techterms.com. 23. Januar 2018, abgerufen am 19. Oktober 2018 (englisch).
  3. Carles Mateo: Performance of several languages. In: carlesmateo.com. 26. März 2017, abgerufen am 8. April 2021 (englisch, Blogeintrag).
  4. What Is: bytecode. In: techtarget.com. September 2005, abgerufen am 19. Oktober 2018 (englisch).
  5. CS1Bh Lecture Note 7 Compilation I: Java Byte Code. (PDF) In: ac.uk. 6. Februar 2003, S. 5, archiviert vom Original am 28. März 2014; abgerufen am 19. Oktober 2018 (englisch).