Reverse Engineering: Wie das Oszilloskop bootet
Im ersten Teil einer fünfteiligen Serie habe ich Linux auf einem Oszilloskop zum Laufen bekommen. In Teil zwei meiner Serie zur Analyse eines Oszilloskops per Reverse Engineering erkunde ich den Bootvorgang des Oszilloskops und untersuche dessen Dateisystem – wobei ich mir zum Schluss ein wenig die Haare raufe.
Die CPU im Oszilloskop ist ein Samsung S3C2416-SoC mit DDR2-SDRAM als Arbeitsspeicher und einem NAND-Flash-Speicher als Massenspeicher – ein einfaches Embedded-System, das Samsungs SMDK2416-Referenzdesign sehr ähnlich ist.
Ich habe schon mit anderen SoCs (System-on-a-Chip) der S3C24xx-Familie gearbeitet, ich weiß ungefähr, wie der Bootloader funktioniert und mit dem Flash-Speicher umgehen müsste. NAND-Speicherbausteine sind ein wenig unzuverlässig, mit der Zeit entwickeln sich Bitfehler und einige Speichersektionen sind dann derart kaputt, dass sie überhaupt nicht mehr verwendet werden können.
Jedes Flash-Dateisystem unterstützt deshalb Fehlerkorrekturcodes (ECC), um Bitfehler zu korrigieren und fehlerhafte Sektionen umzukopieren. Viele Flash-Speicher-Hersteller garantieren allerdings, dass im ersten Sektor oder in den ersten drei Sektoren des Speichers niemals Fehler auftreten werden. Deswegen kann dort ein sehr kleiner Bootloader untergebracht werden, der sogenannte First-Stage-Bootloader.
Im ROM der CPU befindet sich ein Programm, das die ersten 8 KByte vom Flash-Speicher in den 64 KByte großen RAM der CPU kopiert und dann ausführt. Dieser First-Stage-Bootloader führt einige Basiskonfigurationen durch, etwa die Konfiguration der GPIO-Pins und die Initialisierung des DDR-Speichers. Dann lädt er den größeren Second-Stage-Bootloader aus dem Flash-Speicher, kopiert ihn in den DDR-Speicher und führt diesen aus. Der First-Stage-Bootloader sollte ECC beherrschen und Flash-Sektoren umkopieren können, um zuverlässig mit den Einschränkungen des Flash-Speichers umzugehen.
Der Bootvorgang nach dem Bootvorgang
Wenn Anwender vom Bootloader sprechen, meinen sie meist den Second-Stage-Bootloader. Er hat zumeist eine Kommandoeingabe, die über einen seriellen Anschluss oder USB verfügbar ist. Er lädt das Betriebsystem, zum Beispiel von einem Flash-Speicher und verfügt dafür über eine einfache Implementierung eines geeigneten Dateisystems. Um die Entwicklung zu vereinfachen, kann ein Bootloader manchmal auch ein Betriebssystem über die serielle Schnittstelle, USB oder Ethernet laden.
Den Inhalt des Flash-Speichers analysieren
Als ich per JTAG auf das Oszilloskop zugreifen kann, erstelle ich als Erstes ein komplettes Abbild des Flash-Speichers. Die CPU lädt den First-Stage-Bootloader aus den ersten 8 KByte des Flash-Speichers, also fange ich dort an. Ich sehe einige Zeichenketten, die dem gleichen, was ich auf der seriellen Ausgabe gesehen habe, nachdem ich das Oszilloskop angeschaltet hatte. Andere Zeichenketten deuten auf eine Art Speicherinitialisierung und einen Speichertest hin. Soweit entspricht der Code zum Glück also meinen Erwartungen.
pre> 00000a30 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 |0123456789ABCDEF| 00000a40 4c 49 4c 4c 49 50 55 54 0d 0a 00 44 53 4f 20 54 |LILLIPUT...DSO T| 00000a50 41 52 47 45 59 20 42 4f 41 52 44 20 56 45 52 20 |ARGEY BOARD VER | 00000a60 31 2e 30 0d 0a 00 54 45 53 54 20 53 44 52 41 4d |1.0...TEST SDRAM| 00000a70 20 4d 45 4d 4f 52 59 0d 0a 00 4d 45 4d 4f 52 59 | MEMORY...MEMORY| 00000a80 20 41 4c 4c 20 4f 4b 0d 0a 00 4d 45 4d 4f 52 59 | ALL OK...MEMORY| 00000a90 20 54 45 53 54 20 46 41 49 4c 0d 0a 00 45 52 4f | TEST FAIL...ERO| 00000aa0 52 52 20 41 44 44 52 45 53 53 20 00 4f 4b 20 00 |RR ADDRESS .OK .| 00000ab0 45 4e 54 45 59 20 4d 41 49 4e 00 00 04 f0 1f e5 |ENTEY MAIN......|Dem First-Stage-Bootloader folgt wahrscheinlich der Second-Stage-Bootloader. Und tatsächlich taucht später im Abbild eine Liste von Zeichenketten auf, die nach Befehlen für eine Kommandozeile aussehen. Das sieht vielversprechend aus.
Außerdem habe ich den Eindruck, dass der Bootloader yaffs(öffnet im neuen Fenster) implementiert, ein beliebtes Dateisystem für NAND-Flash-Speicher für Linux, das auch unter einer kommerziellen Lizenz für andere Plattformen verfügbar ist.
Es ist recht eindeutig, wo der Bootloader aufhört. Nach 75 KByte kommen im Abbild nur 0xff-Werte. Dabei handelt es sich um den Standardwert, wenn Flash-Speicher gelöscht wird. Das geht so weiter, bis nach 640 KByte etwas anfängt, das nach einem Dateisystem aussieht.
Ich speichere die ersten 75 KByte in einer eigenen Datei.
Firmware disassemblieren
Was soll ich jetzt damit anstellen? Eine Möglichkeit, um mehr über das System herauszufinden, ist es, die Firmware zu disassemblieren. Dabei wird der binäre Maschinencode mit Hilfe eines Disassemblers in eine symbolische Assemblersprache umgewandelt, die ein Mensch versteht. Einfachere Varianten übersetzen den Maschinencode rein mechanisch in symbolische Instruktionen, bessere können den Programmablauf analysieren und auch die Struktur des Codes ermitteln. Manche Disassembler-Programme ermöglichen auch ein interaktives Vorgehen, so können Anmerkungen eingefügt oder die Übersetzung beeinflusst werden. Sogenannte Decompiler können sogar Maschinencode in höhere Sprachen übersetzen, wie zum Beispiel C oder Go.
Programme zum Disassemblieren
Ich benötige ein Werkzeug, mit dem ich auf einen Codebereich schauen und Anmerkungen einfügen kann, während ich lerne, den Code zu verstehen. Deswegen schaute ich mir verschiedene Disassembler an.
IDA(öffnet im neuen Fenster) ist eines der besten kommerziellen Programme. Ich habe es schon vor zehn Jahren benutzt, und auch wenn es etwas schrullig war, hat es gut funktioniert und sollte heute sogar noch besser sein. Aber für die Analyse von ARM-Code ist es recht teuer und ich will für ein Hobbyprojekt nicht soviel Geld ausgeben.
Ich probiere auch einige Online-Disassembler, aber sie kommen mit meinem Speicherabbild nicht zurecht. Also probiere ich auch einige Open-Source-Programme aus. Manche sehen sehr provisorisch aus oder sind lange Zeit nicht aktualisiert worden. Trotzdem schaue ich mir einige genauer an.
Ich probiere Radare(öffnet im neuen Fenster). Es scheint tauglich für meine Ansprüche, aber es wirkt, als ob die Programmierer an das Motto glauben: "Es war schwer zu schreiben, also sollte es auch schwer zu nutzen sein". Ich verstehe die Dokumentation nicht, sie wirkt wie eine Übersicht für Leute, die es bereits kennen. Und ich komme nicht mit der Benutzeroberfläche zurecht.
Capstone(öffnet im neuen Fenster) sieht vielversprechender aus. Es ist ein Fork von Bestandteilen in LLVM, die mit Maschinencode arbeiten. Mir gefällt die Idee, auf LLVM zurückzugreifen, denn es unterstützt viele Prozessoren und das würde auch für den Disassembler gelten. Aber Capstone bietet nur eine Infrastruktur und keine interaktiven Werkzeuge, die mir gefallen, und mir fehlt die Zeit, selber etwas zu schreiben.
Valgrind(öffnet im neuen Fenster) dient eigentlich zum Speichercheck. Es dekodiert den Maschinencode im Speicher und übersetzt ihn in eine Zwischensprache, fügt dann Code zur Speicherüberprüfung hinzu und übersetzt es wieder in Maschinencode zurück. Valgrind unterstützt ARM und die Zwischensprache sieht nutzbar aus. Aber auch hier handelt es sich eher um Infrastruktur ohne Benutzeroberfläche.
Schließlich stoße ich auf Medusa(öffnet im neuen Fenster). Das Programm ist längst noch nicht fertig, es kann nicht alle Instruktionen übersetzen, läuft nicht immer stabil und hat noch einige Bugs. Allerdings kann ich aufgrund des offenen Quellcodes einige Fehler umgehen und die fehlenden Instruktionen ergänzen. Ich entscheide mich schließlich für das Programm.
Ich lade meine Datei in Medusa und beschäftige mich damit, wie das Programm funktioniert. Praktisch an Medusa ist dessen Datenbankformat. Es handelt sich um eine Textdatei in einem leicht verständlichen Format. Ich füge darin einige Bezeichner für diejenigen Speicherbereiche ein, die SoC-Registeradressen entsprechen. So kann ich das Resultat des Disassemblers leichter lesen und verstehen.
Der First-Stage-Bootloader wird analysiert
Ich fange wieder mit dem First-Stage-Bootloader an, er ist klein und hat keinerlei Abhängigkeiten. Perfekt, um mich auch mit Medusa vertraut zu machen. Ich finde zuerst Code, der auf die GPIO-Register zugreift und mit dem DDR-Speicher-Controller kommuniziert. Einen Teil des Codes, der anscheinend mit dem Koprozessor zu tun hat, kann Medusa nicht korrekt übersetzen. Der Code des Bootladers kommuniziert mit dem Flash-Speicher-Controller.
Ich untersuche den Code nicht allzu genau. Ich weiß ungefähr, was vor sich geht, und die Details sind nicht so wichtig. Was mich interessiert, ist der Code, der den Second-Stage-Bootloader in den DDR-RAM lädt, und ab wo er ausgeführt wird.
Ich finde Code-Anweisungen, die die ersten 128 KByte vom Flash-Speicher an die Adresse 0x30000000 laden, die Startadresse des DDR-RAMs. Dann werden vom Offset 0xac4 an 70 KByte zur Adresse 0x33c00000 kopiert – die letzten 512 KByte des DDR-RAMs – und dann führt die CPU ab dort den Code aus.
Der Second-Stage-Bootloader ist als Nächstes dran
Ich erstelle eine zweite Medusa-Datenbank für den Second-Stage-Bootloader mit den bereits mir bekannten Speicheradressen und beginne, damit zu arbeiten.
Ich vergrabe mich aber auch nicht zu tief in diesen Bootloader. Selbst wenn der Maschinencode nur 75 KByte umfasst, ist das recht aufwendig, ich überfliege ihn ebenfalls nur. Ich stelle schnell fest, dass der Code Thumb-Befehle(öffnet im neuen Fenster) umfasst, mit denen Medusa größere Probleme hat. Ich folge dem Programmfluss, dabei werden vor allem Datenstrukturen initialisiert. Diesen Teil überspringe ich. Ich stoße aber auch auf interessante Anweisungen, die mit den SoC-Registern zu tun haben.
Es gibt einen Abschnitt im Code, der einen Zeiger auf die Zeichenkette "param" speichert, gefolgt vom Aufruf einer Funktion, die mit dem Flash-Speicher-Controller kommuniziert. Könnte das einer Datei-öffnen-Funktion entsprechen? Danach wird mit dem Rückgabewert der Datei-öffnen-Funktion ein Zeiger auf einen Speicherbereich außerhalb des Programmcodes erzeugt und ein Register mit dem Wert 460 beschrieben. Könnte das einer Datei-lesen-Funktion entsprechen? Vielleicht.
Nach einer Weile finde ich Code, der sich auf die Zeichenketten "Boot to load (Y/N)?" und "Wait for Enter" bezieht. Außerdem gibt es einen Codeaufruf im Zusammenhang mit der seriellen Schnittstelle und danach wird der Wert 0x75 geprüft. Dieser Wert entspricht dem ASCII-Code von "u". Deshalb probiere ich, "u" einzugeben, während das Oszilloskop bootet. Und tatsächlich erscheint darauf die Ausgabe "Enter USB-download Mode" und das Oszilloskop hält an. Wahrscheinlich wartet es jetzt auf einige Kommandos über USB.
Wenn während der Boot-Abfrage eine vorgegebene Zeit überschritten wird, lädt der Bootloader eine Datei namens "os" an die Adresse 0. Dann wird anscheinend eine Prüfsummen-Kalkulation durchgeführt. Stimmt der Wert nicht mit einem Wert an einer bestimmten Speicheradresse im RAM überein, dann versucht der Bootloader, eine Datei "oscp" zu laden und erneut die Prüfsumme zu ermitteln. Schlägt auch das fehl, wird die Meldung "open or read OS ERR" ausgegeben. An dieser Stelle wird offensichtlich versucht, das Betriebsystem zu laden, und dessen Zustand überprüft.
Ich versuche, herauszufinden, ob ich irgendwie die Kommandozeile des Bootloaders aufrufen kann. Ich vermute aber, dass der Zugriff in der Produktionsversion deaktiviert wurde. Die noch vorhandenen Kommandonamen waren einfach Überbleibsel davon.
Das Dateisystem untersuchen
Wenn ich mir die Datei-öffnen- und lesen-Funktion näher anschaue, könnte ich vielleicht das Dateisystem im Flash-Speicher näher bestimmen. Doch das scheint mir zu viel Arbeit zu sein. Ich probiere einen anderen Weg.
Yet Another Flash File System (Yaffs) ist ein Dateisystem speziell für NAND-Flash-Speicher. Es kann mit all den Besonderheiten von Flash-Speicher umgehen wie der Fehlerkorrektur, dem Umkopieren beschädigter Bereiche und dem gleichmäßigen Beschreiben aller Speicherbereiche (Wear leveling). Das Dateisystem ist Open Source und für den Einsatz in Linux und uBoot unter GPL lizenziert, es gibt aber auch kommerzielle Lizenzen für proprietäre Systeme.
Ich finde einige Zeichenketten im Speicherabbild des Oszilloskops, die auf Yaff hindeuten. Auch einige Kommentare im EEVblog-Forum sprechen davon. Ich suche nach Werkzeugen, die mit einem Yaff-Dateisystem umgehen können.
Ich verbringe einige Zeit damit und scheitere komplett. Keines der Werkzeuge kann etwas mit den Daten anfangen. Ich schäme mich fast dafür, aber es dauert einige Zeit, bevor ich anfange, nachzudenken und direkt auf die Daten zu schauen. Ich begreife dann schnell, dass es offensichtlich nicht Yaff war.
Die erste Datei
Es sieht so aus, als ob das Dateisystem nach 640 KByte (0xa0000) im Speicherabbild beginnt. Hier die erste Sektion im Flash, auch Page genannt:
pre> 000a0000 01 ff ff ff 01 00 00 00 ff ff 66 70 00 00 00 00 |..........fp....| 000a0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 000a0100 00 00 00 00 00 00 00 00 00 00 ff ff 80 f1 00 00 |................| 000a0110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000a0120 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff |................| 000a0130 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 000a01c0 ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 |................| 000a01d0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 000a01e0 ff ff ff ff ff ff ff ff 00 00 00 00 ff ff ff ff |................| 000a01f0 ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 |................| 000a0200 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 000a07e0 ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 |................| 000a07f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|Jede Sektion ist 2 KByte groß und nachdem ich mein Gehirn eingeschaltet habe, scheint es mir klar, dass 01 ff ff ff 01 00 00 00 eine magische Markierung ist und "fp" der Dateiname.
pre> 000a0800 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 000a0810 55 99 aa 66 0c 85 00 e0 04 00 8c 85 c0 01 8c 82 |U..f............| 000a0820 bc 00 8c 86 90 77 8c 43 20 00 08 c9 0c 87 00 f3 |.....w.C .......| 000a0830 0c 83 00 81 04 00 04 00 04 00 04 00 04 00 04 00 |................| 000a0840 04 00 04 00 04 00 04 00 04 00 04 00 04 00 04 00 |................| 000a0850 04 00 04 00 04 00 cc 81 3c 13 8c 81 10 81 2c 84 |........ᐸ.....,.| 000a0860 00 00 4c 80 00 f8 8c 87 ff ff cc 84 00 a0 cc 82 |..L.............| 000a0870 00 20 cc 80 80 00 4c 86 00 00 4c 81 00 00 4c 85 |. ....L...L...L.| 000a0880 00 00 4c 83 00 00 4c 87 00 00 cc 85 d8 47 cc 43 |..L...L......G.C| 000a0890 00 00 00 00 04 00 04 00 0c 44 00 00 00 00 0c 85 |.........D......| 000a08a0 00 80 0a 06 00 40 19 b5 00 00 00 00 00 00 00 00 |.....@..........| 000a08b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|Die Daten ab 0xa0800 kommen mir bekannt vor, wo habe ich das schon einmal gesehen?
Es ist der Anfang eines Xilinx-Spartan-6-Bitstroms, wie ich ihn schon einmal in einem Blogpost(öffnet im neuen Fenster) gesehen habe! Auch der Dateiname "fp" für den FPGA-Bitstrom liegt nahe.
Die Bits in den Daten sehen verdreht aus, aber das ist üblich. Xilinx' eigene Werkzeuge arbeiten damit und die Daten entsprechen deren Standardausgabe.
Ein wenig später taucht eine Sektion mit einem fast identischen Anfang auf, wahrscheinlich markiert sie das Dateiende.
pre> 000f4800 01 ff ff ff 01 00 00 00 ff ff 66 70 00 00 00 00 |..........fp....| 000f4810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 000f4900 00 00 00 00 00 00 00 00 00 00 ff ff 80 f1 00 00 |................| 000f4910 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000f4920 00 00 00 00 7c 32 05 00 ff ff ff ff ff ff ff ff |....|2..........| 000f4930 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 000f49c0 ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 |................| 000f49d0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 000f49e0 ff ff ff ff ff ff ff ff 00 00 00 00 ff ff ff ff |................| 000f49f0 ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 |................| 000f4a00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 000f4fe0 ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 |................| 000f4ff0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|Allerdings gibt es auch einige Unterschiede, im Dateikopf steht am Index 0x224 der Wert 00 00 00 00, am Dateiende hingegen 7c 32 05 00. Hmm... Wie groß sind alle Sektionen zwischen dem Dateikopf und seinem Ende? 0Xf4800 – 0xa0800 = 0x54000. Wenn ich den Wert 0x5327c vom Dateiende nehme und aufrunde, entspricht es diesem Wert. Das Feld könnte der Dateigröße entsprechen.
Eine Kopie der ersten Datei
Die nächste Sektion scheint der Dateikopf einer Datei namens "fpcp" zu sein.
pre> 000f5000 01 ff ff ff 01 00 00 00 ff ff 66 70 63 70 00 00 |..........fpcp..| 000f5010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 000f5100 00 00 00 00 00 00 00 00 00 00 ff ff 80 f1 00 00 |................| 000f5100 00 00 00 00 00 00 00 00 00 00 ff ff 80 f1 00 00 |................| 000f5110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000f5120 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff |................| 000f5130 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|Der Inhalt entspricht dem der Datei "fp". Womöglich handelt es sich um ein Backup des FPGA-Bitstreams in einer zweiten Datei.
pre> 000f5800 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 000f5810 55 99 aa 66 0c 85 00 e0 04 00 8c 85 c0 01 8c 82 |U..f............| 000f5820 bc 00 8c 86 90 77 8c 43 20 00 08 c9 0c 87 00 f3 |.....w.C .......| 000f5830 0c 83 00 81 04 00 04 00 04 00 04 00 04 00 04 00 |................| 000f5840 04 00 04 00 04 00 04 00 04 00 04 00 04 00 04 00 |................|Das "fpcp"-Dateiende sieht genauso aus wie für "fp" mit der Dateiangabe am Index 0x124:
pre> 00149800 01 ff ff ff 01 00 00 00 ff ff 66 70 63 70 00 00 |..........fpcp..| 00149810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00149900 00 00 00 00 00 00 00 00 00 00 ff ff 80 f1 00 00 |................| 00149910 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00149920 00 00 00 00 7c 32 05 00 ff ff ff ff ff ff ff ff |....|2..........| 00149930 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|Die Param-Datei
Den Dateien "fp" und "fpcp" folgt ein Kopf für die Datei "param".
pre> 0014a000 01 ff ff ff 01 00 00 00 ff ff 70 61 72 61 6d 00 |..........param.| 0014a010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 0014a100 00 00 00 00 00 00 00 00 00 00 ff ff 80 f1 00 00 |................| 0014a110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0014a120 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff |................| 0014a130 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 0014a1c0 ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 |................| 0014a1d0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0014a1e0 ff ff ff ff ff ff ff ff 00 00 00 00 ff ff ff ff |................| 0014a1f0 ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 |................| 0014a200 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 0014a7e0 ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 |................| 0014a7f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|Darauf folgt der Dateiinhalt.
pre> 0014a800 00 00 00 00 00 00 00 00 30 30 30 30 30 30 30 30 |........00000000| 0014a810 30 30 30 30 30 30 30 30 00 00 00 00 00 00 00 00 |00000000........| 0014a820 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 0014a990 00 00 00 00 00 00 00 00 cd ab cd ab 00 00 39 30 |..............90| 0014a9a0 7c 32 05 00 22 6c 62 40 00 00 00 00 00 00 00 00 ||2.."lb@........| 0014a9b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0014a9c0 00 00 00 00 00 00 00 00 e9 5b c9 92 00 00 00 00 |.........[......| 0014a9d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 0014afff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|Und schließlich das Dateiende.
pre> 0014b000 01 ff ff ff 01 00 00 00 ff ff 70 61 72 61 6d 00 |..........param.| 0014b010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 0014b100 00 00 00 00 00 00 00 00 00 00 ff ff 80 f1 00 00 |................| 0014b110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0014b120 00 00 00 00 cc 01 00 00 ff ff ff ff ff ff ff ff |................| 0014b130 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 0014b1c0 ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 |................| 0014b1d0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0014b1e0 ff ff ff ff ff ff ff ff 00 00 00 00 ff ff ff ff |................| 0014b1f0 ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 |................| 0014b200 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 0014b7e0 ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 |................| 0014b7f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|Die Dateigröße am Index 0x124 beträgt gerade mal 0x1cc, es ist eine sehr kleine Datei.
Der "param"-Datei folgt eine identische Datei namens "paramcp", genauso wie bei "fp".
Weitere Dateien
Die nächste Datei heißt "tx" mit jeder Menge Text, wahrscheinlich handelt es sich um Kalibrierungsdaten:
Hier fällt mir auf, dass die letzten 20 Byte eines Sektors immer nur Nullen enthalten. Auch bei den anderen Dateien wie "fp" ist das der Fall. Aus unklaren Gründen benutzt der Hersteller nur 2028 Byte pro Sektion statt 2048. Seltsam, aber ich nehme es erstmal hin.
Dateien extrahieren
Mittlerweile glaube ich, das Schema verstanden zu haben. Eine Datei besteht aus einer Kopf-Sektion, einer Datensektion und einer Fuß-Sektion. Nur die ersten 2028 Byte einer Sektion enthalten auch Daten. Jeder Datei folgt eine identische Kopie mit dem Anhang "cp" im Dateinamen sowie einer "param"- und "paramcp"-Datei.
Als fauler Programmierer schreibe ich mir natürlich ein Werkzeug, um die Extraktion zu automatisieren.
Ein Python-Programm durchläuft das Abbild und extrahiert alle Dateien, die es finden kann. Es parst die Kopf-Sektion und kopiert die Nutzdaten heraus, bis es auf eine Fuß-Sektion trifft. Zum Schluss prüft das Programm, ob die Angaben der Dateigröße im Fuß mit den extrahierten Daten übereinstimmen und dass die Daten danach alle Null sind. Die Nutzdaten werden schließlich in eine Datei geschrieben. Existiert bereits eine Datei mit diesem Namen, wird eine Zahl angehängt.
Nachdem ich das Programm ausgeführt habe, liegen einige Dateien in meinem Testverzeichnis. Das Unix-Programm "file" kann Dateien analysieren und das Dateiformat ermitteln. Ich lasse es über die Dateien laufen:
pre> fp: AIX core file fulldump 32-bit 64-bit fpcp: AIX core file fulldump 32-bit 64-bit param: data paramcp: data tx: ISO-8859 text, with CRLF line terminators txcp: ISO-8859 text, with CRLF line terminators param.0: data paramcp.0: data bmp: PC bitmap, Windows 3.x format, 800 x 600 x 8 bmpcp: PC bitmap, Windows 3.x format, 800 x 600 x 8 param.1: data paramcp.1: data hz: data hzcp: data param.2: data paramcp.2: data os: data oscp: data param.3: data paramcp.3: data me: Little-endian UTF-16 Unicode text, with CRLF line terminators mecp: Little-endian UTF-16 Unicode text, with CRLF line terminators param.4: data paramcp.4: data hlp: Little-endian UTF-16 Unicode text, with CRLF line terminators hlpcp: Little-endian UTF-16 Unicode text, with CRLF line terminators param.5: data paramcp.5: dataDass es sich um die "fp"-Datei um einen FPGA-Bitstrom handelt, weiß ich, deshalb irritiert mich die "AIX core"-Aussage nicht sehr. "bmp" soll ein Bild im Windows-Bitmap-Format sein. Schauen wir es uns an:
Das Boot-Logo. Schön! Und da ich das Bild laden konnte und es keine sichtbaren Störungen aufweist, kann ich sicher sein, dass mein Dateiextraktor funktioniert.
Die Datei "hz" enthält jede Menge binärer Daten. Sie ist ungefähr 3 MByte groß, vielleicht ist sie wichtig. Aber ich habe keine Idee, was sie tut.
Eine andere 3 MByte große Datei heißt "os" und enthält wahrscheinlich ARM-Maschinencode. Ausgehend vom Namen handelt es sich wohl um das Betriebssystem des Oszilloskops.
Die "me"- und "hlp"-Dateien enthalten Unicode-Text, darin befinden sich wohl die Menü-Einträge und Hilfetexte.
Danach liefert der Extraktor nur noch Unsinn. Es stehen weitere Daten im Speicherabbild, aber die "param"-Datei danach hat keine Fuß-Sektion. Nachdem ich die Kopfsektion mit der vorherigen verglichen habe, fällt mir auf, dass am Index 0x124 die Dateigröße steht und nicht mehr Nullen wie vorher. Wenn die Dateigröße im Kopf steht, dann ist der Fuß wohl nicht notwendig. Ich überarbeite mein Programm und mache weiter.
Allerdings wird es schlimmer. Die nächste Sektion ist einfach Müll. Danach gibt es weitere Kopfsektionen für Dateien namens "savewave" und mehrere Kopien von Dateien namens "table1", "tale" und "table3". Ich verändere mein Programm, um solche merkwürdigen Sektionen zu ignorieren und nach der nächsten Kopfsektion Ausschau zu halten.
Ich bin mir nicht sicher, ob der "Müll" nicht irgendetwas Wichtiges enthält. Ich hoffe, ich muss mir darum keine Gedanken machen.
Der Inhalt der Param-Datei
Als ich den Second-Stage-Bootloader disassembliert habe, finde ich einen Abschnitt zur Überprüfung mit einer Prüfsumme des Dateiinhaltes. Aber womit wurde die Prüfsumme verglichen?
Nach ein wenig Nachdenken wird mir klar, dass sie in der Param-Datei stehen muss. Für die Datei "os" gibt es dort fünf Einträge von jeweils 32 Bit Länge am Index 0x124. Der erste Eintrag ist ein Flag. Wenn es den Wert 0xabcdabcd enthält, heißt das, die Datei wurde in den Flash-Speicher geschrieben. Die folgenden Einträge enthalten die Speicheradresse, eine weitere Adresse mit unklarem Zweck, schließlich die Dateigröße und die Prüfsumme.
Dann folgen weitere Abschnitte mit den entsprechenden Einträgen für die Dateien "hz", "tx", "me", "hlp", "fp" und "bmp".
Für jede Datei und ihre Kopie, die in den Flash-Speicher geschrieben wird, wird auch eine neue Param- und Paramcp-Datei erzeugt. Das erklärt, warum die Datei mehrfach auftaucht.
Mir gelingt es sogar, die Funktion für die Prüfsummen-Ermittlung zu disassemblieren. Es handelt sich um eine CRC32-Variante. Ich ergänzte mein Python-Programm.
Ich teste die Prüfsummen-Funktion mit den Werten aus der Param-Datei und sie stimmen tatsächlich überein. Mein Extraktionsprogramm funktioniert also tatsächlich, wie es sollte.
Die hässliche Erkenntnis
Entsprechend einiger Zeichenketten aus dem Speicherabbild sieht es so aus, als ob Owon als Dateisystem Yaff verwenden wollte, sich dann aber entschied, ein eigenes zu implementieren. Ich würde es nicht einmal direkt als Dateisystem bezeichnen. Es ist sehr simpel, kümmert sich nicht um eine gleichmäßige Verteilung der Schreibvorgänge und es gibt auch keinen Mechanismus, Dateien zu überschreiben, wenn das Speichermedium voll ist.
Das ist für mich soweit in Ordnung, wenn nur eine Sammlung statischer Dateien erforderlich ist. Problematischer finde ich, dass es keine Absicherung gegen Speicherfehler gibt. Wie ich schon schrieb: NAND-Speicherbausteine sind unzuverlässig. Es wird zu spontanen Bitfehlern kommen, selbst wenn der Baustein nie beschrieben wird. Deshalb ist eine Fehlerkorrektur unbedingt erforderlich.
Das Dateisystem aber hat keinerlei Fehlerkorrektur. Da in jeder Speichersektion 20 Byte nur mit Nullen aufgefüllt waren, vermute ich, dass ursprünglich der Einsatz von Fehlerkorrekturcodes (ECC) vorgesehen war, aber niemals implementiert wurde. Selbst das wäre aber unsinnig gewesen. Flash-Speicherbausteine haben 64 Byte für OOB-Daten pro Sektion(öffnet im neuen Fenster), die für Fehlerkorrekturcodes vorgesehen sind. Später lasse ich mir die OOB-Daten unter Linux ausgeben – sie sind komplett leer und unbenutzt.
Außerdem sehe ich keine Methode, wie vernünftig mit schlechten Sektoren umgegangen wird. Eventuell wird einfach nach dem nächsten korrekten Kopfsektor gesucht, wie ich es auch in meinem Programm getan habe, als ich auf die Mülldaten stieß. Ich bin mir auch nicht sicher, wie die Dateisystem-Implementierung damit umgeht, wenn Kopfdaten in einer Datensektion auftauchen.
Zumindest führt der Bootloader eine Fehlererkennung durch. Er führt für wichtige Dateien, wie die Betriebssystem-Datei, eine Prüfsummenberechnung durch. Ist sie falsch, wird die Kopie der Datei benutzt. Das ist eine Art Dateikorrektur, da die Daten vom Original oder der Kopie repariert werden können, allerdings nicht, wenn beide Dateien kaputt sind.
Und das Risiko ist hoch, dass sowohl die Originaldatei als auch ihre Kopie Fehler enthalten. Hohes Risiko heißt, dass es nicht jedes Oszilloskop betreffen wird. Aber bei einem von hundert ist es realistisch. Als Hersteller wäre mir das zu viel, wenn ich Tausende davon herstellen würde. Insbesondere da die Umsetzung der Fehlerkorrektur einfach gewesen wäre. Es ist nicht schwer, nach "ECC algorithm source" zu suchen und es umzusetzen. Der Samsung-SoC hat sogar eine Fehlerkorrektur mit Hilfe der OOB-Daten implementiert, die Entwickler mussten lediglich den entsprechenden Schalter setzen, wenn Sektionen im Flash-Speicher geschrieben und gelesen werden.
Auch für den Second-Stage-Bootloader fehlt die Fehlerkorrektur. Das Risiko von Speicherfehlern in 75 KByte ist zwar nur gering, aber es wäre einfach umzusetzen gewesen.
Ich habe inzwischen das Gefühl, dass Owon zwar vernünftige Hardware baut, aber die Software nicht auf dem Stand der Technik ist, und das enttäuscht mich.
Wie es weitergeht
Ich konnte bis jetzt eine ganze Reihe von Dateien extrahieren, insbesondere die für mich wichtigen Dateien des Betriebssystems und des FPGA-Bitstroms. Ich sollte das Betriebssystem in Medusa laden und weiter analysieren können.
Wollte ich nur das Oszilloskop manipulieren, um zum Beispiel eine höhere Bandbreite zu erzielen, könnte ich nach der Funktion suchen, die die entsprechenden Filter im Verstärker ansteuert. Das sollte nicht zu schwer sein. Dann würde ich die "os"-Datei ändern und die Prüfsumme in der "param"-Datei anpassen.
Aber daran bin ich gar nicht interessiert. Ich will verstehen, wie die Hardware im Oszilloskop funktioniert, und darauf meine eigene Firmware und meinen eigenen FPGA-Code zum Laufen bekommen. Deshalb konzentriere ich mich auf die Bestandteile im Betriebssystem, die die Hardware steuern, und werde die übrigen Teile des originalen Betriebssystems weitgehend ignorieren.
Im nächsten Teil der fünfteiligen Artikelserie wird Christer Weinigel die GPIO-Pins des SoC erkunden, um später möglichst viele Bestandteile des Oszilloskops kontrollieren zu können.
Christer Weinigel(öffnet im neuen Fenster) ist freiberuflicher Ingenieur in seinem eigenen Unternehmen (Weinigel Ingenjörsbyrå AB). Er entwickelt hardwarenahe Software, arbeitet mit Embedded-Betriebssystemen und spielt deshalb auch öfters direkt mit Hardware.
Diese Artikelserie erschien zuerst in seinem Blog. Mit seiner Erlaubnis hat Golem.de seine Artikel ins Deutsche übersetzt und dabei einige Kürzungen und Ergänzungen vorgenommen, damit der Inhalt auch Einsteigern verständlich ist.
- Anzeige Hier geht es zu Linux: Das umfassende Handbuch bei Amazon Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.