1 Punkte von GN⁺ 2025-07-02 | 1 Kommentare | Auf WhatsApp teilen
  • Der Fehler mit den rotierenden Fässern in Donkey Kong Country 2 tritt im ZSNES-Emulator auf
  • ZSNES emuliert das Open-Bus-Verhalten nicht korrekt, wodurch sich die Fässer dauerhaft weiterdrehen
  • Anders als auf echter Hardware gibt ZSNES bei ungültigen Speicherzugriffen immer 0 zurück, was den Bug auslöst
  • Bei korrektem Verhalten stoppt die Logik die Drehung exakt in der richtigen Richtung von 8 Richtungen
  • Vermutlich geht das Problem auf einen kleinen Codierfehler zurück, nämlich die Verwendung von absoluter Adressierung statt unmittelbarer Adressierung

Donkey Kong Country 2 und der Fass-Bug im ZSNES-Emulator

Donkey Kong Country 2 hat einen bekannten Bug, bei dem die rotierenden Fässer in einigen Levels im alten SNES-Emulator ZSNES nicht korrekt funktionieren.

Wenn man in ein Fass springt, sollte es sich eigentlich nur so lange drehen, wie man die Richtungstaste links/rechts gedrückt hält. In ZSNES dreht sich das Fass jedoch selbst dann für immer weiter in diese Richtung, wenn links/rechts nur kurz angetippt wird.

Durch diesen Bug werden besonders in späteren Levels die Abschnitte mit rotierenden Fässern über Dornen oder Hindernissen deutlich schwerer, als es die Entwickler beabsichtigt hatten.

Das Problem war früher in den ZSNES-Foren teilweise dokumentiert, doch da das Forum inzwischen verschwunden ist, sind dazugehörige Informationen heute schwer zu finden.

Ursache des Bugs – Open-Bus-Emulation

Die eigentliche Ursache dieses Bugs ist, dass ZSNES das Open-Bus-Verhalten nicht emuliert.

  • Open Bus ist ein Verhalten, das auf älteren Plattformen wie dem SNES beim Lesen ungültiger Speicheradressen auftritt
  • Auf echter Hardware wird der zuletzt auf dem Bus liegende Wert zurückgegeben
  • Die Haupt-CPU des SNES ist der 65C816 (65816)
  • Der 65816 ist eine 16-Bit-Version des 6502, hat einen 24-Bit-Adressbus und verwendet Memory Banking

Im Code der rotierenden Fässer in DKC2 wird bei Zugriffen auf ungültige Adressen (Bank $B3, $2000 und $2001) auf echter Hardware über Open Bus der Wert 0x2020 zurückgegeben.

Da ZSNES diese Funktion nicht besitzt, wird dort immer 0 zurückgegeben, wodurch der Bug entsteht.

So funktioniert der Game-Code

Die Game-Routine für die rotierenden Fässer arbeitet ungefähr so:

  • Die aktuelle Fassrichtung und der Drehwert (die Geschwindigkeit) werden addiert und in einer temporären Variablen gespeichert
  • Per XOR wird die Richtungsänderung ermittelt, und das Ergebnis wird per AND mit dem über Open Bus gelesenen Wert verknüpft
  • Ist das Ergebnis dieses AND 0, läuft die Drehung weiter; ist es nicht 0, stoppt sie und die Richtung wird auf eine der 8 Richtungen gerundet und ausgerichtet

Auf echter Hardware ist der Open-Bus-Wert 0x2020. Würde stattdessen 0 zurückgegeben, würde die Drehung unendlich weiterlaufen.

