Selbstmodifizierender Code

Mit der Bezeichnung Selbstmodifizierender Code (englisch Self Modifying Code) wird ein Abschnitt eines Computerprogramms bezeichnet, das zur Lösung der Programmaufgabe Teile des eigenen Programmcodes während der Ausführung gezielt verändert. Unter der Bezeichnung „freier Rechenplan“ hatte schon Konrad Zuse selbstmodifizierenden Code als Möglichkeit in die von ihm entworfene Programmiersprache Plankalkül aufgenommen.

Das Programm muss dabei in der Lage sein, im Maschinencode bestimmte Befehle durch sinnvolle andere Maschinenbefehle zu ersetzen. Bei höheren Programmiersprachen (z. B. APL) manipuliert das Programm meist den Quellcode als Zeichenkette (Text, String).

Selbstmodifizierender Code kann unter anderem da verwendet werden, wo es möglich ist, mehrere, nur an wenigen Stellen unterschiedliche Programmteile zu einem einzigen zusammenzufassen.

Der selbstmodifizierende Code eines Programms hat meist nichts mit Lernen oder der Verbesserung eines Programmes zu tun. Selbstmodifizierende Programme, die die Hochsprache des Programms modifizieren, sind in der Zukunft möglicherweise hilfreich, die Maschinenintelligenz zu steigern.

Motivation

Die Methode, Code sich selbst modifizieren zu lassen, stammt hauptsächlich aus einer Zeit, in der Ressourcen (CPU-Zeit, Speicher) noch sehr knappe Güter waren – es wurde also oftmals eine Optimierung des Laufzeitverhaltens oder Speicherverbrauchs angestrebt. Sogenannte Laufzeitpacker dekomprimieren mittels einer Hilfsroutine das eigentliche Programm, bevor sie es starten. Sowohl die Laufzeitoptimierung mittels Selbstmodifikation als auch die Speicherbedarfsreduktion sind mittlerweile nur noch äußerst selten notwendig (z. B. beim „Retro Computing“, wenn also auf sehr alten Systemen programmiert wird). Ein anderer Grund zur Selbstmodifikation war der Schutz geistigen Eigentums, um die tatsächlichen Algorithmen zu verbergen. In Anbetracht der historischen Motivationen zum Schreiben von selbstmodifizierendem Code sollte das Vorhandensein von solchem Code nicht alleine nach modernen Maßstäben zur Bemessung von Codequalität bewertet werden, sondern auch immer die (historischen und/oder technischen) Umstände berücksichtigt werden.

Architektur- und Sprachabhängigkeit

Die In-Memory-Veränderung eines Maschinensprachen-Programmcodes ist in einer Von-Neumann-Architektur einfach möglich, da Programmteile zeitweise als Daten betrachtet werden können, später dann wieder als Programmteile; bei der Von-Neumann-Architektur besitzen Programm und Daten denselben Adressraum. In Prozessoren mit Harvard-Architektur ist das Modifizieren von Maschinencode während der Laufzeit nicht vorgesehen, Programm und Daten haben getrennte Adressräume. Evtl. stehen spezielle Befehle für das Übertragen von Informationen zwischen den Adressräumen zur Verfügung, oder es müssen Umwege außerhalb des Arbeitsspeichers beschritten werden.

Höhere Programmiersprachen können in das „normale Programm“ einen Compiler einbetten, dann müssen die Modifikation ggf. nicht direkt in Maschinensprache ausgeführt werden. Hilfreich ist, wenn die Sprache Homoikonizität besitzt (Selbst-Abbildbarkeit: die Eigenschaft einer Programmiersprache, dass Programme gleichzeitig Datenstrukturen derselben Sprache sind/sein können; in solchen Sprachen ist es einfach, Programme zu schreiben, die Programme schreiben). Die Portierung von selbstmodifizierendem Maschinencode auf einen beliebigen Prozessor ist fast nicht möglich. Mittlerweile verfügen viele Prozessorarchitekturen, die eigentlich Von-Neumann-artig aufgebaut sind, über Methoden, das Schreiben in (Maschinen-)Codebereiche sowie das Ausführen von Datenbereichen zu verhindern (z. B. NX-Bit), als Schutzmaßnahme gegen Pufferüberlauf-Angriffe. Bei höheren Programmiersprachen sind für selbstmodifizierenden Code in der Regel interpretierende (also nicht kompilierende) Systeme notwendig.

Vorteile

  1. Bei bestimmten Aufgabenstellungen kann ein sehr kompaktes Programm konstruiert werden.
  2. Die gefundene Programmlösung kann elegant erscheinen.
  3. Das Programm kann vor Reverse Engineering besser geschützt werden.

Nachteile

  1. Die Erstellung von selbstmodifizierendem Code wird von Compilern nicht unterstützt.
  2. Der Programmcode ist schwierig oder gar nicht portierbar.
  3. Der Maschinencode ist schwierig nachzuvollziehen.
  4. Der CPU-Entwurf wird deutlich komplizierter; mitunter kommt es bei anderen CPU-Versionen zu Fehlern.[Anmerkung 1]

