- In einigen Abschnitten mit rotierenden Fässern in Donkey Kong Country 2 gibt es im alten SNES-Emulator ZSNES einen Bug, bei dem sich das Fass weiterdreht, selbst wenn man das Steuerkreuz loslässt; die Ursache ist die fehlende Implementierung des Open-Bus-Verhaltens
- Ein echtes SNES liest beim Zugriff auf nicht gemappte Adressen den zuletzt auf dem Datenbus liegenden Wert erneut, und DKC2 verlässt sich bei Lesezugriffen auf $2000/$2001 in Bank $B3 auf diese Eigenschaft
- Die problematische Routine XORt die vorherige und die neue Richtung des Fasses und führt dann
and $2000aus; auf echter Hardware liefert dieser 16-Bit-Open-Bus-Lesezugriff 0x2020 zurück und erkennt so das Überschreiten einer Richtungsgrenze - Wenn ein Open-Bus-Lesezugriff wie in ZSNES 0 zurückgibt, ist das AND-Ergebnis immer 0, sodass der Sprung zum Beenden der Rotation nicht ausgeführt wird und sich das Fass weiterdreht, bis man die Gegenrichtung drückt
- Der betreffende Befehl hätte sehr wahrscheinlich
and #$2000sein sollen; in einer ROM-Revision funktioniert es auch ohne Open Bus korrekt, wenn man an $33EDAC den Opcode von0x2Dauf0x29ändert
Rotierende Fässer stoppen in ZSNES nicht
- In Donkey Kong Country 2 gibt es einen alten Bug, bei dem einige rotierende Fässer in ZSNES nicht korrekt funktionieren
- Im Normalfall kann der Spieler die Rotation nur steuern, solange er im Fass die linke oder rechte Richtungstaste gedrückt hält
- In ZSNES dreht sich das Fass nach einem einzigen Druck auf links oder rechts dauerhaft in diese Richtung weiter, und bei Druck auf die Gegenrichtung rotiert es dann dauerhaft in die andere Richtung
- In späteren Abschnitten sind die Fässer über Stacheln oder anderen Gefahren platziert, wodurch dieser Bug den Schwierigkeitsgrad der Stage deutlich über die ursprüngliche Absicht der Entwickler hinaus erhöht
- Dasselbe Problem wurde offenbar schon vor rund 20 Jahren von Anomie gefunden und in Snes9x behoben; damals geschah der Fix nicht durch vollständige Open-Bus-Emulation, sondern durch Hardcoding der konkreten Adresswerte, von denen das Spiel abhängt
- In ZSNES wurde dieser Bug letztlich nie behoben, und der letzte Release des Projekts stammt aus dem Jahr 2007
Open Bus auf dem SNES und 65816-Adressierung
- Auf dem SNES führt selbst das Lesen ungültiger Speicheradressen normalerweise nicht zum Absturz des Programms
- Beim Lesen nicht gemappter Adressen tritt das Open-Bus-Verhalten auf, bei dem die CPU den zuletzt auf dem Datenbus befindlichen Wert erneut liest
- Die Haupt-CPU des SNES ist der 65C816 bzw. 65816 und steckt im Ricoh-5A22-S-CPU-Package
- Der 65816 ist eine 16-Bit-Erweiterung der 6502-CPU, und das SNES verwendet zwar einen 24-Bit-Adressbus, aber die meisten Adressen entstehen aus der Kombination einer 8-Bit-Bank und eines 16-Bit-Offsets
- Viele Befehle verwenden intern 16-Bit-Adressen; beim Instruction Fetch ist das Program Bank Register beteiligt, bei Datenzugriffen das Data Bank Register
- Im Zustand der rotierenden Fässer liest DKC2 $2000 und $2001 in Bank $B3; diese Adressen sind in Bank $B3 auf nichts gemappt und führen daher zu Open-Bus-Lesezugriffen
Speicherzugriffe der problematischen Routine
- Wenn man im rotierenden Fass die linke oder rechte Richtungstaste loslässt, führt die pro Frame laufende Routine einen Open-Bus-Lesezugriff aus
- Der Anfangszustand hat das Data Bank Register auf
$B3, die Direct Page auf$0000, und die M/X-Flags sind beide gelöscht, sodass Register und Speicherzugriffe 16 Bit breit sind - Diese Routine verwendet mehrere Adressen im Bereich
$0000-$2001$0EE6: aktuelle Fassrichtung$0E0A: Rotationsmenge pro Frame$0032: eine Position, die wie eine temporäre Variable wirkt
- In den Banks $00-$3F und $80-$BF ist
$0000-$1FFFauf die ersten 8 KB des 128-KB-WRAMs der Konsole gemappt $2000-$20FFist ein nicht gemappter Bereich, daher wird der Befehland $2000zu einem Open-Bus-Lesezugriff
Wie 0x2020 zur Erkennung des Rotationsendes verwendet wird
- Die Routine addiert die Rotationsmenge zur aktuellen Richtung, erzeugt so die neue Richtung und speichert sie in einer temporären Variable
- Danach XORt sie die vorherige und die neue Richtung, wendet auf das Ergebnis
and $2000an und prüft dann, ob das Ergebnis 0 ist - Auf echter SNES-Hardware liefert der 16-Bit-Open-Bus-Lesezugriff
and $2000immer 0x2020 zurück- Der Maschinenkode von
and $2000ist2D 00 20 - Der 65816 verwendet einen 8-Bit-Datenbus und führt einen 16-Bit-Lesezugriff daher als zwei 8-Bit-Lesezugriffe aus
- In diesem Fall ist das zuletzt gelesene Byte das High-Byte der Adresse, also
0x20, weshalb beide Male0x20zurückgegeben wird
- Der Maschinenkode von
- Dadurch entspricht das tatsächliche Verhalten funktional
and #$2020 - Wenn das AND-Ergebnis 0 ist, speichert die Routine die neue Richtung unverändert und setzt die Rotation im nächsten Frame fort
- Ist es nicht 0, setzt sie die Rotationsmenge auf 0, addiert
0x1000zur neuen Richtung und maskiert anschließend mit0xE000, um auf die nächstgelegene Richtung auszurichten, die ein Vielfaches von0x2000ist
Warum es sich in ZSNES endlos weiterdreht
- Der Richtungswert des Fasses scheint so skaliert zu sein, dass
0x0000nach unten und0x4000nach links zeigt - Beim analysierten Fass beträgt die Rotationsmenge im Uhrzeigersinn
0x0300, gegen den Uhrzeigersinn0xFD00, und eine volle 360-Grad-Drehung dauert etwas mehr als 85 Frames - Das ist bei 60 fps etwas kürzer als 1,5 Sekunden
- Beim AND mit
0x2020ist die tatsächlich relevante Änderung Bit 13, also 0x2000 - Eine Änderung von
0x2000im Richtungswert entspricht einem Schritt zur nächsten Grund- oder Diagonalrichtung - Auf korrekter Hardware dreht sich das Fass nach dem Loslassen der Richtungstaste bis zur nächsten Grund- oder Diagonalrichtung weiter und stoppt dann exakt in dieser Ausrichtung
- Wenn ein Open-Bus-Lesezugriff dagegen immer 0 zurückgibt, ist auch das AND-Ergebnis immer 0 und der Code zum Beenden der Rotation wird nie ausgeführt
Möglicher Tippfehler und Ergebnis eines ROM-Patches
and $2000ist ein Befehl mit absoluter Adressierung, und logisch hätte es wahrscheinlichand #$2000mit Immediate-Adressierung sein sollen- Auf echter Hardware liefert Open Bus jedoch
0x2020, sodassand $2000zufällig wie beabsichtigt funktioniert - Dieses zufällige Verhalten hängt von der Bedingung ab, dass die unteren 6 Bits der Rotationsmenge pro Frame immer 0 sind, wodurch
0x2020funktional mit0x2000gleichgesetzt wird - Der fehlerhafte Opcode wird bei Bank
$B3, Offset$EDACausgeführt und ist in der analysierten Revision innerhalb der 4-MB-ROM des Spiels auf$33EDACgemappt - Wenn man dieses Byte von
0x2Dauf0x29ändert, funktionieren die rotierenden Fässer auch dann korrekt, wenn Open-Bus-Lesezugriffe immer 0 zurückgeben - Die genaue ROM-Position kann sich in anderen Revisionen des Spiels unterscheiden
- In fast allen SNES-Emulatoren außer altem ZSNES funktioniert das Spiel korrekt, daher ist dieser Fall weniger ein praktischer Patch als vielmehr eine Analyse hardwareabhängigen Verhaltens
1 Kommentare
Meinungen auf Hacker News
Beim Schreiben von 6502-Assembly habe ich wegen genau solcher Fehler – etwa das
#vor einem Immediate-Wert zu vergessen und dadurch auf Speicher zuzugreifen – eine Menge Lebenszeit verloren.Wie in diesem Fall funktioniert es zufällig in manchen Situationen trotzdem. Noch schlimmer ist es, wenn man sich statt auf einen Floating Bus auf nicht initialisierten RAM verlässt: Wegen der Eigenschaften von DRAM kann es auf der eigenen Maschine oder im Emulator immer funktionieren, auf der Maschine eines anderen mit anderen DRAM-Chips aber kaputtgehen. Meist merkt man das dann auf einer Demoparty 15 Minuten vor der Präsentation, wenn es auf der Party-Maschine nicht läuft.
LDA #2immer als „lade die Zahl 2 in A“ undLDA 2als „lade den Wert an Speicheradresse 2 in A“ unterschieden.Weil Open Bus im Titel großgeschrieben war, begann ich zunächst zu lesen, weil ich dachte, es sei der Eigenname eines alten Busprotokolls oder Standards, von dem ich noch nie gehört hatte.
Beim Lesen stellte sich heraus, dass der Adressleitungs-Decoder bei der angegebenen Adresse
$2000kein Speichergerät aktiviert und der Bus daher „offen“ ist, also mit nichts verbunden. Ziemlich witzig ist, dass das vergessene#bei der Immediate-Adressierung erst dadurch auffiel, dass ein alter Emulator Speicherlesezugriffe nicht wie echte Hardware behandelte. Wenn man als Fix statt absoluter Adressierung Immediate-Adressierung verwendet, erfolgt kein Speicherlesezugriff, also sollte die Ausführung auch schneller werden. In diesem Codeblock könnten es ungefähr 2 µs sein, aber das ist vermutlich nur auf Bare Metal relevant; Emulatoren werden ohnehin wahrscheinlich keine vollständig exakte Timing-Genauigkeit haben.In Donkey Kong 64 gab es meines Wissens ein Speicherleck, durch das das Spiel nach 8 bis 9 Stunden starb – einer für damalige Verhältnisse unrealistisch langen ununterbrochenen Spielzeit. Während der Entwicklung wurde es nicht gefunden, aber wenn man Emulator-Savestates nutzt und statt der In-Game-Speicherfunktion einfach weiterspielt, summiert sich die Zeit schnell. Die Geschichte ist allerdings unklar. Manche Quellen behaupten, die Beilage des Memory Pak sei eine Last-Minute-Maßnahme gewesen, um den Bug zu kaschieren, indem die Crash-Zeit von 8–9 Stunden auf 13–20 Stunden hinausgeschoben wurde. Neuere Untersuchungen deuten jedoch eher darauf hin, dass das Zufall war und Rare oder Nintendo den Bug nicht wissentlich veröffentlicht haben.
[0] https://arstechnica.com/gaming/2021/06/how-snes-emulators-go...
Als ich an RetroArchs RunAhead-Funktion arbeitete und prüfte, ab wann Savestates nicht mehr übereinstimmten, habe ich einmal gesehen, dass SNES Puyo Puyo den PPU Open Bus benutzt.
Nach dem Laden eines States war der aus dem PPU Open Bus gelesene Wert anders, sodass die Trace-Logs der CPU-Ausführung nicht mehr übereinstimmten.
Ich mache nicht ständig Fehler der 6502-Art, aber wenn, dann ist es meist der Fehler, statt eines Immediate-Werts eine Speicheradresse zu verwenden.
Das ist ein sehr häufiger und leicht zu begehender Fehler, und ich glaube, sogar Chuck Peddle selbst hat die Syntax, Immediate-Werte mit
#wie in#$1234zu kennzeichnen, zutiefst bereut. In der IDE habe ich#hellrot eingefärbt; das hilft ein wenig. Selbst die Assembly-Götter bei Rare sind auf dasselbe Problem hereingefallen.intel_syntax noprefixdes GNU Assemblers ein ähnliches Problem.Bei Instruktionen, die sowohl Immediate-Werte als auch Speicheradressen akzeptieren, gab es eine syntaktische Mehrdeutigkeit, bei der ein vorwärts referenzierter benannter Immediate-Konstantenwert als Referenz auf ein unbekanntes Symbol interpretiert werden konnte. Das Ergebnis war, dass nicht der erwartete Immediate-Wert assembliert wurde, sondern eine Instruktion mit einer Platzhalter-Speicheradresse, die beim Linken mit der Relocation-Adresse des Symbols gefüllt werden sollte. Das Debugging war qualvoll.
Ich mag solche Artikel. Assembly-Code kann ich gefühlt immer nur zu etwa 60 % nachvollziehen, daher hilft die danebenstehende Prosabeschreibung wirklich sehr.
Es macht auch Spaß, von Bugs in klassischer Software zu hören, die niemand verstanden hat oder die vielleicht bis heute niemand bemerkt hatte.
Gemeint sind Mechanismen, die für Systeme mit Netzwerkanschluss unverzichtbar sind und inzwischen billig genug, um sie sogar in vollständig isolierte Embedded-Architekturen einzubauen. Beim ursprünglichen NES bestanden viele Lese- und Schreibzugriffe lediglich darin, auf irgendeiner Leitung eine Spannung umzuschalten, und dann geschah eben, was geschah. Den gewünschten Effekt erzielte man, indem man die Spannung auf sehr kontrollierte Weise exakt passend zu einem Signal umschaltete, das den CRT-Blanking-Abschnitt anzeigte. Einige Animationen in Super Mario Bros 3 schalteten einen RAM-Multiplexer um, sodass eine von mehreren Sprite-Datenbanken ausgewählt wurde und die Grafikhardware beim Holen der Sprites aus einem völlig anderen Chip las, in dem sie etwas anders aussahen. Weil das TV-Timing wichtig war, musste Software auch separat für NTSC- und PAL-TV-Regionen erscheinen; beide Verfahren haben unterschiedliche Bildwiederholraten, und diese Bildwiederholrate war der Takt, der die Rendering-Logik antrieb. Es war wirklich eine wilde Zeit.
Soweit ich weiß, sieht man Open Bus nur bei frühen Systemen mit einfachen synchronen Bussen.
Bei den meisten anderen Systemen bekommt man beim Zugriff auf eine nicht vorhandene Adresse meines Wissens einen konstanten Wert, der nur aus Nullen oder nur aus Einsen besteht. Der Grund ist, dass das Busprotokoll ein Handshake hat, über das der Master erkennen kann, dass es keine Antwort gibt; in PCI-Terminologie entspricht das einem „master abort“.
Open Bus bedeutet wörtlich, dass die Datenbus-Leitungen ein offener Stromkreis sind.
Die CPU hat eine Adresse auf den Adressbus gelegt, die entweder nicht gemappt oder nur schreibbar ist, und da keine Hardware am Bus reagiert, werden die Busleitungen nicht getrieben und bleiben schwebend. Formal ist das undefiniertes Verhalten auf Hardwareebene.
Um zu verstehen, was tatsächlich passiert, muss man sich die physische Struktur des Datenbusses etwas genauer ansehen. Es gibt lange Leiterbahnen, die Signale über das Mainboard und zum Cartridge übertragen, getrennt von der Massefläche durch eine dünne isolierende Leiterplattenschicht. Das sieht wie ein Kondensator aus, und tatsächlich beschreiben und modellieren Ingenieure das als parasitäre Kapazität. Dieser Effekt begrenzt die maximale Datenübertragungsrate des Busses, weshalb man versucht, ihn zu minimieren. Gerade wegen dieses Effekts neigt der Bus aber dazu, auf der zuletzt getriebenen Spannung zu verharren, wenn er nicht mehr aktiv getrieben wird. Er verhält sich wie eine kleine DRAM-Zelle, wodurch der im Artikel beschriebene Effekt entsteht: „Ein Open-Bus-Read gibt den Wert zurück, der zuletzt über den Bus gegangen ist.“
Dass ein Spiel wie DKC2 versehentlich von Open-Bus-Effekten abhängt, ist nicht ungewöhnlich. Beim NES treibt das serielle Port-Register für den Controller-Anschluss nur die unteren Bits; die oberen Bits sind Open Bus. Es gibt einige Spiele, die mit
LDA $4016die Controller-Eingabe lesen und dabei den Wert$40oder$41erwarten. Die 4 stammt dabei von dem Wert, der wegen des Open Bus noch übrig ist.Es gibt auch Speedrun-Strategien, die im Rahmen von Speicher-Korruption oder Arbitrary-Code-Execution-Exploits vom Open-Bus-Verhalten abhängen. Beim Credits Warp in Super Mario World etwa wird der Program Counter in nicht gemappten Speicher geschickt, läuft dort eine ganze Weile weiter und landet schließlich im RAM, wo ein durch präzise manipulierte Gegnerpositionen erzeugter Payload ausgeführt wird [1].
Allerdings gibt es auch Ausnahmen vom normalerweise vorhersagbaren Open-Bus-Verhalten. Nicht standardkonforme Cartridges können für nicht gemappten Speicher Standardwerte zurückgeben oder Pull-up-/Pull-down-Widerstände enthalten, die das Open-Bus-Verhalten beeinflussen. Außerdem gibt es interessante Wechselwirkungen mit DMA. Das SNES unterstützt eine Funktion namens HDMA, mit der Anwendungen genau getaktete Datenübertragungen von der CPU zur Grafikhardware einplanen können, um mitten im Frame Daten hochzuladen oder Einstellungen zu ändern [2]. Diese DMA-Transfers halten die CPU kurz an, um den Bus für die Übertragung zu nutzen. Wenn ein DMA-Transfer mitten in die Ausführung einer Instruktion fällt – also nachdem die Zieladresse gelesen wurde, aber vor dem eigentlichen Open-Bus-Read –, kann er das Verhalten dieses Open-Bus-Reads verändern.
Dieser sehr spezielle Grenzfall hat großen Einfluss auf einen Super-Metroid-Speedrun-Exploit [3]. Der Exploit löst ein
memcpyaußerhalb des gültigen Bereichs aus, um einen großen Datenblock vom Open Bus in den RAM zu kopieren. Open-Bus-Reads geben fast immer 0 zurück, weil das letzte Byte der relevanten Load-Instruktion 0 ist. In bestimmten Räumen mit vielen HDMA-Grafikeffekten ist es jedoch durchaus möglich, dass ein DMA-Transfer einen dieser Reads beeinflusst. Dann landet ein Nicht-Null-Byte an einer kritischen Stelle, der Exploit funktioniert nicht korrekt und das Spiel stürzt ab. Das hat in der Community eine kleine Debatte ausgelöst. Manche Routen und Strategien sind nur auf Emulatoren und nicht standardkonformer Firmware stabil. Spieler auf Originalhardware oder sehr genauen Emulatoren haben ein höheres Crash-Risiko; die meisten Emulatoren, einschließlich aller offiziellen Neuveröffentlichungen von Nintendo, emulieren diesen speziellen Grenzfall aber nicht, bei dem ein HDMA-Transfer mitten in einer Instruktion den Wert eines Open-Bus-Reads verändert.Auch der derzeit schnellste Super-Metroid-TAS-Clear [4] hängt von dieser HDMA-Wechselwirkung ab. Man hatte Situationen gefunden, in denen der Versuch, Open Bus auszuführen, zu einem Crash führte, aber normalerweise ließ sich das nicht sinnvoll kontrollieren. Durch Manipulation der Gegner im Raum konnte das CPU-Timing beeinflusst werden, und über HDMA ließ sich zum richtigen Zeitpunkt eine nützliche Instruktion auf den Bus legen. Am Ende wurde die Konsole dazu gebracht, Controller-Eingaben als Code auszuführen, wodurch vollständige Arbitrary Code Execution erreicht wurde.
[1]: https://youtu.be/vAHXK2wut_I
[2]: https://youtu.be/K7gWmdgXPgk
[3]: https://youtu.be/CnThmKhtfOs
[4]: https://tasvideos.org/8214S
Dank seiner Videoserie über den Bau eines Breadboard-Computers mit dem 6502 verstehe ich den Inhalt des Artikels und die Beschreibung der Hardwareprobleme tatsächlich. Natürlich ist das gedanklich eine Übertragung seiner einfachen Bus-Beispiele auf eine kommerzielle Maschine. Ohne das wüsste ich fast nichts darüber.
https://eater.net
Beim Programmieren des Parallax-Propeller-Chips gibt es bis zu einem gewissen Grad ein ähnliches Problem.
Man muss
JMP #addressverwenden, um zu der angegebenen Speicherposition zu springen, schreibt aber ständigJMP address, was zu der Adresse springt, die aus der angegebenen Speicherposition gelesen wurde. Vermutlich steckt der 6502-Assembler noch im Muskelgedächtnis.Propeller:
JMP #address6502:
JMP addressPropeller:
JMP address6502:
JMP (address)Das Schlimmste ist, dass fehlerhafter Propeller-Code wie in diesem Artikel manchmal trotzdem funktioniert. Bis er irgendwann stehen bleibt und man Stunden damit verbringt herauszufinden, warum.
Die mit SGI vorgerenderten 3D-Grafiken von DKC 1 waren damals hochmodern. Vector Man auf dem Genesis machte etwas Ähnliches, bekam aber weniger Aufmerksamkeit.
Ich konnte kaum glauben, was ich da sah. Zur Veröffentlichung gab es eine Videokassette, die das Spiel anteaserte und auch Blicke hinter die Entwicklung zeigte; ich erinnere mich, dass es wohl Werbematerial war, das man über so etwas wie eine Müslipackung anfordern konnte. Ich habe mir dieses Tape wirklich oft angesehen. DKC habe ich selbst nie besessen, konnte es aber bei einem Freund spielen.
Die Magazine jener Zeit formulierten das Thema ziemlich vage und deuteten oft an, die Leistung des SNES würde Figuren und Ähnliches in Echtzeit rendern. In Wirklichkeit war es im Kern eher eine Art Daumenkino-Animation.
Wenn man beim Spielen per Emulation nicht weiterkommt, fragt man sich am Ende oft, ob es nicht doch ein Emulator-Bug ist.
Bei diesem konkreten Problem hätte ich gedacht, dass das Spiel einfach so designt wurde und eben schwierig ist. Es ist nicht direkt dasselbe, aber wenn sich ein Spiel wirklich schwer anfühlt, denke ich ähnlich: „Liegt das an der Emulationslatenz?“ Nachdem ich mich wegen dieses Problems tiefer damit beschäftigt hatte, habe ich mir schließlich selbst ein MiSTer FPGA gebaut.
Ich erinnere mich an eine Stelle, an der man nach dem Fangen einer Ratte vier Tasten gleichzeitig drücken musste. USB-Eingaben übertrugen aber nur drei auf einmal, sodass man, um vorbeizukommen, die vier Tasten wild drücken musste, damit sie innerhalb eines sehr kurzen Zeitfensters schließlich alle registriert wurden. Man brauchte mehrere Versuche, und es war sehr frustrierend.
Wie gesagt dachte ich einfach, es sei beabsichtigtes Game Design, beim Abschuss aus dem Fass das Timing so zu treffen, dass der richtige Winkel entsteht. Ich war wirklich überrascht zu erfahren, dass es ein Bug war.
Als ich es Anfang der 2000er in einem Emulator startete, war es viel schwieriger, als ich es in Erinnerung hatte. Später stellte sich heraus, dass es einen Emulations-Bug gab, bei dem die Gegner auch nach der Sprengung der Basis nicht verschwanden, und Ladd blieb weiterhin eingefroren. Dadurch brauchte man zum Abschließen des Levels ungefähr zwei Lebensbalken mehr. Einmal habe ich es so geschafft, nur um zu sehen, ob es möglich ist, aber danach nie wieder.