Vermutlich sollte in dieser Logik die AND-Operation eigentlich mit einem unmittelbaren Wert (address #$2000) ausgeführt werden, doch versehentlich wurde absolute Adressierung (address $2000) verwendet.

Durch die Open-Bus-Eigenschaften der Hardware funktionieren in der Praxis jedoch beide Varianten korrekt.

Lösung und Fazit

Andere SNES-Emulatoren wie Snes9x haben diesen Bug per Hardcoding behoben, während ZSNES wegen eingestellter Entwicklung nicht gepatcht wurde.

Wenn man in der betreffenden Routine den Opcode des AND-Befehls von 0x2D auf 0x29 (AND #$2000) ändert, funktionieren die rotierenden Fässer auch ohne Open-Bus-Verhalten korrekt.

Auf echter Hardware oder in modernen Emulatoren tritt dieses Problem nicht auf.

Letztlich ist dieser Bug ein Beispiel dafür, wie fehlende Open-Bus-Emulation und ein Codierfehler zusammen ein Problem verursachen können.


Zusätzlicher Hintergrund: 65816-Architektur und SNES-Speicherkarte

Die 65816-CPU hat zwar einen 24-Bit-Adressbus, verwendet aber meist die Kombination aus 8-Bit-Bank und 16-Bit-Offset.

  • Der Program Counter (PC) ist 16 Bit breit, der vollständige Adressraum ergibt sich über das Program Bank Register (PBR, K)
  • Die Data Bank (DBR, B) dient zur Auswahl der Bank für Datenoperationen
  • Hardware-Stack und Direct Page liegen immer in Bank $00

Auch die Speicherbelegung des SNES basiert auf dem 65816, daher ist es effizienter, Adressen als 8-Bit-Bank plus 16-Bit-Offset zu betrachten.

Abschluss

Dieser Fall zeigt, dass Eigenheiten von Legacy-Hardware wie Open Bus in der Emulation zu unerwarteten Bugs führen können.

Die Entwickler hätten unmittelbare Adressierung verwenden sollen, doch hier funktionierte zufällig auch absolute Adressierung korrekt.

Heute deutet das darauf hin, dass selbst die Emulation von Open-Bus-Verhalten für die präzise Reproduktion älterer Software sehr wichtig ist.

1 Kommentare

 
GN⁺ 2025-07-02
Hacker-News-Kommentar
  • Als 6502-Assemblerprogrammierer habe ich unzählige Stunden damit verloren, aus Versehen das # wegzulassen und dadurch statt eines Immediate-Werts auf Speicher zuzugreifen. Solche Fehler sind besonders lästig, weil sie manchmal mit etwas Glück trotzdem zu funktionieren scheinen. Noch schlimmer als das Floating-Bus-Problem im Beispiel ist aber Code, der sich auf nicht initialisiertes RAM verlässt: Der Anfangswert ist je nach DRAM unterschiedlich, sodass es auf dem eigenen Rechner oder im Emulator immer läuft, auf einem anderen System mit anderem DRAM aber scheitert. Meist entdeckt man so etwas auf einer Demoparty, wenn weniger als 15 Minuten bleiben und der Code auf fremder Hardware plötzlich nicht mehr läuft

    • Ich frage mich, ob es auf dem 6502 tatsächlich Architekturen mit dynamischem Speicher gab. Meiner Erfahrung nach nutzten diese Plattformen immer nur statisches RAM

    • Der 6502 war meine erste Assemblersprache, und ich habe LDA #2 immer als „die Zahl 2 in das A-Register laden“ verstanden. LDA 2 fühlte sich dagegen eher an wie „den Wert aus Speicheradresse 2 laden“, und genau diese Unterscheidung half mir von Anfang an, solche Fehler zu vermeiden

    • In solchen Situationen kann es tatsächlich nützlich sein, den Code einmal durch ein LLM laufen zu lassen. Eine Stärke von LLMs ist, genau solche folgenschweren Tippfehler oder typische Fehlerstellen zu entdecken

  • Als ich „Open Bus“ großgeschrieben sah, hielt ich es erst für irgendein altes Busprotokoll oder einen Standard. Tatsächlich bedeutet es einfach, dass der Bus mit nichts verbunden ist, weil an der vom Adressdecoder gewählten Adresse ($2000) kein Speicherbaustein aktiviert wurde. Das Weglassen des Immediate-Modus (#) führte also dazu, dass effektiv nichts aus dem Speicher gelesen wurde; entdeckt wurde das, weil ein alter Emulator sich anders verhielt als die echte Hardware. Die Lösung bestand darin, die Anweisung auf Immediate-Adressierung umzustellen. Dann findet gar kein Speicherzugriff mehr statt und der Code wird um etwa 2 us schneller. Allerdings scheint so ein Leistungsunterschied außerhalb echter Hardware — besonders in Emulatoren ohne vollständig exaktes Timing — kaum relevant zu sein

    • Es wird erklärt, dass (einige) SNES-Emulatoren inzwischen nahezu zeitbasierte Perfektion erreicht haben. Ein Unterschied von 2 us fällt aber außer in extremen Sonderfällen praktisch nicht ins Gewicht. Verwandter Artikel: How SNES emulators got a few pixels from complete perfection

    • Es gibt mehrere Fälle, in denen Firmen wie Rare Spiele veröffentlichten, in denen Bugs jahrzehntelang verborgen blieben und erst mit neuerer Architektur sichtbar wurden. In Donkey Kong 64 tritt nach 8 bis 9 Stunden Spielzeit ein fataler Memory Leak auf; durch Save-States in Emulatoren lässt sich diese Zeit schlagartig akkumulieren, wodurch der Bug leicht sichtbar wird. Es gab zwar die Legende, das damals beigelegte Memory Pak habe den Bug nur kaschieren sollen, aber neuere Untersuchungen deuten darauf hin, dass weder Rare noch Nintendo den Fehler damals kannten

  • Ich bin in SNES Puyo Puyo auf ein PPU-Open-Bus-Phänomen gestoßen. Das war bei der Arbeit an der RunAhead-Funktion in RetroArch, als ich herausfinden wollte, warum Save-States nicht übereinstimmten. In diesem Sonderfall änderte sich nach dem Laden eines Zustands der aus dem PPU-Open-Bus gelesene Wert, wodurch die CPU-Trace-Logs nicht mehr identisch waren

  • Bei 6502 oder ähnlichem Code verwechsle ich oft Speicheradressen und Immediate-Werte. Notationen wie #$1234 halte ich für fehleranfällig; ich habe sogar gehört, dass Chuck Peddle diese Syntax zutiefst bereut haben soll. In der IDE konnte ich das teilweise verhindern, indem ich # rot hervorheben ließ. Sogar Rare-Entwicklern ist so ein Fehler passiert

    • Vor ziemlich langer Zeit hatte ich im GNU-Assembler mit intel_syntax noprefix ein ähnliches Problem: Beim Vorwärtsverweis auf eine benannte Immediate-Konstante gab es eine syntaktische Mehrdeutigkeit, sodass sie als Speicheradresse oder Symbol interpretiert werden konnte. Dadurch entstand unerwartet eine temporäre Speicheradresse, die sogar bis zum Link-Zeitpunkt des Symbols warten musste, und die Fehlersuche war wirklich unerquicklich

    • Befehlssätze wie ARM, die eigene Instruktionen für Speicherzugriffe haben, verhindern solche leicht verwechselbaren Fehler grundsätzlich

  • Soweit ich weiß, tritt das Open-Bus-Phänomen nur in frühen einfachen synchronen Bussystemen auf. Die meisten anderen Systeme geben bei Zugriffen auf nicht existente Adressen einen konstanten Wert wie nur Nullen oder nur Einsen zurück, oder das Bussystem behandelt den fehlenden Zugriff per Handshake, sodass der Master erkennen kann, dass niemand geantwortet hat (etwa master abort bei PCI)

  • Beim Programmieren des Parallax-Propeller-Chips ist mir ein ähnlicher Fehler wiederholt passiert. Ich verwechsle oft JMP #address und JMP address, wohl wegen meines 6502-Assembler-Muscle-Memory. Bei Propeller bedeutet JMP #address, zu der angegebenen Adresse zu springen, während JMP address zu dem Wert springt, der an dieser Adresse gelesen wurde. Das Problem ist, dass solche Bugs manchmal trotzdem funktionieren, sodass man stundenlang nach der Ursache sucht, bis das Verhalten irgendwann zusammenbricht

  • Open Bus bedeutet, dass die Datenleitungen des Busses tatsächlich offen sind. Wenn die CPU eine nicht gemappte oder schreibgeschützte Adresse auf den Bus legt, reagiert keine Hardware, und die Busleitungen bleiben in einem schwebenden Zustand — also Undefined Behavior auf Hardware-Ebene. Um zu verstehen, was tatsächlich passiert, muss man sich die physische Struktur des Datenbusses ansehen. Der Bus ist ein langer Leiter, der Signale zwischen Motherboard und Cartridge überträgt, und ist durch ein dünnes Isoliermaterial von der Massefläche getrennt. Diese Struktur wirkt wie ein Kondensator und hält dadurch die Spannung des zuletzt übertragenen Signals für eine gewisse Zeit fest. Deshalb liest man bei Open Bus oft effektiv den zuletzt übertragenen Wert erneut. Spiele wie DKC2 verlassen sich unabsichtlich auf dieses Open-Bus-Verhalten, und auch der serielle Controller-Port des NES treibt nur die niedrigen Bits; die höheren Bits bleiben Open Bus, weshalb manche Spiele bei LDA $4016 konkret $40 oder $41 erwarten. Das Open-Bus-Verhalten wird sogar in Speedrun-Strategien wie dem Super-Mario-World-Credits-Warp genutzt, etwa für Memory Corruption oder Arbitrary Code Execution. Nicht standardisierte Cartridges, Pull-up-/Pull-down-Widerstände oder ungewöhnliche Wechselwirkungen mit DMA (etwa Horizontal DMA) können aber Ausnahmen verursachen. Wenn zum Beispiel eine HDMA-Übertragung auf dem SNES mitten in einer Instruktion stattfindet, beeinflusst das das Timing eines Open-Bus-Reads; im Super-Metroid-Speedrun-Exploit kann dadurch ein abweichender Wert zwischen die zu kopierenden Speicherblöcke geraten und den Exploit zerstören. Deshalb kommt es auf Originalhardware oder in sehr präzisen Emulatoren zu Abstürzen, während die Strategie in den meisten Emulatoren oder offiziellen Neuveröffentlichungen funktioniert, weil diese das Nischenverhalten nicht vollständig nachbilden. Auch der TAS-Weltrekord-Run von Super Metroid hängt von diesem HDMA-Verhalten ab. Durch das Manipulieren der Gegnerpositionen wird das CPU-Timing so verschoben, dass HDMA den gewünschten Wert auf den Open Bus legt; am Ende werden Controller-Eingaben als Code ausgeführt, bis hin zu Arbitrary Code Execution Super Mario World credits warp video, HDMA-Anwendung video, Super Metroid DMA exploit video, Super Metroid TAS-Rekord

    • Die Videoserie von Ben Eater über seinen 6502-Breadboard-Computer hat mir sehr geholfen zu verstehen, wie solche Hardwaremechanismen funktionieren. Daran bekommt man ein gutes Gefühl dafür, wie sich solches Busverhalten in kommerziellen Geräten erweitert Ben Eater Website
  • Ich mag solche Inhalte zur Analyse interessanter Bugs sehr. Dem Assemblercode kann ich vielleicht nur zu etwa 60 % folgen, aber durch die begleitenden Erläuterungen im Text versteht man trotzdem viel. Besonders spannend finde ich solche Geschichten, in denen in legendärer Software nach sehr langer Zeit noch unbekannte Bugs entdeckt werden

    • Diese Systeme aus jener Zeit sind auch deshalb so faszinierend, weil ihnen die meisten Prüfmechanismen fehlten, die heute in Embedded-Systemen praktisch unverzichtbar sind — unabhängig davon, ob Netzwerkfähigkeit eine Rolle spielt oder nicht. In der NES-Ära bedeuteten viele Reads/Writes schlicht, die Spannung auf einer Leitung umzuschalten, und was genau passieren würde, wusste man erst in diesem Moment. Man erzielte gewünschte Effekte, indem man Spannungen mit präzisem Timing synchron zu den CRT-Blanking-Signalen toggelte, und in Super Mario Bros. 3 schaltete man etwa den RAM-Multiplexer um, um bei jedem Bildaufbau die Sprite-Bank zu wechseln. Weil die NTSC-/PAL-Unterschiede regionaler Fernsehsysteme mit ihrer Bildwiederholrate faktisch als Taktgeber für die Renderlogik dienten, musste man tatsächlich getrennte Softwareversionen für die jeweiligen Fernsehnormen veröffentlichen — eine wirklich wilde Zeit
  • Wenn ich ein Spiel im Emulator spiele und irgendwo nicht weiterkomme, denke ich immer: „Vielleicht ist es ein Emulator-Bug?“ Bei diesem Fall hätte ich wahrscheinlich einfach angenommen, dass das Spieldesign absichtlich so schwierig ist. Und wenn ein Spiel wirklich sehr schwer ist, frage ich mich auch oft: „Liegt das vielleicht an der Emulator-Latenz?“ Deshalb habe ich mir schließlich selbst ein MiSTer FPGA gebaut und benutze das

    • In Chrono Trigger gibt es eine Stelle, an der man vier Tasten gleichzeitig drücken muss. Weil USB-Eingaben nur drei gleichzeitig übermitteln konnten, wurde nur etwa jeder vierte Versuch registriert, was die Stelle extrem frustrierend machte

    • Ich habe DKC nur mit ZSNES gespielt, daher wusste ich bis zum Lesen des Artikels überhaupt nicht, dass das ein Emulator-Bug war. Ich dachte immer, das Spieldesign sei einfach absichtlich so schwer, und als ich erfuhr, dass es ein Bug war, war ich wirklich schockiert

    • Ich habe als Kind viel Bionic Commando gespielt, und als ich es im Emulator erneut spielte, kam es mir viel schwerer vor. Später stellte sich heraus, dass es an einem Emulator-Bug lag: Gegner verschwanden nicht, sodass man effektiv doppelt so viele Leben brauchte. Ich habe es auf diese Weise zwar einmal geschafft, aber noch einmal würde ich mir das nicht antun

  • Die auf SGI basierenden vorgerenderten 3D-Grafiken von DKC 1 waren damals Spitzentechnologie. Vector Man auf dem Mega Drive nutzte eine ähnliche Technik, bekam aber bei weitem nicht so viel Aufmerksamkeit wie DKC

    • 1995 gehörte ich genau zur Hauptzielgruppe von DKC (11 Jahre alt), und die Grafik dieses Spiels war wirklich schockierend beeindruckend. Um die Veröffentlichung herum bekam ich sogar ein Werbevideo, und das Tape mit den Behind-the-Scenes-Aufnahmen habe ich mehrmals angesehen. Besessen habe ich das Spiel selbst nicht, aber ich konnte es bei Freunden spielen

    • Als Kind hatte ich bei der Grafik von DKC immer das Gefühl, dass sie irgendwie „falsch“ oder künstlich war. Magazine erklärten damals oft ziemlich aufgesetzt, das SNES würde 3D-Charaktere in Echtzeit rendern, aber ich ahnte schon irgendwie, dass es eher wie eine Art Daumenkino-Animation funktionierte