Der Casio CFX und Assembler
Assembler durch ROM Manipulation
Ich will mich gar nicht lange mit Erklärungen aufhalten: welche Vorteile es hätte, den Casio CFX direkt in Assembler programmieren zu können, liegt auf der Hand wenn man weiß, dass die Hardware dieses Rechners durchaus mit der eines Game Boys vergleichbar ist, und dass einzig das Casio Basic schuld an der langsamen Ausführung von Taschenrechnerprogrammen ist (mehr dazu bei den Programmiertips und im Kapitel Die Hardware des Casio CFX).
Grundsätzlich besteht NATÜRLICH die Möglichkeit, Maschinencode für die CPU des GTR in den RAM einzuschleusen, indem man einfach das RAM Image des manipuliert und dann als Backup zurück auf Rechner überspielt (siehe Die optimale Art, den Casio CFX zu hacken). Allerdings hat man dann das Problem, dass sich dieser nicht ausführen lässt, denn das Betriebssystem des Casio CFX sieht eine solche Möglichkeit nicht vor! Zwar könnte man den Assemblercode auch als normales Casio Basic Programm tarnen und in die reguläre Programmliste packen, das hätte aber wenig Sinn, da die CPU diese Programme ja nicht direkt ausführt, sondern sie nur vom Interpreter eingelesen werden. Maschinencode in Casio Basic Programmen würde also nicht auf der CPU ausgeführt werden, sondern lediglich einen Syn ERROR verursachen.
Das Problem ist einfach, dass das Betriebssystem NIE einen Sprung in den RAM Bereich des Rechners ausführt, sondern dass die Programmkontrolle immer im ROM bleibt, es also durch bloße RAM Manipulation gar nicht möglich ist, die CPU dazu zu bringen, Assemblercode im RAM auszuführen.
Hier müssen schon schwerere Geschütze aufgefahren werden: theoretisch kann man das Betriebssystem des Rechners so manipulieren, dass es unter bestimmten Umständen (z.B. bei Auswahl eines eigens kreierten Assembler Mode - Menüpunktes im Hauptmenü) einfach an eine bestimmte Stelle im RAM springt - die Ausführung von Assemblercode im RAM wäre dann natürlich problemlos möglich. Allerdings setzt das eben voraus, dass man:
- Den ROM Chip des Rechners ausbaut und im PC einliest
- Das enthaltene Betriebssystem in der entsprechenden Art und Weise modifiziert (was wiederum gute Kentnisse über die Maschinensprache des Rechners voraussetzt, von der allerdings scheinbar keine Dokumentationen existieren)
- Das modifizierte System auf einen neuen ROM Chip brennt und
- zu guter letzt diesen wieder in den Rechner einlötet
Das hat zwar tatsächlich schon mal jemand getan: Martin Poupe (mehr dazu auf seiner Homepage). Aber ich will natürlich nicht bestreiten, dass Aufwand und Nutzen hierfür in keinem Verhältnis stehen, und das es wohl eher die Ausnahme bleiben wird, das jemand den ROM seines GTR modifiziert.
Assembler ohne ROM Manipulation
Vor einiger Zeit hatte ich aber eine Idee, die es eventuell DOCH ermöglichen könnte, selbstgeschriebenen Assemblercode im RAM - Bereich des Rechners auszuführen, und zwar ganz OHNE Manipulation des Casio CFX ROMs, nämlich indem man den Stack zur Laufzeit verändert!
Zunächst einmal: ich meine nicht den Stack, den der Casio Basic Interpreter benutzt (auch dieser richtet sich nämlich im RAM eine Art "Hilfsstack" ein, in dem z.B. die Reihenfolge von Operatoren bei Termen, oder welche Anweisungen und Schleifen noch offen sind usw., vermerkt wird), sondern den, den die CPU selbst verwendet.
Die Idee ist nun folgende:
Zwar ist über die CPU des Casio CFX wenig bekannt, aber man kann trotzdem mit Sicherheit davon ausgehen, dass sie einen Stack verwendet, denn erst der Stack ermöglicht prozedurale Programmierung (d.h., das Aufrufen bestimmter Subroutinen aus jeder Stelle im Programmcode heraus mit call und ret um gleichartige Codesequenzen nicht mehrfach implementieren zu müssen). Stacks haben bereits seit dem Erscheinen der ersten 8 Bit CPU alle Prozessoren unterstützt (das wird also sicher auch bei der Casio CFX CPU der Fall sein), und es ist auch kaum vorstellbar, dass das doch relativ komplexe Betriebssystem des Rechners ohne eine einzige Subroutine auskommen kann.
Was tut der Stack aber eigentlich? Im Stack wird beim Aufruf einer Subroutine durch einen call - Befehl die Adresse vermerkt, wohin die CPU nach der Ausführung der entsprechenden Subroutine zurückkehren soll (üblicherweise das erste Byte, dass dem call - Befehl folgt; das geschieht durch einen ret - Befehl). Der Stack muss dazu natürlich beschreibbar sein, denn sonst könnten die Rücksprungadressen ja nicht eingetragen werden. Und außer dem Speicher des Displayadapters (der nämlich über einen separaten RAM verfügt) gibt es beim Casio CFX nur noch einen beschreibbaren Speicher: Das 32KB RAM - Modul!
Ich denke also, dass man mit ziemlicher Sicherheit davon ausgehen kann, dass das Betriebssystem direkt nach dem Einschalten einen Teil des RAMs als CPU Stack initialisiert (wahrscheinlich 704B im Adressbereich 0000..02BF für ZX933 - bzw. 735B von 0000..310 für GY359 Systeme; siehe Die Casio CFX Memory Map), in dem dann Rücksprungadressen gespeichert werden, wenn das Betriebssystem seine Subroutinen im ROM aufruft.
Nun kann man hoffen, dass auch der Casio Basic Interpreter als eigene Subroutine des Betriebssystems umgesetzt ist (genau kann man ja nicht wissen, wie die Casio - Leute das System programmiert haben). Denn WENN folgende beiden Bedingungen gelten:
- Die CPU des Rechners richtet tatsächlich einen Stack innerhalb des 32KB RAM Moduls für sich ein und
- Der Casio Basic Interpreter wird innerhalb des Betriebssystems durch eine eigene Subroutine aufgerufen (am besten durch eine Art far call, also intersegmentär),
was wahrscheinlich ist, dann besteht DEFINITIV die Möglichkeit, Assemblercode im RAM - Bereich des Rechners ohne vorherige Modifikationen des ROMs auszuführen.
Und zwar so:
Wenn obige Bedingungen gelten, dann passiert beim Start eines Casio Basic - Programms nämlich folgendes: das Betriebssystem ruft die Prozedur des Casio Basic Interpreters auf der das Basic Programm verarbeiten soll, und vermerkt dabei im Stack die ROM Adresse, zu der der Interpreter nach getaner Arbeit zurückkehren soll. Wird nun das Basic Programm beendet, wird auch der Interpreter beendet und die CPU führt einen ret - Befehl aus, springt also zur im Stack angegebenen Adresse zurück.
Was würde jetzt aber passieren, wenn das Basic Programm während seiner Laufzeit die Rücksprungadresse im Stack einfach verändern, z.B. durch eine Adresse, die an irgend eine Stelle des RAM zeigt, ersetzen würde? Die CPU würde dann zu eben dieser Adresse springen, anstatt zurück an die Stelle im ROM, von wo aus der Casio Basic Interpreter aufgerufen wurde!
Und grundsätzlich ist es einem Casio Basic Programm durch vorherige Manipulation des RAM - Images ja möglich, an JEDE Stelle im RAM zuzugreifen: Zwar nützt es nichts, den Stack bereits direkt in einem RAM Image zu verändern und dieses auf den GTR zu übertragen, denn er wird ja durch die CPU gleich wieder überschrieben; die Manipulation des Stacks muss daher unbedingt zur Laufzeit einer Subroutine, also zwischen dem call - und dem entsprechenden ret - Befehl erfolgen. Aber da der RAM des Casio CFX dynamisch verwaltet wird, und Listen, Matrizen, Programme usw. im Speicher keine feste Adresse haben, kann man durch Manipulation eines entsprechenden Eintrages der Grow - Up - Pointertabelle irgend einer Variablen, beispielsweise A, genau DIE Adresse im RAM zuzuweisen, an der später die Rücksprungadresse für den Casio Basic Interpreter vermerkt sein wird (denn diese wird immer an der selben Stelle im RAM auftauchen, da der Stack seinen festen Platz hat!). Weist man der Variablen A dann während der Ausführung eines Basic Programms einem beliebigen Wert zu, wird die Rücksprungadresse im Stack durch den Inhalt der Variablen überschrieben, und der Casio Basic Interpreter kehrt nach der Ausführung nicht mehr zu der Stelle im Betriebssystem zurück, von wo aus er aufgerufen wurde, sondern - je nach dem Inhalt von A - irgendwo anders hin, also beispielsweise auch an eine bestimmte Stelle des RAM!
Die Probleme (an denen ich bislang auch gescheitert bin) sind dabei nur folgende:
- Über den Aufbau des Stacks der Casio CFX CPU ist wenig bekannt; zwar vermute ich ihn im Adressbereich 0000..02BF/0310 des RAMs (s.o.), aber es ist z.B. unbekannt, ob es sich um einen grow - up oder grow - down Stack handelt
, oder welches Format Adressangaben im Stack genau haben.
- Es ist auch nicht bekannt, welchen Wert der Stackpointer zum fraglichen Zeitpunkt hat, an welcher RAM Adresse X sich also die zu verändernde Rücksprungadresse befindet.
- Wenn der Aufruf des Basic - Interpreters über eine Art far call erfolgt ist, muss man außerdem wissen, über welches Segment das Casio CFX Betriebssystem auf den RAM zugreift, da der Sprung ja in dieses Segment hinein erfolgen soll.
- Erfolgt der Aufruf des Interpreters dagegen über einen einfachen call, kann es sein, dass ein "Rücksprung" in den RAM überhaupt nicht möglich ist. Nämlich dann nicht, wenn die CPU zwar Speichersegmente verwalten kann, das Betriebssystem die Segmente aber so einrichtet, dass Code - und Datensegment an verschiedene Stellen im Speicher verweisen. Und das die Segmente voneinander abweichen ist nicht ganz unwahrscheinlich, denn das System geht ja sowieso davon aus, dass sich im RAM kein ausführbarer Maschinencode befindet. Die Möglichkeit überhaupt in den RAM zu gelangen bestünde im Falle eines einfachen call also nur dann, wenn die CPU den Speicher ähnlich dem Z80 verwaltet, und durch sogenanntes Banking jeweils den RAM oder einen Teil davon, und einen Teil des ROMs zu einem gemeinsamen 64KB Speicherbereich zusammenschaltet. Leider ist über die Art, wie die Casio CFX CPU Speicherverwaltung betreibt, nichts bekannt.
Beispiel für das Starten von ASM Code im RAM
Wenn diese Daten jedoch alle bekannt wären (hier würden eine ausführliche Dokumentation der Opcodes, die die CPU benutzt, sowie der original Quellcode des Casio CFX Betriebssystems erheblich weiterhelfen), dann könnte eine Prozedur, die es ermöglicht OHNE ROM Manipulation Assemblercode im RAM auszuführen, z.B. folgendermaßen aussehen:
Gehen wir für dieses Beispiel zunächst vom einfachsten Fall aus, dass das Casio CFX Betriebssystem den Casio Basic Interpreter zwar über einen einfachen call - Befehl aufruft (so dass auch der ret - Befehl im Casio Basic Interpreter nur ein einfacher ist und die CPU daher keine Segmentangabe aus dem Stack liest), die CPU den Speicherbereich aber ähnlich dem Z80 verwaltet, und der 16Bit Adressraum wie folgt geschalten ist:
- 32KB RAM - Fenster im Adressbereich 0000 .. 7FFF
- 32KB ROM - Fenster im Adressbereich 8000 .. FFFF
Gehen wir weiterhin davon aus, dass sich im Stack an Adresse X des RAMs (von der nur bekannt ist, dass sie irgendwo im Adressbereich 0000..7FFF liegt) die 16Bit Rückkehradresse des Casio Basic Interpreters befindet, die in den Adressbereich 8000..FFFF verweist (also an eine Stelle des ROM), und dass sie so aufgebaut ist, dass sich an Adresse X das niederwertige, und an Adresse X+1 das höherwertige Byte des entsprechenden 16Bit Wertes befindet.
- Festlegen, dass die Ausführung des Assemblercodes z.B. an Adresse 5010 im RAM starten soll (das Assemblerprogramm dürfte dann eine maximale Größe von FFFE-5010 ~ 12KB haben). Jetzt den Assemblercode schreiben, der ausgeführt werden soll, wobei ggf. zu beachten ist, dass unser Codesegment an 5010 beginnt (also sollte das Assemblerprogramm direkt nach dem Start ggf. zunächst die Segmentregister mit gültigen Werten laden)
- Den Assemblercode als Binary compilieren
- Wir wollen die Rücksprungadresse an Adresse X im RAM durch einen Variableninhalt überschreiben, sagen wir, durch den Inhalt der Variablen A. Welchen Wert muss die Variable, die ja nicht wie die Rücksprungadresse im ganzzahligen, sondern im FP - Format gespeichert ist, dafür haben? Wir wissen, dass die Rücksprungadresse, die in den Stack geschrieben werden soll, 5010 lauten muss, und das Adressen im RAM oberhalb X für einen grow - down - Stack nicht überschrieben werden dürfen, da hier ggf. Rücksprungadressen weiterer Subroutinen abgelegt sein könnten. Diese zu überschreiben würde bedeuten, dass die CPU bereits früher zu irgend einer falschen Adresse zurückkehren würde, und der für uns wichtige ret - Befehl, der zur an X gespeicherten Adresse zurückkehrt, niemals ausgeführt werden würde. Wir müssen deshalb den 10 Byte Variableninhalt von A in den Adressbereich X..X+9 legen, damit die Inhalte von X-1 und die darunterliegenden unverändert bleiben.
Ist der Variableninhalt im Speicher an X..X+9 ausgerichtet, spiegeln die Felder E1:E0:BS:ES der FP Datenstruktur die Inhalte der Rückkehradresse im Stack wieder (siehe Das FP - Format zur Speicherung von Zahlen). Für die Rückkehradresse 5010 müssten dann die Felder E1=1, E0=0, BS=5 und ES=0 sein, da E0 die Bits 0..3, E1 4..7, ES 8..11 und BS 12..15 der Rückkehradresse belegt.
Für A ist also jeder beliebige Wert möglich, für den gilt, dass Basis und Exponent negativ sind, und dass der Exponent -(100-10)=-90 lautet (der Wert -1.0 E-90 wäre z.B. solch ein gültiger).
- Jetzt den Taschenrechner resetten und ein Casio Basic Programm (das man zweckmäßig z.B. GoASM nennen könnte) mit folgendem Inhalt schreiben: -10^-90->A
- Außerdem der Variablen A zunächst irgend einen beliebigen Wert zuweisen, damit das Betriebssystem überhaupt erst einmal Speicher für A reserviert. Das erspart später Arbeit bei der Manipulation des RAM - Images
- Ansonsten keine weiteren Aktionen erst mit dem Rechner durchführen, damit das RAM möglichst nahe am Originalzustand nach einem Reset liegt, und gleich ein Backup auf den PC übertragen
- Den Hexdump des RAM Images jetzt wie unter Die optimale Art den Casio CFX zu hacken beschrieben in einen Hexeditor laden. Nun können wir das RAM Image entsprechend verändern (siehe Casio CFX Memory Map):
- Zunächst müssen wir das 10 Byte große Basic - Programm GoASM, das den Adressbereich 7FF5..7FFE des RAM belegt, in den Bereich 5006..500F verlegen. Dazu ist der entsprechende Adressausschnitt zu kopieren und an 5006 wieder einzufügen. Jetzt müssen wir die geänderte Adresse noch im Programmheader von GoASM eintragen: die Bytes FE,7F and Adresse 083F und 0840 sind durch 0F,50 zu ersetzen (Anmerkung: die Adresse zeigt auf das Ende des Programms). Außerdem muss der gesamte belegte Grow - Down - Speicher ebenfalls nach oben verschoben - und die Adressdifferenz muss von allen Einträgen der Grow - Down - Pointertabelle subtrahiert werden, damit der GTR den Speicher nicht fehlerhaft verwaltet (ansonsten kann GoASM dann ggf. nicht aus der Programmliste gestartet werden)
- Nun ist die Adresse der Variablen A, die momentan 0842 lautet, auf X zu ändern, damit der Variableninhalt von A künftig an X..X+9 gelesen und geschrieben wird. Diese 16 Bit Adresse von A ist ihrerseits im RAM als Grow - Up - Pointertabelleneintrag an Adresse 041E gespeichert (siehe Grow - Up - Pointertabelle), der 16 Bit Wert an 041E ist also einfach durch X zu ersetzen. Außerdem müssen auch die restlichen Grow - Up - Einträge entsprechend geändert werden, um ebenfalls fehlerhafter Speicherverwaltung vorzubeugen (s.o.)
- Zu guter Letzt wird nun noch der Code der Assembler - Binary in das Image kopiert. Dieser muss bei 5010 beginnen, und kann den Speicher bis zur Adresse 7FFE belegen (also 12271 Bytes maximal)
- Die modifizierte Datei speichern; Jetzt haben wir ein gültiges RAM Image für die Ausführung von Assemblercode im RAM!
- Nachdem das manipulierte Backup erfolgreich auf den Rechner übertragen worden ist, braucht man nur noch ins Programm - Menü zu gehen und das Casio Basic Programm GoASM zu starten, und das Assemblerprogramm läuft!
Das klingt alles theoretisch sehr gut. Der Haken an der Sache ist eben nur, dass man die RAM - Adresse X nicht kennt, und das es auch keine Möglichkeit gibt, diese herauszufinden außer durch das Trial & Error - Prinzip. Bei hunderten möglicher Adressen, die für X in Frage kommen, dürfte das aber äußerst zeitaufwendig sein, zumal ja nicht einmal genau bekannt ist, ob im Stack wirklich nur eine 16Bit Rücksprungadresse gespeichert ist, wie wir für dieses Beispiel angenommen haben, oder ob nicht vieleicht doch das entsprechende Rücksprungsegment mit vermerkt ist. Da wir nämlich nicht wissen, welches Segment das Casio CFX Betriebssystem für den RAM initialisiert hat, wäre das Finden einer Lösung durch Trial & Error dann noch um einiges komplizierter.
Ich selbst habe es deshalb auch gar nicht erst versucht, aber: ES IST ZUMINDEST IM PRINZIP MÖGLICH!
PS: Wenn jemand zufällig über eine detailierte Dokumentation der Casio CFX CPU Opcodes und/oder den original Quellcode des Casio CFX Betriebssystems verfügt und mir mal mailen könnte, wäre ich sehr dankbar. Es sollte dann leicht möglich sein, eine Lösung zu finden, weil man auf Trial & Error verzichten könnte.
Copyright (C) 2004 by Marco Kaufmann