Beispiele

Videospiel

In einem Videotennis-Spiel kann im Programmteil, das den Ball steuert, ein Inkrement-Befehl durch einen Dekrement-Befehl ersetzt werden, wenn er an die Wand prallt, dadurch wird die Bewegungsrichtung umgekehrt.

Die Bytes, die die Koordinaten des Balles beinhalten, können so im Speicher abgelegt werden, dass sie gleichzeitig als direkte Parameter eines Kommandos interpretiert werden. Man stelle sich beispielsweise einen Befehl vor, der dazu führt, dass der Ball an einer bestimmten Stelle angezeigt wird. Statt nun die beiden Argumente „X-Position“ und „Y-Position“ indirekt als Variablen anzusprechen, können sie direkt so im Speicher abgelegt sein, dass sie Teil des Befehls „Stelle Ball dar“ sind.

Kombination der beiden Beispiele als Pseudo-Programm:

  • wenn Ball an vertikale Wand geprallt ist und im Programmcode „inkrementiere x-Koordinate“ steht, dann schreibe an die entsprechende Speicherstelle den Befehl für „dekrementiere x-Koordinate“ und überspringe den nächsten Befehl
  • wenn Ball an vertikale Wand geprallt ist und im Programmcode „dekrementiere x-Koordinate“ steht, dann schreibe an die entsprechende Speicherstelle den Befehl für „inkrementiere x-Koordinate“
  • wenn Ball an horizontale Wand geprallt ist und im Programmcode „inkrementiere y-Koordinate“ steht, dann schreibe an die entsprechende Speicherstelle den Befehl für „dekrementiere y-Koordinate“ und überspringe den nächsten Befehl
  • wenn Ball an horizontale Wand geprallt ist und im Programmcode „dekrementiere y-Koordinate“ steht, dann schreibe an die entsprechende Speicherstelle den Befehl für „inkrementiere y-Koordinate“
  • inkrementiere x-Koordinate des Balldarstellungsbefehls
  • inkrementiere y-Koordinate des Balldarstellungsbefehls
  • Stelle den Ball dar an Position 1, 1 und fange von vorne an

Sowohl die beiden Befehle zum Inkrementieren als auch die Koordinaten „1,1“ stellen in diesem Beispiel lediglich Anfangswerte dar, die vom Programm selbst modifiziert werden.

Mathematikprogramm

In Microsoft BASIC auf Commodore Computern (z. B. PET, VC 20, C64) war es über ein kurzzeitiges Anhalten eines Programms effektiv möglich, eine über den INPUT-Befehl im Programm abgefragte Benutzerfunktion (z. B. "SIN(X)") an den Programmeditor zu übergeben, der eine Zeile im BASIC-Programm entsprechend veränderte, worauf das Programm ohne Verlust der Variableninformation (mittels GOTO-Befehl) wieder fortgesetzt wurde und die neue Zeile für Berechnungen nutzen konnte. Dies geschah durch Ausdruck der gewünschten neuen Programmzeile in der obersten Bildschirmzeile (unter Benutzung des Microsoft-BASIC-Ausdrucks "DEF FN") und Ausgabe des Befehls "GOTO xxx" zum Rücksprung ins Programm in der zweiten Bildschirmzeile. Füllen des Tastaturpuffers mit den Zeichen HOME und mehreren Steuerzeichen für Wagenrücklauf sorgte dafür, dass nach dem STOP-Befehl der systemeigene Programmeditor die zuvor ausgegebene Programmzeile bearbeitete und bei Erreichen des GOTO-Befehls (ausgelöst durch die Wagenrücklauf-Zeichen) das BASIC-Programm wieder ausführte.

Kopierroutinen (6502-CPU)

Ein solches Unterprogramm bekam Startadresse, Zieladresse und Größe in Byte oder Speicherseiten (je 256 Byte) übergeben. Die normale Art und Weise zu kopieren bestand darin, die Adressen in zwei Zeigern innerhalb der Zeropage zu speichern, und dann indirekt-zeropage adressierbare Lade- und Speicherbefehle mit Indexzugriff zu verwenden. Diese brauchen aber auf der 6502-CPU zwei Taktzyklen mehr als die absolut adressierbaren. Der Trick zur Steigerung der Geschwindigkeit besteht darin, absolut adressierbare Befehle zu verwenden. Bei dieser Art des selbst­modifizierenden Codes werden nicht das Indexregister und die Zeigeradressen hochgezählt, sondern die Adressen im Programmcode hinter dem Opcode der absolut adressierbaren Lade- und Speicherbefehle. Damit lassen sich Kopierroutinen deutlich beschleunigen.

Anmerkungen

  1. Selbstmodifizierender Code wurde z. B. verwendet, um den Intel 8088 vom Intel 8086 zu unterscheiden, da einer eine längere Befehlspipeline besaß: Der Prozessor mit der kurzen Pipeline folgte der Änderung, der Prozessor mit der längeren Pipeline führte jedoch weiterhin den „alten“ Befehl aus, da dieser bereits in der Pipeline gespeichert war.