Hello, Guest the thread was called6.2k times and contains 51 replays

last post from detlef at the

Disassembler Logik

  • Ich spiele ein wenig mit dem disassemblieren von Code herum.

    Dabei stolpere ich über ein Problem, bei dem ich nicht weiss, wie ich es lösen kann.


    Beispiel (den Sinn des Code bitte ignorieren.)

    Der Maschinencode dafür


    Code
    1. ad 17 08 4c 18 08 10 8d 21 d0 60

    Beim disassemblieren gehe ich so vor, dass ich eine mnemonic Tabelle habe, in der ich nachschaue, aus wie vielen bytes eine Anweisung besteht,


    Code
    1. "ad":{
    2. "ins": "lda $hhll"        <- hh und ll durch die nächsten 2 folgenden bytes ersetzen
    3. },


    So wird also "AD 17 08" also LDA $0817


    Für einfache Listings, in denen sich Code und Daten nicht mischen, funktioniert das schon sehr gut. Nicht aber im obigen Fall, wo Daten mitten im Code stehen.


    Das !byte $10 wird hier nämlich durch das mnemonic BPL ersetzt, welches ein weiteres Byte erwartet für die relative Adressierung


    Code
    1. "10": {
    2. "ins": "bpl $hh",
    3. "rel": 1
    4. },


    Dadurch stimmt dann der folgende Code nicht mehr.

    Der obige Bytecode wird daher zu diesem Code disassembliert:


    Code
    1. * = $0811
    2. lda x0817
    3. jmp x0818
    4. x0817
    5. bpl 8d
    6. and ($d0,x)
    7. rts


    Ich vermute daher, dass ich mit dem aktuellen Ansatz, Code zu erkennen, in einer Sackgasse stecke.

    Ich wüsste aber auch nicht, wie man mit Sicherheit Code von Daten unterscheiden könnte. Wäre der obige Datenbereich statt $10 (für BPL) zum Beispiel $EA (NOP) gewesen, dann würde es zwar codeseitig keinen Sinn ergeben, aber durchaus wieder korrekt kompilieren.


    Ich rufe an dieser Stelle direkt mal Assembler-Gott Mac Bacon herbei :)

    Freue mich über Hinweise, Tipps und Denkanstösse :)

  • Code kannst du nicht von Daten unterscheiden. Du kannst dich nur entlang "sicherer" Wege hangeln. Du brauchst immer einen Einstiegspunkt, ab da bist du bis zu einem RTS oder JMP sicher.

    Ein JMP oder JSR liefert dir neue Einsprungadressen, bei denen du weitersuchen kannst. Bis dahin läuft der Automatismus ziemlich gut.


    Ab da muss der Benutzer mithelfen; neue Werte aus Hi-/Lo-Byte-Jump-Tabellen suchen, etc.

    Ggf. kann man bekannte Kernal-Adressen auch verwenden; setzt aber voraus, dass der Code wirklich Kernal-Routinen aufruft, und nicht etwa zufällig am gleichen Ort im RAM liegt.

  • In meinem Disassembler schaue ich mir auch noch die Befehle und deren Werte an.

    Also für z.B. Jmp, lda , sta usw die Adressen wenn sie nicht indirekt sind.

    Wenn jsr ,Jmp oder bne z.b

    dann ist der Bereich grundsätzlich erstmal Code. Bei lda ,sta usw. sind es Daten.

    Ich gehe also nicht linear durch das File durch sondern von Adresse zu Adresse


    Das Ganze klappt aber auch nicht 100 %tig

    Bei Sprungtabellen wird es dann schnell kompliziert oder IRQ Vektoren

  • Damit kann man nochmal ein paar Prozent bessere Erkennung herausholen, aber da es nicht hunderprozentig funktioniert, nutzt das eigentlich wenig, weil man sowie nacharbeiten muss.


    Ich habe das bei meinem "Unassembler" so gelöst, dass ich erzeugte Code- und Daten-Labels unterschiedlich benenne. Insofern schaue ich mir als auch die Befehle an. Aber ich erstelle dann aus diesen Infos manuell eine Konfigurationsdatei, in die ich die Code- und Datenbereiche eintrage.


    Das braucht dann 4-5 manuelle Durchgänge, dann sind in der Datenbereiche gefunden. Man muss dann noch analysieren, ob die Datenbereiche evtl. Adressen enthalten. Dafür habe ich dann zusätzliche Attribute für die Datenbereiche.

    Das ist also kein Disassembler, um mal schnell irgendwo reinzuschauen. In der Regel erzeuge ich mit diesem Verfahren aus einem nicht zu komplexen Code innerhalb von 1-2 Stunden einen relozierbaren Assembler-Quellcode.


    Ich habe mir auch schon überlegt, ob man diese manuellen Durchläufe automatisieren könnte. Aber es gibt sehr viele Sondernfälle, wo das schief gehen könnte und dann muss man doch wieder von hand ran. Und in diesem 1-2 Stunden habe ich mich dann auch schon inhaltlich mit der Struktur des Programms beschäftigt. Die Zeit hätte es also sowieso gebraucht.


    So sieht dann beispielsweise diese Konfigurationsdatei aus:



    Im Anhang noch mal ein (anderes) Assemblerlisting, das ich gerade vor ein paar Tagen erzeugt habe. Das Listing ist unbearbeitet und wurde direkt so vom Unssembler erzeugt. Inkl. der Trennlinien zwischen den Prozeduren.

  • Code kannst du nicht von Daten unterscheiden. Du kannst dich nur entlang "sicherer" Wege hangeln. Du brauchst immer einen Einstiegspunkt, ab da bist du bis zu einem RTS oder JMP sicher.

    Ein JMP oder JSR liefert dir neue Einsprungadressen, bei denen du weitersuchen kannst. Bis dahin läuft der Automatismus ziemlich gut.

    Oder ein bedingter Sprung, der immer ausgeführt. z.B.


    LDA #0

    BEQ ...


    Leider kann man da nie hundertprozentig sicher sein. Solche Pattern sind gar nicht so selten und das zu erkennen ist dann schon aufwändig.

  • Ich wüsste aber auch nicht, wie man mit Sicherheit Code von Daten unterscheiden könnte.

    Die Nintendoleute behelfen sich mit CDL-Dateien: http://fceux.com/web/help/CodeDataLogger.html


    "The Code/Data Logger makes it much easier to reverse-engineer NES ROMs. The basic idea behind it is that a normal NES disassembler cannot distinguish between code (which is executed) and data (which is read). The Code/Data Logger keeps track of what is executed and what is read while the game is played, and then you can save this information into a .cdl file, which is essentially a mask that tells which bytes in the ROM are code and which are data. The file can be used in conjunction with a suitable disassembler to disassemble only the actual game code, resulting in a much cleaner source code where code and data are properly separated."

  • Im Grunde arbeitet mein Disassmbler erstmal nach dem Motto "Data First".

    Es wird dann erstmal ein Startpunkt definiert. Alles was dann nicht via Branch, JMP oder JSR aufgerufen wird ist dann bleibt dann "Data"

    Im Prinzip funktoniert das dann so:

    Am Anfang steht sozusagen ein grosses DatenArray. Mit der StartAdresse wird dann das Array weiter aufgesplittet, wenn ein Opcode ekannt wurde, in kleinere Arrays.

    Je nachdem ob der Disassembler dann durch einen Branch,... oder LDA STA usw. Opcode läuft wird dieser Teil vom aktuellen Array abgespalten und mit "VISITED" markiert.

    Im abgespaltenen Teil werden dann noch die Caller Addressen hinterlegt.

    Am Ende bleiben dann im Bezug auf den Code 1,2 oder 3 Byte grosse Arrays übrig oder eben 1..n Bytes grosse Daten Arrays.


    Ich hoffe ich habe das einigermassen verständlich beschrieben :rolleyes:

  • Das !byte $10 wird hier nämlich durch das mnemonic BPL ersetzt, welches ein weiteres Byte erwartet für die relative Adressierung

    Du kannst die Daten nicht vom Code ohne weiteres unterscheiden. Das ist eigentlich ein typisches Problem beim Disassemblieren. Wenn du den Disassembler aufrufst, dann musst du ja davon ausgehen, dass die Startadress erstmal code ist. Darauf basierend, müsstest du dann eine Tabelle erzeugen, von Zielen, die durch diesen Code angesprungen werden (branches, jmps, etc.) wenn du dann Überlappungen hast, müsstest du zurückgehen in der Liste und dann das letzte Ziel nehmen, das die Überlappung überspringt. Problematisch wirds sowieso bei selbstmodifizierenden Code oder Code der bewusst in sich reinspringt weil der so angelegt ist, dass er trotzdem sinnvoll bleibt.

  • Auf der sicheren Seite wäre man mit einem externen Tool, das quasi "von Außen" mitloggt, was der Code ausführt. Also ähnlich dem in Post #8 beschriebenen Tool für die NES ROMs.


    Wenn beim Programmablauf kontinuierlich festgehalten wird, welche Adressen als Code abgearbeitet werden und welche nur gelesen/geschrieben werden, dann hat man am Ende die Trennung von Daten und Code.


    Das ist allerdings nur mit dem Gerät, auf dem der Code läuft, nicht so ohne Weiteres oder auch gar nicht umsetzbar.

  • Das Hauptproblem beim C64 ist, daß man mit einem statischen Disassembler eh nicht weit kommt. Am Anfang von so ziemlich jedem Programm wird erstmal Code von A nach B kopiert/entpackt, dann nach B gesprungen usw.

    Jede zusätzliche Intro bzw. jedes Trainermenü macht die Sache noch unübersichtlicher.

    Dazu kommt, daß bei einer derart limitierter CPU selbstmodifizierender Code in vielen Fällen als elegante/performante Lösung angesehen wurde und entsprechend gerne und häufig benutzt wird.

    Eigentlich bräuchte man für den C64 einen interaktiven Disassembler, aber sowas scheint es nicht wirklich zu geben. Immerhin kann man bei WFDis Code bis zu einer bestimmten Stelle ausführen, aber zum einen dauert das da ewig, zum anderen ist es wohl auch nicht so richtig zu Ende gedacht.

  • Auf der sicheren Seite wäre man mit einem externen Tool, das quasi "von Außen" mitloggt, was der Code ausführt. Also ähnlich dem in Post #8 beschriebenen Tool für die NES ROMs.

    Sowas nennt man einen "Flow/Instruction Trace". Einigermaßen moderne Prozessoren haben dafür Unterstützung eingebaut (On Chip Debugging), aber man braucht teure externe Tools und Software, um die entsprechenden Trace-Messages aufzuzeichnen und auszuwerten. Beim C64 könnte man all das per Emulator (oder FPGA-Lösung + externer SW) machen, aber leider gibt es das in dieser Form auch nicht. Hatte mal einen entsprechenden Wunsch im Denise-Thread abgesetzt, aber klar, das wäre erstmal sehr viel Arbeit. Wer mal sehen möchte, wie ein moderner Debugger aussieht, kann man mal bei Lauterbach oder in sehr abgespeckter Form bei Segger vorbeischauen.

    Ich finde es trotzdem irgendwie erstaunlich, daß in den letzten 30 Jahren so wahnsinnig viel Arbeit in diverse Tool, Spiele, Erweiterungen, diverse parallel entwickelte Emulatoren und FPGA-Lösungen investiert wurde, aber es nach wie vor weder eine moderne Debugging-Lösung für den C64 gibt noch einen wirklich brauchbaren interaktiven Debugger.

  • Im Grunde arbeitet mein Disassmbler erstmal nach dem Motto "Data First".

    Es wird dann erstmal ein Startpunkt definiert. Alles was dann nicht via Branch, JMP oder JSR aufgerufen wird ist dann bleibt dann "Data"

    Im Prinzip funktoniert das dann so:

    Am Anfang steht sozusagen ein grosses DatenArray. Mit der StartAdresse wird dann das Array weiter aufgesplittet, wenn ein Opcode ekannt wurde, in kleinere Arrays.

    Je nachdem ob der Disassembler dann durch einen Branch,... oder LDA STA usw. Opcode läuft wird dieser Teil vom aktuellen Array abgespalten und mit "VISITED" markiert.

    Kann man so machen. Aber versuch das z.B. mal mit dem C64-Basic mit seinen Sprungtabellen, auf den Stack geschobenen Return-Adressen und Interrupt-Routinen. Da werden viele Datenbereiche übrig bleiben, die eigentlich Code sind.


    Ich bin der Meinung, man erkennt einem Assemblerlisting leichter an, dass es eigentlich Daten sind (z.B. enstehend da sinnlose Sprung- und Daten-Labels, die man leicht erkenne kann). Einen Datenbereich nachträglich als Code zu identifizieren, ist viel schwieriger.


    Deswegen mache ich "Code First".

  • Beim C64 könnte man all das per Emulator (oder FPGA-Lösung + externer SW) machen, aber leider gibt es das in dieser Form auch nicht.

    Das sehe ich auch so: DAS wäre wirklich ein Alleinstellungsmerkmal und eine geniale Unterstützung in einem Emulator!


    Man hätte quasi nach ein paar Runden Spielen, "nebenbei" noch die Aufteilung von Code und Daten des Programms geliefert bekommen. Bequemer ginge es nicht! ;)

  • Beim C64 könnte man all das per Emulator (oder FPGA-Lösung + externer SW) machen, aber leider gibt es das in dieser Form auch nicht.

    Ja, leider. Beim Ultimate 64 haben wir die Möglichkeit, jene Info per Ethernet zu streamen (ist in etwa alle Leitungen des Expansionsportes, also insb. Adress- und Datenbus). Aber eine Auswertesoftware gibt es nicht.

    Ja, ich sehe den Unterschied: Eine echte Instruktionsmarkierung hat man so nicht. Aber man schaut so einen C64 bei der Ausfürung zu.

  • Das mit der Markierung von Code basiert aber eh auf der Annahme, daß das eine statische Zuordnung ist. Bei einem typischen C64-Spiel, das ständig nachlädt, entpackt und Code modifiziert, hat man davon nicht viel.

    Aber mit einem richtig guten Trace-Feature könnte man immerhin sehen, wie man an einen bestimmten Punkt gekommen ist, wer wann auf welche Adresse geschrieben oder von dort gelesen hat usw.

    Es wäre aber schon hilfreich, überhaupt mal einen guten Debugger zu haben, der natürlich auch diverse disassemblierte Funktionen, Speicherdumps und Peripherieregister anzeigen kann.

    Falls also doch mal Code erst zur Laufzeit ins RAM kopiert und ausgeführt wird, kann man man dann halt mit einem Breakpoint zur richtigen Zeit genau diese Bereich disassemblieren, wenn er auch ausführbar ist (oder gerade ausgeführt wird).

    Ich arbeite in der Firma fast täglich mit einem Lauterbach-Debugger an 32bit-MultiCore-CPUs im Embedded-Bereich und kann nur sagen, daß man mit solch mächtigen Werkzeugen bestimmten Problemen sehr viel leichter auf die Spur kommt als mit einfacheren Debug-Möglichkeiten oder manueller Code-Analyse.