2 Punkte von GN⁺ 2025-01-27 | 1 Kommentare | Auf WhatsApp teilen
  • Der Mac Classic II kam wegen eines Out-of-Range-Jump-Bugs im ROM beinahe dazu, auf eine falsche Adresse zu schreiben, aber die echte Motorola-MC68030-CPU führte eine undokumentierte Instruktion aus, änderte dabei das Register A1 und vermied so den Absturz
  • Das Problem wurde in der Classic-II-Emulation von MAME sichtbar, als 32-Bit-Adressierung aktiviert wurde und Sad Mac sowie Chimes of Death erschienen; mit 24-Bit-Adressierung sah der Start normal aus
  • Der ROM-Pfad InstallSoundIntHandler wandte beim Classic II BoxFlag 17 auf eine Verzweigungstabelle mit 16 Einträgen an und sprang zu 0x40A43B94; diese Position lag ursprünglich mitten in einer MOVEA.L-Instruktion
  • Die Bytefolge 0C EC 08 A9 00 04 ähnelte CAS D1,D2,$0004(A4), war aber eine ungültige CAS-artige Instruktion mit gesetzten reservierten Bits; auf einem echten 68030 änderte sie A1 von FFFF8FBA auf 40A4BBB2
  • Tests mit drei Custom-ROMs in einem echten Classic II bestätigten dies: Entfernte man die Instruktion per NOP, scheiterte auch die Hardware mit Sad Mac; bis das genaue Verhalten bekannt ist, umgeht MAME den Start per ROM-Patch

In MAME sichtbarer Startfehler des Classic II

  • MAME ist vor allem als Emulator für Arcade-Spiele bekannt, unterstützt aber auch die Emulation von Mac-Modellen auf 68000-Basis
  • Während in MAME die Funktion zum Aufruf des Debuggers per Tastenkombination command+power angepasst wurde, fiel ein seltsames Verhalten beim Classic II auf
    • Der Classic II besitzt eine physische Interrupt-Taste und außerdem den 68HC05-Mikrocontroller „Egret“, der Tastatur, Maus und anderes verarbeitet
    • In MacsBug gibt es Code, der einen Befehl sendet, um die Tastenkombination command+power zu aktivieren
  • Der Classic II startete mit 24-Bit-Adressierung normal, doch sobald die für das Laden von MacsBug nötige 32-Bit-Adressierung aktiviert wurde, erschienen Sad Mac und Chimes of Death
  • Laut Apple-Tech-Info-Library-Dokumentation bedeutet der Sad-Mac-Code 0000000F eine Ausnahme und 00000001 einen Busfehler
    • Bei 68k-Macs steht ein Busfehler meist für einen Zugriff auf eine falsche Adresse, etwa auf eine nicht vorhandene Erweiterungskarte

ROM-Analyse: Pfad zur Initialisierung des Sound-Interrupts

  • Im MAME-Debugger wurde ein Breakpoint auf die Busfehler-Handler-Adresse 0x40A026F0 gesetzt und der Aufrufpfad mithilfe der ROM-Symboltabelle verfolgt
    • Diese Adresse ist in der ROM-Map als GenExcps markiert
    • Die gemeinsame Fehlerbehandlungsroutine ist als ToDeepShit bezeichnet
  • Die Instruktion, die den Busfehler auslöste, war folgender Code bei 0x40A43B9C
move.b #$90, ($1c00,A1)
  • Dieser Code befand sich in der in der ROM-Map als InstallSoundIntHandler markierten Routine und hängt mit der Initialisierung des Sound-Interrupts des Classic II zusammen
  • Der Classic II ähnelt architektonisch eher der Macintosh-LC-Reihe als dem ursprünglichen Macintosh Classic und verwendet das EAGLE-Gate-Array, das dem V8-Chip des LC ähnelt
    • Daher tauchen im ROM-Code Namen wie V8SndIntPatch1 auf
    • EAGLE enthält Funktionen, die einer abgespeckten Variante des Apple Sound Chip (ASC) entsprechen

Fehlerhafte Verzweigungstabelle und das Problem mit Register A1

  • V8SndIntPatch1 prüft Hardware-Eigenschaften über den Gestalt-Trap-Selektor gestaltHardwareAttr, also 'hdwr'
    • Ist das Bit gestaltHasASC nicht gesetzt, kehrt die Routine zurück
    • Beim Classic II ist gestaltHasASC gesetzt
  • Danach liest der Code den Wert BoxFlag aus 0xCB3 im RAM und legt ihn in D0 ab
    • Beim Classic II ist BoxFlag gleich 0x11, also 17
  • Der problematische Verzweigungscode verdoppelt D0 und verwendet ihn dann als PC-relativen Jump-Offset
