Einen heutigen Spielstand laden

In meinem letzten Beitrag habe ich berichtet, dass ich daran arbeite es zu ermöglichen, in BMP einen Spielstand auch mehrfach laden zu können – normalerweise ist es in BMP nämlich so, dass man einen Spielstand nur ein Mal laden kann. Ebenfalls ist nach dem Speichern ein Weiterspielen unmöglich, was heißt dass man nach dem Speichern das Spiel verlassen und neu starten muss, um dann den eben gespeicherten Spielstand zu laden, aber das ist eine andere Baustelle. Mittlerweile habe ich es geschafft, die Prüfung, ob ein Spielstand bereits geladen wurde, zu umgehen. In diesem Beitrag möchte ich beschreiben, wie ich vorgegangen bin.

Die verantwortliche Stelle im Spielstand

Ganz richtig beschrieb ich in meinem vorherigen Spielstand, dass die Bytes 29-32 (nullbasiert) im Spielstand selbigen identifizieren. Genauer gesagt tun das Byte 29 und Byte 30, denn die weiteren zwei Bytes sind immer 0. Jetzt musste ich erstmal rausfinden, wann diese beiden Bytes überprüft werden

Ladevorgang eines Spielstand

Bei BMP werden Spielstände nicht komplett geladen sondern durch viele einzelne Lesevorgänge aus der Spielstandsdatei, identisch zur Liste der Schreibvorgänge. Meine Idee war jetzt, zu schauen ob der ganze Spielstand geladen wird und dann die relevanten Bytes untersucht werden oder ob sie direkt nach dem Laden untersucht werden und der Spielstand dann gar nicht komplett geladen wird.

Also flugs einen Breakpoint auf INT 21,3F – Read From File or Device Using Handle gesetzt und geschaut wie oft der Interrupt aufgerufen wird – bei einem bereits vorher geladenen Spielstand nur drei Mal, nämlich zwei Mal um den kompletten Header zu lesen und beim dritten Mal, um die 4 Identifikations-Bytes zu lesen. Wurde der Spielstand bereits geladen, ist danach Schluss und die Meldung “Keinen heutigen Spielstand laden” wird angezeigt.

Wie hab ich diesen Screen geliebt… ;)

Und die Abfrage ist wo?

Ich steppte ein bisschen durch den Code nach dem dritten INT 21,3F, konnte aber keinen Vergleich identifizieren. Der Vergleich wäre eigentlich die eleganteste Stelle zum Patchen gewesen, aber fürs erste kam ich da nicht weiter.

Wo stehen die Daten?

Meine nächste Idee war, zu schauen wo die Daten der bereits geladenen Spielstände abgelegt werden – irgendwo im Speicher muss BMP ja die Identifikation vorhalten um sie dann zu prüfen. Ich manipulierte drei Spielstände derart, dass die Identifikation FF FF 00 00, FF FE 00 00 und FF FD 00 00 war, das sollte ja im Speicher zu finden sein. In DOSBox machte ich dann einfach einen binären MEMDUMP mittels MEMDUMPBIN 0000:0000 FFFFFF. 16 MB Speicherabbild sind vielleicht Quark, aber da ich mit dem DOS(Box)-Speicherlayout nicht so vertraut bin schien mir das okay.

In der MEMDUMP.BIN habe ich dann mit einem Hex-Editor nach der obigen Zeichenkette gesucht und bingo! Ab Position 312922 stand sie im Speicher. Jetzt bestand nur das Problem, von der linearen Adresse auf die Adresse im Speicher zu kommen. Die Speicher-Segmentierung bei DOS ist nicht trivial (ganz gut hier bei en.wikipedia.org erklärt), man muss die Adresse aber nur passend umrechnen:

312922 = 0x4C65A, 0x4C64A → 4C60:005A

Und schau an, wenn man im DOSBox-Debugger den Speicher mittels D 4C60:005A auf den passenden Bereich setzt, findet man die gesuchten Daten:

Und die gesuchten Daten werden wann geschrieben?

Nachdem ich ja bei der Abfrage vorher keinen Erfolg hatte, dachte ich mir ich suche einfach nach der Stelle, wo die Identifkationsbytes in den Speicher geschrieben werden. Das ist insofern einfacher, weil DOSBox es erlaubt, beim Schreiben in einen Speicherbereich zu Breaken. Da das beim Lesen mit DOSBox nicht geht, ist es viel schwieriger, den Vergleich zu finden, da hätte ich die Hilfe von SoftICE gebraucht.

Also das Spiel beendet, nochmal neu gestartet und vor dem ersten Ladevorgang einen Speicher-Breakpoint auf 4C60:005A gesetzt: BPM 4C60:005A und mal einen Spielstand geladen – Erfolg! DOSBox zeigt die Meldung “DEBUG: Memory breakpoint : 4C60:005A – 00 -> FF” und hält den Programmlauf an, wir sehen, nachdem wir den Code etwas hochgescrollt haben, folgendes, die Ausführung ist pausiert Zeile 3268:1429 (grün), denn die vorherige Zeile hat die gesuchte Speicherstelle verändert:


3268:1418 8E06D4A3 mov es,[A3D4] ds:[A3D4]=40C2
3268:141C 8B86BAFE mov ax,[bp-0146] ss:[ACAA]=FFFF
3268:1420 8B96BCFE mov dx,[bp-0144] ss:[ACAC]=0000
3268:1424 2689873ABA mov es:[bx-45C6],ax [illegal]
3268:1429 2689973CBA mov es:[bx-45C4],dx [illegal]

