Befehlssatz
KI-Hinweis: Dieser Text entsteht mit Unterstützung von KI-Systemen, wird aber vor der Veröffentlichung redaktionell geprüft und verantwortet.
Redcode hat 16 Befehle. Diese Seite beschreibt jeden einzeln: was er tut, welche Modifier bei ihm eine Rolle spielen und wo er in echten Warriors vorkommt.
Wenn du nur kurz nachsehen willst, was ein Befehl macht, reicht die Übersicht. Die Abschnitte darunter gehen ins Detail.
Übersicht
| Befehl | In einem Satz |
|---|---|
DAT |
Tötet den Prozess, der darauf läuft. |
MOV |
Kopiert A nach B. |
ADD |
Addiert A auf B. |
SUB |
Zieht A von B ab. |
MUL |
Multipliziert B mit A. |
DIV |
Teilt B durch A. |
MOD |
Rest der Division von B durch A. |
JMP |
Springt nach A. |
JMZ |
Springt nach A, wenn B null ist. |
JMN |
Springt nach A, wenn B nicht null ist. |
DJN |
Zieht eins von B ab, springt dann, wenn B nicht null ist. |
SPL |
Erzeugt einen zweiten Prozess bei A. |
SEQ |
Überspringt die nächste Zeile, wenn A gleich B ist. |
SNE |
Überspringt die nächste Zeile, wenn A ungleich B ist. |
SLT |
Überspringt die nächste Zeile, wenn A kleiner als B ist. |
NOP |
Tut nichts. |
LDP und STP gibt es zusätzlich, sie greifen auf den P-Space zu. Im
Standard-Regelwerk '94nop sind sie abgeschaltet; Details stehen unter
Standards.
Die Befehle im Einzelnen
DAT
DAT.F #0, #0
Tut nichts — und beendet damit den Prozess, der darauf läuft. DAT ist die
einzige Instruktion, die tötet.
Zwei Dinge, die man wissen sollte:
Der leere Core besteht vollständig aus DAT.F $0, $0. Der Speicher ist vor
Rundenbeginn nicht leer, sondern flächendeckend tödlich. Ein Warrior stirbt
deshalb nicht daran, „getroffen" zu werden, sondern daran, auf ein DAT zu
laufen.
Als Bombe ist DAT unsichtbar. Ein Scanner sucht nach Abweichungen vom
leeren Core. Eine Zelle, in der DAT 0, 0 steht, sieht für ihn aus wie
unbenutzter Speicher — die Bombe ist tödlich und unauffällig zugleich. Deshalb
bombardieren viele Warriors mit genau diesem Wert.
Die Operanden von DAT werden nie ausgeführt, sind aber ganz normaler Speicher.
Warriors nutzen DAT-Zellen deshalb gern als Zeiger und Zähler.
MOV
MOV.I $0, $1
Kopiert. Der wichtigste Befehl der Sprache — und der, bei dem der Modifier am meisten ausmacht.
| Modifier | Was kopiert wird |
|---|---|
.A |
A-Feld nach A-Feld |
.B |
B-Feld nach B-Feld |
.AB |
A-Feld der Quelle ins B-Feld des Ziels |
.BA |
B-Feld der Quelle ins A-Feld des Ziels |
.F |
beide Felder, A→A und B→B |
.X |
beide Felder über Kreuz, A→B und B→A |
.I |
die ganze Instruktion, samt Opcode und Adressierungsmodi |
Nur .I überträgt den Opcode. Alle anderen Modifier bewegen ausschließlich
Zahlen und lassen den Opcode der Zielzelle unverändert.
Das ist kein Detail. Ein Imp mit MOV.I überlebt drei Viertel seiner Kämpfe;
derselbe Imp mit einem beliebigen anderen Modifier verliert jede einzelne
Runde, weil seine Kopien nur Zahlen sind und der Opcode dort DAT bleibt.
Die Messung dazu steht in Redcode verstehen.
ADD und SUB
ADD.AB #4, $bomb
Rechnen mit Feldern. ADD addiert, SUB subtrahiert — jeweils feldweise, wie
der Modifier es vorgibt.
Der Hauptzweck ist das Fortschreiben von Zeigern. Ein Bomber addiert bei jedem
Durchlauf seine Schrittweite auf das Zielfeld und wandert so durch den Core.
ADD.AB #4, $bomb heißt: Nimm die Zahl 4 und zähle sie auf das B-Feld der Zelle
bomb.
Beachte den Modifier .AB — er ist hier fast immer richtig, weil der Zielwert
üblicherweise im B-Feld steht, der Summand aber als unmittelbarer Wert im
A-Feld. ADD.F würde beide Felder gleichzeitig verändern, was meistens nicht
gewollt ist.
Alle Rechnungen laufen modulo der Coregröße. Es gibt keinen Überlauf und keine negativen Adressen; 8000 − 1 ergibt 7999, und 7999 + 1 ergibt wieder 0.
MUL, DIV und MOD
MUL.B $2, $3
Multiplikation, Division und Rest. In der Praxis selten — mit einer wichtigen Ausnahme.
DIV und MOD durch null töten den Prozess. Das ist der einzige Fall außer
DAT, in dem eine Instruktion einen Prozess beendet, und eine beliebte Falle:
Wer eine Division einbaut, muss sicherstellen, dass der Divisor nie null wird.
MUL hat einen eleganten Spezialeinsatz in modernen Quickscannern. Dort werden
Konstanten so gewählt, dass eine einzige Multiplikation die Fundstelle des
Gegners exakt rekonstruiert — statt sie in mehreren Schritten auszurechnen. Der
Gewinn ist Latenz: weniger Zyklen zwischen Fund und erstem Angriff.
JMP
JMP.A $start
Springt nach A, bedingungslos. Das B-Feld wird nicht ausgewertet und dient deshalb häufig als Speicherplatz für Konstanten oder Zeiger.
Üblicher Modifier ist .A oder .B — bei JMP spielt er praktisch keine
Rolle, weil ohnehin nur die Adresse zählt.
JMZ und JMN
JMZ.B $loop, $counter
Bedingte Sprünge. JMZ springt, wenn das geprüfte Feld null ist, JMN, wenn es
nicht null ist.
Welches Feld geprüft wird, bestimmt der Modifier: .A prüft das A-Feld, .B
das B-Feld, .F und .X prüfen beide (und springen nur, wenn beide die
Bedingung erfüllen).
Achtung bei Nebenwirkungen: Steht am B-Operanden ein < oder >, wird der
Zeiger auch dann verändert, wenn nicht gesprungen wird. Der Seiteneffekt
passiert beim Auflösen der Adresse, nicht beim Ausführen des Sprungs.
DJN
DJN.F $loop, $counter
Decrement and Jump if Not zero. Zieht eins vom geprüften Feld ab und springt dann, wenn das Ergebnis nicht null ist.
Der klassische Schleifenzähler — zwei Operationen in einer Instruktion, und damit ein gesparter Zyklus pro Durchlauf. In einem Core-Clear, der zehntausende Zellen überschreibt, summiert sich das erheblich.
DJN hat außerdem eine Rolle als Waffe: Ein sogenannter DJN-Stream zieht
fortlaufend Werte in fremden Zellen herunter und macht sie damit unbrauchbar,
ohne sie zu überschreiben.
SPL
SPL.B $1, $0
Erzeugt einen zweiten Prozess, der bei A weiterläuft, während der erste hinter
der SPL-Zeile fortfährt. Die Grundlage jedes Replikators.
Und hier steht der teuerste Anfängerirrtum der ganzen Sprache:
Prozesse machen dich nicht schneller.
Ein Warrior führt pro Zyklus genau eine Instruktion aus, ganz gleich, wie viele Prozesse er hat. Die Prozesse teilen sich die Zeit, sie vervielfachen sie nicht. Ein Warrior mit 100 Prozessen kommt in jedem einzelnen davon hundertmal langsamer voran als mit einem.
Prozesse sind Robustheit, nicht Tempo: Ein Warrior mit vielen Prozessen ist schwer totzukriegen, weil ein einzelner Treffer nur einen davon erwischt. Genau deshalb funktionieren Papers — nicht weil sie schnell sind, sondern weil sie nicht alle gleichzeitig sterben.
Wie deutlich das ist, zeigt ein Beispiel aus einem echten Kampf: Ein Warrior wuchs auf über 6000 Prozesse und verlor trotzdem gegen einen Scanner mit ein paar hundert.
Die Zahl gleichzeitiger Prozesse ist begrenzt — im Standard-Regelwerk auf 8000.
Ist das Limit erreicht, hat ein weiteres SPL keine Wirkung mehr.
SEQ und SNE
SEQ.I $ptr, $ptr+100
Vergleicht zwei Zellen und überspringt die nächste Instruktion, wenn die
Bedingung zutrifft: SEQ bei Gleichheit, SNE bei Ungleichheit. SEQ heißt in
älteren Quelltexten CMP; beide sind dasselbe.
Das ist das Herz jedes Scanners. Der übliche Aufbau:
SEQ.I $ptr, $ptr+dist ; sind beide Stellen gleich?
JMP $gefunden ; nein -> hier ist jemand
ADD #step, $ptr ; ja -> weitersuchen
Weil der leere Core überall identisch aussieht, bedeutet ein Unterschied
zwischen zwei Stellen, dass dort jemand ist. Der Modifier entscheidet über die
Genauigkeit: .I vergleicht die vollständige Instruktion, .F nur die beiden
Zahlenfelder — schneller zu formulieren, aber blind für Unterschiede im Opcode.
SLT
SLT.AB #10, $ptr
Skip if Less Than. Überspringt die nächste Instruktion, wenn A kleiner als B ist.
Der Vergleich ist vorzeichenlos: Alle Werte liegen zwischen 0 und Coregröße − 1, und −1 ist dasselbe wie 7999. Wer hier ein Vorzeichen erwartet, baut sich einen schwer auffindbaren Fehler ein.
SLT wird für Bereichsprüfungen benutzt — etwa um festzustellen, ob ein Zeiger
den eigenen Code erreicht hat.
NOP
NOP
Tut nichts und kostet einen Zyklus.
Klingt nutzlos, hat aber zwei echte Anwendungen: Erstens als Platzhalter, um
Code an eine bestimmte Adresse auszurichten. Zweitens als Decoy — als
Fülltext, der einen Scanner beschäftigt. Weil NOP sich vom leeren Core
unterscheidet, hält ein Scanner die Zelle für einen Fund und verschwendet seinen
Angriff auf eine Instruktion, deren Zerstörung nichts bewirkt.
Was der Assembler ergänzt
Schreibst du keinen Modifier, setzt der Assembler einen ein — nach festen Regeln, die vom Opcode und vom Adressierungsmodus des A-Operanden abhängen:
| Befehle | A ist # |
sonst |
|---|---|---|
DAT, NOP |
.F |
.F |
MOV, SEQ, SNE |
.AB |
.I |
ADD, SUB, MUL, DIV, MOD |
.AB |
.F |
SLT |
.AB |
.B |
JMP, JMZ, JMN, DJN, SPL |
.B |
.B |
Beispiele:
| Geschrieben | Wird zu |
|---|---|
MOV $1, $2 |
MOV.I |
MOV #1, $2 |
MOV.AB |
ADD $1, $2 |
ADD.F |
ADD #1, $2 |
ADD.AB |
SLT $1, $2 |
SLT.B |
JMP $1 |
JMP.B |
DJN #1, $2 |
DJN.B |
DAT #1, #2 |
DAT.F |
Zwei Stellen, an denen man sich leicht vertut: SLT fällt ohne # auf .B
zurück, nicht auf .F wie die übrigen Rechenbefehle. Und die Sprungbefehle
ignorieren den A-Modus vollständig — DJN #1, $2 wird .B, obwohl im
A-Operanden ein unmittelbarer Wert steht.
Empfehlung: Schreib den Modifier immer selbst hin. Nicht weil die Voreinstellungen falsch wären, sondern weil sie unsichtbar sind. Wer sich auf sie verlässt, sucht irgendwann lange nach einem Fehler, der in einem Zeichen steckt, das gar nicht dasteht.
Verwandte Seiten
- Adressierungsmodi — die acht Modi und ihre Nebenwirkungen
- Modifier — vollständige Semantik aller sieben Modifier
- Standards — Unterschiede zwischen '88, '94 und '94nop