add.w d0,d0
jmp loc_40A43B72(pc,d0.w)
  • Da jede BRA.S-Instruktion 2 Byte lang ist, wird der BoxFlag-Wert verdoppelt und als Index in die Verzweigungstabelle verwendet
  • Tatsächlich enthielt die Tabelle aber nur 16 Einträge für BoxFlag 0 bis 15; der Wert 17 des Classic II lag außerhalb dieses Bereichs
    • Das berechnete Sprungziel war 0x40A43B72 + 0x22 = 0x40A43B94
    • Diese Position war nicht der Beginn einer beabsichtigten Instruktion, sondern lag mitten in einer MOVEA.L-Instruktion
  • Eigentlich hätte MOVEA.L eine gültige Adresse in A1 laden sollen, aber durch den Sprung in die Mitte der Instruktion blieb in A1 der Wert 0xFFFF8FBA stehen, der zuvor für die Berechnung des relativen Sprungs verwendet worden war
  • Wenn danach move.b #$90, ($1c00,A1) ausgeführt wird, ergibt sich als Schreibadresse 0xFFFF8FBA + 0x1C00 = 0xFFFFABBA, was auf dem Classic II eine ungültige Adresse ist

Undokumentiertes 68030-Verhalten

  • Die Bytefolge, an der der Out-of-Range-Jump landete, lautete wie folgt
0C EC 08 A9 00 04
  • IDA und GNU objdump konnten an dieser Stelle die Instruktion nicht sauber disassemblieren
  • Als dieselbe Bytefolge in MacsBug auf einem Macintosh IIci ausgeführt wurde, wurde sie ähnlich wie CAS.W D1,D2,$0004(A4) angezeigt, doch nach der Ausführung hatte sich A1 verändert
    • A1 änderte sich von 0xFFFF8FBA zu einem Wert, der wie eine RAM-Adresse aussah
    • Der neue A1-Wert schien vom ursprünglichen A1, A7 und dem Programmzähler abzuhängen
  • Laut dem Motorola M68000 Family Programmer’s Reference Manual sieht das erste Wort zwar wie eine CAS-Instruktion aus, aber im zweiten Wort sind drei Bits auf 1 gesetzt, die 0 sein müssten
  • Daher handelt es sich nicht um eine dokumentierte gültige CAS-Instruktion, sondern um eine fehlerhafte Instruktion mit gesetzten reservierten Bits
  • Ein echter 68030 behandelt diese Instruktion nicht als Illegal-Instruction-Ausnahme, sondern führt einen Read-Modify-Write-Buszyklus auf A4+4 aus
    • Setzt man A4 auf eine ungültige Adresse, zeigt MacsBug einen Busfehler während des Read-Modify-Write auf A4+4
    • Auch A1 wird verändert, obwohl eine gültige CAS-Instruktion A1 nicht verändern dürfte

Verifikation auf echter Classic-II-Hardware

  • Um die Hypothese zu prüfen, wurde ein 1991 gebauter Classic II gekauft und repariert; anschließend wurde das ROM durch programmierbare SST29EE010-EEPROMs ersetzt
    • Die SMD-Kondensatoren alter Macs können korrosives Elektrolyt auslaufen lassen, daher wurden sie vor dem Einschalten entfernt und repariert
    • Statt CRT und Analogplatine wurde das Logic Board separat betrieben und die Ausgabe mit einem Raspberry-Pi-Pico-basierten VGA-Konverter überprüft
  • In freien ROM-Bereichen wurde 68030-Assemblercode untergebracht, der den A1-Wert auf dem Bildschirm anzeigt, und daraus wurden drei Custom-ROMs erstellt
    • Custom ROM 1: Ersetzt MOVE.B an 0x40A43B9C, das in MAME den Sad Mac auslöste, durch den Code zur Anzeige von A1
    • Custom ROM 2: Ersetzt die CAS-artige Instruktion am Out-of-Range-Sprungziel 0x40A43B94 durch den Code zur Anzeige von A1
    • Custom ROM 3: Ersetzt die CAS-artige Instruktion an 0x40A43B94 durch NOP
  • Custom ROM 1 zeigte, dass dieser Code auch auf echter Hardware tatsächlich ausgeführt wird und dass A1 an der Stelle, an der MAME abstürzte, den Wert 0x40A4BBB2 hat
    • Dieser Wert ist eine ROM-Adresse und damit kein sinnvoller Schreibadressbereich, aber ein Schreibversuch darauf löst keinen Busfehler aus
  • Custom ROM 2 zeigte, dass A1 direkt vor dem Out-of-Range-Jump denselben Wert 0xFFFF8FBA hat, der auch in MAME beobachtet wurde
    • Der Out-of-Range-Jump tritt also tatsächlich auf
    • Das passt zur Hypothese, dass die anschließende CAS-artige Instruktion A1 verändert
  • Custom ROM 3 zeigte, dass auch ein echter Classic II mit Sad Mac scheitert, wenn die CAS-artige Instruktion entfernt wird
    • In diesem Test zeigte die Hardware denselben Sad Mac auch im 24-Bit-Modus
    • Dass MAME im 24-Bit-Modus keinen Busfehler für den falschen Schreibzugriff auslöste, war also ein toleranteres Verhalten als auf echter Hardware