Wir sehen daraus folgendes:

  • AX enthält anscheinend die ersten zwei Bytes der Identifikation
  • DX enthäklt anscheinend die zweiten zwei Bytes. Das könnten wir noch überprüfen, indem wir sie anders setzen, soll uns aber jetzt nicht interessieren
  • Wir sind in einer Funktion, die die beiden Werte auf dem Stack übergeben bekommen hat, als [bp-0146] und [bp-0144]
  • Die beiden Werte werden dann in den Speicherbereich verschoben, an dem wir sie auch in MEMDUMP.BIN gefunden haben: es:[bx-45C6] und es:[bx-45C4]
  • Eigentalich egal, aber trotzdem: warum der Index für dieses Verschieben so komisch ist, weiß ich nicht, BX ist ja an sich 0, insofern ergibt BX-z für eine nichtnegative Zahl z ja eine Zahl kleiner 0 – DOSBox zeigt wohl deshalb die Adresse als “illegal” an. Wir wissen aber, dass es:[bx-45C6] zum Beispiel 4C60:005A ist. ES ist im aktuellen Fall, siehe Bild, 40C2, das heißt die Instruktion zeigt auf die Adresse 40C2:BA3A. Dort finden wir auch die Daten. Ich habs nicht ganz verstanden, ist aber eine Overflow-Geschichte, denn von der 0 die 45C6 abziehen lässt BX ins negative überfließen und am Ende kommt der richtige Offset dabei raus.

Auf zum Patch

Jetzt wissen wir erstmal genug, um uns schonmal einen Patch zu überlegen. Da gibt es jetzt verschiedene Möglichkeiten, am einfachsten erschien mir, die oben rot markierte Zeile zu verändern:


3268:141C 8B86BAFE mov ax,[bp-0146]

Anscheinend sind die ersten zwei Bytes, die nach AX geladen werden, in echten Spielständen immer ungleich 0, zumindest habe ich da nie zwei Null-Bytes gesehen. Warum also nicht einfach den Wert, der im Speicher zur Identifikation verwendet wird, auf 0 setzen? Ein bisschen die Suchmaschinen bemüht, denn meine Kenntnis über Opcodes ist aus dem Stegreif nicht sonderlich gut. Ergebnis war dann, dass ich den einen Befehl oben wie folgt ersetzt habe:


8B0000 mov ax,0000
90 nop

Anstatt den Wert vom Stack zu laden setze ich das Register also einfach auf 0 und da die Instruktion ein Byte kürzer ist als das Original fülle ich mit einem NOP auf. Gut, soweit wäre das erledigt, muss nur noch die BMMAIN.EXE gepatcht werden. Suchen wir mal nach 8B86BAFE, wir finden die Byte-Folge zwar drei Mal, aber nur die zweite hat den richtigen Kontext an Bytes außenrum. Die 8B86BAFE ändern wir in 8B000090, löschen alle bisherigen Breakpoints im DOSBox-Debugger, setzen einen auf auf die Instruktion vor der, die wir geändert haben, also BP 3268:1418, und starten die modifizierte BMMAIN.EXE.

Da ist unser modifierter Code

Et voilà, DOSBox bricht ab wie im Bild gezeigt, wir sehen den geänderten Code und beim Drücken von F5 läuft das Spiel weiter und lädt unseren Spielstand – beim erneuten Laden lädt sie ihn nochmal, und so weiter. Mission erfolgreich, wir können jetzt Spielstände mehrfach laden, was einerseits zum Testen hilft, andererseits auch ganz nett sein kann, wenn man mal unbedingt ein enges Spiel gewinnen will, dann lädt man halt so lange bis es klappt. :)

To do

Immer schön, wenn man mal was rausgefunden hat, aber wie so oft bleiben fast mehr Fragen als gelöst wurden. :)

  • Rausfinden, wie und wann die relevanten Bytes (29-32 im Spielstand) erzeugt werden – wird vielleicht so eine Art Zufallszahlengenerator sein.
  • Einen frisch gespeicherten Spielstand kann man nicht laden – wann werden beim Speichern die Bytes gesetzt? Sitzen die dann an der gleichen Stelle?*
  • Andere Baustelle, aber trotzdem: weiterspielen nach dem Speichern. Sollte der vorherige Punkt geklärt werden, dann kann man vielleicht so vorgehen, dass man einen frisch gespeicherten Spielstand neu lädt und dann weiterspielt?

* Ist nicht unlogisch, dass das nicht geht, denn wir haben ja nur verhindert, dass der Wert, der aus einem Spielstand geladen wird, in den Speicher geschrieben wird. Beim Speichern gibt es eine eigene Routine, die die Identifikationsbytes ebenfalls in den Speicher schreibt, das sollte sich aber durch beobachten des relevanten Speicherbereichs, den wir ja jetzt kennen, überprüfen lassen. Edit: Jepp, so siehts aus. Die Bytes werden an die gleiche Stelle geschrieben wie beim Laden, ändert man diese im Speicher ab, kann man den gerade gespeicherten Spielstand laden und dann weiter spielen. Mehr dazu bei Gelegenheit in einem weiteren Beitrag.

P.S.: Bezieht sich alles auf die deutsche BMP v1.03.
P.P.S.: Um den Patch selbst durchzuführen, BMMAIN.EXE laden und die 4 Bytes 8B86BAFE ab Offset 205436 in 8B000090 ändern.

This entry was posted in BMP, DE-1.03, General information, Save games, Technical information and tagged , , , , . Bookmark the permalink.

Leave a Reply