Offene Probleme für MAME und die Emulation

  • Das entdeckte Verhalten ist ein Fall, in dem eine undokumentierte MC68030-Instruktion einen Read-Modify-Write-Buszyklus ausführt und sogar das Register A1 verändert
  • Der Bug im Classic-II-ROM blieb dank dieses Verhaltens verborgen, und das echte Gerät funktionierte normal
  • Dasselbe Codefragment wurde auch in einem neueren Macintosh-IIvx-ROM gefunden, dort wurde die Verzweigungstabelle aber vergrößert und der Classic-II-Fall springt direkt zu RTS
  • Wegen dieses Verhaltens gibt es derzeit möglicherweise keinen 100 % perfekten Emulator oder Nachbau eines Motorola MC68030
    • Man könnte mit dieser Instruktion und der Prüfung des resultierenden A1 einen kleinen Testcode schreiben, der einen physischen 68030 von einem Emulator unterscheidet
  • Bis das genaue Instruktionsverhalten geklärt ist, ermöglicht MAME den Start per Patch für den Classic-II-ROM-Bug
  • Unabhängig davon funktioniert die Tastenkombination command+power auf einem echten Classic II bei installierter MacsBug zwar, in MAME aber noch nicht

1 Kommentare

 
GN⁺ 2025-01-27
Hacker-News-Kommentare
  • Das beobachtete Verhalten des MC68030 könnte weniger eine von den CPU-Designern absichtlich geschaffene „echte“ Instruktion sein, sondern eher das Ergebnis davon, dass die interne CPU-Logik bei einer illegalen Instruktion zufällig mit Don’t-care-Eingaben weiterlief.
    Normalerweise sollte die CPU illegale Instruktionen erkennen und eine Exception auslösen, aber in manchen Fällen scheint das nicht zu passieren.
    Auf den Seiten 8–9 von https://www.nxp.com/docs/en/reference-manual/MC68030UM.pdf werden illegale Instruktionen anhand des Bitmusters im „ersten Word“ definiert.
    Diese Instruktion besteht aus 3 Words; das erste Word ist gültig, und die seltsamen Bits befinden sich im zweiten Word. Daher ist es sehr wahrscheinlich, dass der 68030 das zweite Word nicht validierte und direkt mit der Implementierungslogik der CAS-Instruktion fortfuhr.

    • Die Fake-CPU, die wir im Rechnerarchitekturkurs an der Uni gelernt haben, teilte den Instruktionscode in mehrere Bitfelder auf, wobei bestimmte Bits primitive Operationen wie einen Addierer aktivierten.
      „Gültige“ Instruktionscodes waren nur Kombinationen, die nützlich genug waren, um dokumentiert zu werden; „ungültige“ Kombinationen waren nutzlos oder bedeutungslos.
      So betrachtet sind illegale/Mystery-Instruktionscodes keine überraschende Ausnahme, sondern nahezu unvermeidlich.
    • Ich bin kein 68k-Experte, aber es sieht so aus, als würde nur das erste Word als Instruktionscode gelten, während das zweite Word dazu dient, die für CAS verwendeten D-Register auszuwählen.
      Normalerweise würde ich erwarten, dass die laut Dokumentation bedeutungslosen 0-Bits vollständig ignoriert werden.
      Beim 68030 müssen diese Bits jedoch offenbar 0 sein; ähnlich wie bei illegalen Opcodes des 6502 könnte es sein, dass beim Setzen dieser Bits fest verdrahtete Logik für eine andere Instruktion aktiviert wird.
  • CAS war schon immer ein Problemfall, und wahrscheinlich war es auch die Instruktion, zu der die meisten Emulations-Bugreports eingingen.
    Im alten King of Fighters gab es ebenfalls einen Bug, der das Carry-Flag der SBCD-Instruktion „falsch“ prüfte; das wurde für das Herunterzählen des Rundentimers und das Rundenende verwendet.
    Wenn man bei BCD-Arithmetik die arithmetischen Statusflags nicht emuliert, selbst wenn das Verhalten undokumentiert ist, läuft der Rundentimer in KOF immer weiter von 00 auf 99 und endet nie.
    SNK waren wirklich die Götter des 68000-Chips.

    • Als Retro-Arcade- und 68k-Fan würde ich gern mehr darüber hören, wie SNK den 68K bis an seine Grenzen ausgereizt hat.
  • Eine wirklich lange Reise.
    Heutzutage hätte ich nicht mehr die Geduld, so tief in den Kaninchenbau hinabzusteigen wie der Autor, aber das Erfolgserlebnis, das aus der Lücke zwischen „ich glaube, etwas zu wissen“ und „ich weiß es wirklich“ entsteht, kann ich sehr gut nachvollziehen.

    • Als Autor neige ich dazu, bei solchen Entdeckungen in eine ziemlich große Tiefenanalyse abzutauchen.
      Auch wenn es viel Zeit kostet: Wenn man bis zum Ende geht, ist es wirklich lohnend.
  • Selbst nach mehr als 30 Jahren erstaunt mich immer wieder, wie effektiv die Mac-Debugger-UI bei einer so geringen Bildschirmauflösung sein konnte.
    Da spürt man wirklich handwerkliche Meisterschaft.

  • Fast jede CPU hat undokumentierte Instruktionen, und 68k ist da keine Ausnahme.
    Allerdings konzentrierten sich damals die meisten Leute mit genügend Interesse und Low-Level-Wissen auf x86/PC; außerdem kann man sagen, dass x86/PC eine deutlich offenere und stabilere Architektur war als die Apple-Seite.
    Der Mikrocode des 8088 und 8086 wurde vor einigen Jahren disassembliert und intensiv untersucht, und meines Wissens gab es auch Versuche einer Simulation auf Transistorebene.
    Auch die Struktur des x86-Opcode-Raums wurde in Dokumenten wie den folgenden detailliert erforscht:
    http://ref.x86asm.net/geek.html
    https://gist.github.com/seanjensengrey/f971c20d05d4d0efc0781...
    Was diese Instruktion genau tut, ist noch unklar.
    In begrenzten Tests schien der Ergebniswert von A1 vom ursprünglichen A1-Wert, vom A7-Wert und vom Program Counter abzuhängen, aber sicher ist das nicht.
    Wenn jemand ein Programm schreibt, das verschiedene Registerwerte und Speicherinhalte ausprobiert, um das genaue Verhalten abzuleiten, könnte auch die Emulation genauer werden.
    Derzeit patcht MAME diesen Bug im ROM, damit der Classic II bootet, bis jemand es für wert hält, tiefer zu graben.
    Persönlich halte ich es für lohnend, das im Sinne einer exakten Emulation ausreichend aufzuklären.
    Ich bin mit 68k nicht besonders vertraut, aber die Bits der Instruktion geben Hinweise. Die Hypothese ist, dass die Bits 5:3 des zweiten Words wie ein weiteres Mode-Feld aussehen; statt mit Mode 000 ein Dn-Register auszuwählen, wählt 101 erneut (d16, An), und das Dc-Feld mit dem Wert 001 wird offenbar als A1 interpretiert.

    • Eine kleine Korrektur: 68k war natürlich keine Architektur von Apple.
      Der 68030 war damals eine ziemlich gute Allzweck-CPU und wurde auch im Amiga 3000, Atari Falcon, Sun 3, NeXT Cube und anderen Systemen eingesetzt.
    • Zu der Zeit, als der Mac Classic relevant war, war der PC im Home Computing noch nicht so überragend, und es gab auch nicht viele Leute, die sich auf x86/PC-Seite in solche Dinge vertieften.
      In der Demoszene hielt man sich weiterhin an 8-Bit-Heimcomputer, und wer 16-Bit-Heimsysteme nutzte, konzentrierte sich auf Atari und Amiga.
      Zu Hause setzten sich PC und x86 erst richtig durch, nachdem VGA und Soundkarten Teil der Standard-PC-Konfiguration geworden waren.
  • Als Kind hatte ich einen Amiga 2000 mit 68000-Prozessor.
    Als ich vom 68020 oder 68030 hörte, war ich völlig begeistert, und später ging es mir mit RISC-Architekturen genauso.
    Wenn man den Amiga mit roboterhafter Stimme Sätze wie „Hello, how are you?“ sagen ließ, waren meine Freunde so erstaunt, als wären sie gerade vom Mond zurückgekommen.
    Ich hätte mir nie vorstellen können, dass wir weniger als 40 Jahre später mit LLMs in natürlicher Sprache mit Computern sprechen und mit Python, VS Code und LLMs als Werkzeugen fast alles automatisieren würden, was wir wollen.
    Es ist wirklich eine verrückte Zeit.

  • Ich vermisse die 68000-Familie.
    Das waren wirklich großartige Chips.

    • Ich vermisse 68000-Assembly.
      Sie war klar und logisch, wie gute Ingenieursarbeit immer sein sollte, sodass man schon beim Überfliegen sofort verstehen konnte, was passiert.
      x86-Assembly wirkt dagegen meistens wie reiner Krimskrams, wie ein cleverer Trick, der nach einer schlimmen Nachtschicht entstanden ist.
      Ein Register von sich selbst zu subtrahieren, nur um 0 zu bekommen, ist wirklich zu viel.
  • Ich frage mich, ob das ein Kopierschutz war, der verhindern sollte, dass es auf bestimmten Systemen läuft, oder ob dieses Verhalten auf allen 68030 auftritt.

    • Dem Text nach halte ich es nicht für Kopierschutz.
      In der hier beteiligten Sprungtabelle fehlte der Eintrag für diese Maschine, und vermutlich haben die ROM-Entwickler einfach vergessen, den neuen Eintrag hinzuzufügen; rein zufällig funktionierte es offenbar auch ohne Tabelleneintrag.
    • Apple hatte bereits begonnen, geplante Obsoleszenz schrittweise auszuprobieren.
  • Auf heutigen Macs scheint so etwas künftig unmöglich zu sein.
    Apples technische Dokumentation ist heutzutage miserabel.

    • Ich denke, der Grund, warum Apple keine Hardware-Dokumentation auf diesem Niveau bereitstellt, ist, dass moderne Macs keine vergleichbare Erweiterbarkeit mehr haben.
      Anders als früher wird kein Systembus mehr über Steckverbinder offengelegt, in den Nutzer Karten einstecken und mit dem Drittanbieter-Entwickler direkt mit der Hardware interagieren müssten.
      Auf modernen Macs gibt es USB und Thunderbolt, mit denen Programme im Userspace umgehen können.
      Natürlich bestreite ich nicht, dass die Dokumentation einiger aktueller macOS-APIs sehr dürftig ist.
      Wenn man auf einer alten Seite landet, deren Header einen blauen Verlauf hat und auf der „Apple documentation archive“ steht, weiß man, dass man auf etwas Großartiges gestoßen ist.
    • In einer modernen Umgebung halte ich es für deutlich unwahrscheinlicher, dass ein solcher Bug in ausgelieferte Firmware oder Software gelangt.
      Der Adressraum ist viel größer, und die meisten Adressen sind nicht gemappt; eine Speicheroperation auf eine Mülladresse würde daher in der Regel fehlschlagen, und eine ungültige Instruktion vermutlich ebenfalls.
      Der Bug, eine Sprungtabelle mit einem Index außerhalb des gültigen Bereichs zu lesen, kann zwar auch in moderner Software auftreten, aber statt wie hier einfach weiterzulaufen, würde der Prozess eher abstürzen.
      Zum Vergleich: WebAssembly hat einen einzigen linearen Adressraum, und alle Adressen sind les- und schreibbar, daher sind Bugs dieser Art dort eher möglich.
      Wenn die Mülladresse innerhalb des Bereichs liegt, können fehlerhafte Reads, Writes oder CAS passieren, ohne dass es sofort auffällt.
      Allerdings würde eine ungültige Instruktion wie im Artikel dazu führen, dass das WASM-Modul nicht geladen wird; es ist also nicht 1:1 dasselbe wie dieses Problem im Mac-ROM.
  • Ich frage mich, ob auch 040/060 diese „undokumentierte Instruktion“ unterstützen.

    • Auf dem 040 scheint sie tatsächlich irgendetwas zu tun, an dem D1 beteiligt ist.
      A1 wurde überhaupt nicht verändert, und ich habe nicht weiter getestet, aber es könnte sein, dass sie einfach als normales CAS behandelt wird.
      Als ich diese Instruktion in MacsBug auf einem LC 475 zum ersten Mal im Single-Step ausgeführt habe, gab es einen Systemfehler, danach war es aber in Ordnung.
    • Auf einem Amiga mit 060 habe ich nachgesehen; diese Instruktion scheint kein A-Register zu verändern.
      Das war nur ein sehr schneller Test genau dieses Instruktionsworts.