Hallo zusammen, ich habe eine alte embedded Software (Mitte der 90er), die ich auf eine moderne Prozessorarchitektur portieren möchte (68HC05 auf Cortex M4). Leider habe ich den Quellcode nicht zur Verfügung. Deshalb habe ich die Binärdaten disassembliert und dann händisch nach C decompiliert. Anschließend habe ich alles auf der neuen Prozessorarchitektur compiliert. Soweit schaut alles auch ganz gut aus. Allerdings würde ich gerne nachweisen, dass bei meiner Arbeit nichts daneben gegangen ist. Welche Möglichkeit habe ich, den Code zu verifizieren? Macht man das per Unit Test o.ä.? Ich habe ein Verständnis vom Code, allerdings schätze ich, dass es dennoch recht aufwändig sein dürfte, Testvektoren zu schreiben. Besteht die Möglichkeit, das zu automatisieren? Etwa, indem ich eine Code-Coverage vorgebe und mein Tool mir daraus automatisch die Vektoren erzeugt? Das Ganze ist Hobby und dementsprechend eng ist mein Budget. 100€ oder so wäre es mir wert. Habt ihr eine Idee? Grüße Steffen
Das alte Gerät hat funktioniert und eine bestimmte Aufgabe erfüllt? Dann schaue, ob das neue Gerät ebenso diesselbe Aufgabe erfüllt! => 100 Euro bereits aufgebraucht, sorry. Tests, die die Funktionen der Geräte (von außen) verifizieren. Das Gerät stellt dann eine Black Box dar. Tests kann man dann auf beiden Geräten laufen lassen und Ergebnis vergleichen. Unit Tests auf Source Code Ebene gehen nur auf dem neuen Gerät, weil vom alten Gerät ja kein Source Code existiert. Manches hängt vielleicht auch etwas davon ab, was das Gerät überhaupt macht, z.B. muss eine PWM erzeugt werden? Oder lauscht es auf der seriellen Schnittstelle, und reagiert dann demensprechend. Oder blinken LEDs in einem bestimmten Takt und Takt soll beibehalten werden? Oder oder oder ...?
:
Bearbeitet durch User
Martin M. schrieb: > Unit Tests auf Source Code Ebene gehen nur auf dem neuen Gerät, > weil vom alten Gerät ja kein Source Code existiert. Es gibt keinen Highlevel Sourcecode, das stimmt. Aber es gibt disassemblierten Assemblercode und damit dürfte es doch gehen. Ich dachte daran, auf Unit Ebene zu testen. Das heißt, ich nehme mir ein Unterprogramm und lasse mir Testvektoren generieren, die die notwendige Code Coverage erreichen. Die lasse ich dann auf der alten Prozessorarchitektur laufen und zeichne die Ergebnisvektoren auf. Anschließend wechsele ich auf meine neue Architektur und wiederhole alles. Die dortigen Ergebnisvektoren vergleiche ich dann mit denen der alten Architektur. Stimmen sie überein, ist alles in Ordnung. Soweit die Theorie. Wie kann ich das in die Praxis umsetzen? Gibt es Tools, die das automatisieren?
:
Bearbeitet durch User
Da Embedded-Anwendugen nicht nur aus einem Programm bestehen, das im leeren Raum vor sich hinwerkelt, sondern mit der jeweiligen Peripherie des Microcontrollers arbeitet, ist eine "Verifikation" auf reiner Funktionsebene nicht zielführend. Denn eine Funktion, die fehlerfrei einen Timer des 68hc05 initialisiert, macht auf einem Cortex M4 einfach ... gar nix. Für die Portierung eines derartigen Projektes muss man die Gesamtheit analysieren, welche Peripheriefunktionen werden genutzt und welche Spezialitäten der Peripheriefunktionen müssen dabei berücksichtigt werden. Erst wenn man das genau verstanden hat, kann man sich daran versuchen, in der Peripherie des neuen Microcontrollers Äquivalenzen zu suchen, um mit der das angestrebte Verhalten der alten Peripherie nachbilden zu können. Das ist also um einiges komplexer als nur "Unterprogramme" daraufhin zu untersuchen, ob sie z.B. drei Werte korrekt miteinander verrechnen.
Harald K. schrieb: > Da Embedded-Anwendugen nicht nur aus einem Programm bestehen, das im > leeren Raum vor sich hinwerkelt, sondern mit der jeweiligen Peripherie > des Microcontrollers arbeitet, ist eine "Verifikation" auf reiner > Funktionsebene nicht zielführend. Du hast recht, natürlich gibt es Programmteile, die auf die Peripherie zugreifen und ein 1:1 Unit Test ist für sie nicht sinnvoll. Allerdings liegen diese Programmteile im Abstraction Layer (mcal) und machen vielleicht 10% aus. Die übrigen 90% sind Funktionscode und vollständig abstrahiert. Kennt jemand eine Möglichkeit, diesen Code quer zu testen?
:
Bearbeitet durch User
Ich habe mal die KI befragt, und die schlägt doch allen ernstes vor, dafür KI einzusetzen ;) Ob dein Code allerdings dafür geeignet ist, wirst du selber rausfinden müssen. Oliver
Steffen H. schrieb: > > Die übrigen 90% sind Funktionscode und vollständig abstrahiert. Kennt > jemand eine Möglichkeit, diesen Code quer zu testen? Wenn es hauptsächlich darum geht dass aus Eingabedaten Ausgabedaten erzeugt werden kann man das z.B. auf Binärcode-Ebene mit einem Simulator testen. Man läßt die enstprechenden Funktionen im Simulator laufen und vergleicht die Ergebnisse, auch die Ausführungszeiten der Funktionen kann man damit vergleichen. Ein teures/professionelles Tool das so etwas kann wäre z.B. TRACE32, sehr viele der unterstützten CPUs lassen sich in einer fast identischen Umgebung wie im Debugger mit der echten Hardware auch simulieren (ohne dass die Hardware dazu benötigt wird). Es gibt natürlich auch noch viel andere Simulatoren, eventuell reicht ja schon QEMU. Die Frage ist halt ob man es für die gewünschten CPUs mit möglichst nur einem Simulator zu tun hat und nicht unterschiedliche Tools benötigt.
:
Bearbeitet durch User
Steffen H. schrieb: > Leider habe ich den Quellcode nicht zur Verfügung. Deshalb habe ich die > Binärdaten disassembliert und dann händisch nach C decompiliert. > Anschließend habe ich alles auf der neuen Prozessorarchitektur > compiliert. Respekt!
Steffen H. schrieb: > Deshalb habe ich die Binärdaten disassembliert und dann händisch nach C > decompiliert Respekt auch von mir. Welche Werkzeuge hat du genutzt? Du hast das doch kaum "zu Fuß" gemacht? Klar, nacharbeiten schon. Darf man wissen um was für ein Projekt es sich handelt? Wer bezahlt soetwas für eine Kaffeemaschine ;) Das muss sich ja lohnen.
Steffen H. schrieb: > Allerdings > liegen diese Programmteile im Abstraction Layer (mcal) und machen > vielleicht 10% aus. Du glaubst, daß derjenige, der vor Ewigkeiten mal den Code für Deinen 68hc05 entwickelt hat, sich an diese Designrichtlinie gehalten und einen sauberen "abstraction layer" verwendet hat? Optimismus im Wandel der Zeiten.
Möchtest Du prüfen, ob der C-Code auf dem HC05 das gleich macht wie der Assembler-Code? Hast Du "Ausgaben"? Eine serielle Schnittstelle, ein Display, etc? Solange das "schnell" ist, kannst Du Testvektoren auch als vollständige Kombination sinnvoller Einzelwerte aller Eingangsgrößen per x-fach verschachtelter Schleife durchlaufen lassen. Einmal die Assembler-Routinen und dann den compilierten C-Code auf HC05, PC und neuer CPU. Läuft der C-Code auf dem HC05 denn schon "rund"?
900ss schrieb: > Welche Werkzeuge hat du genutzt? Du hast das doch > kaum "zu Fuß" gemacht? Klar, nacharbeiten schon. Ja, alles zu Fuß. Der Instruction Set des 68HC05 ist super simpel, da geht das recht gut. Je nach Umfang kostet das dann die Abendstunden von ein paar Wochen. Aber das geht schon. Harald K. schrieb: > Du glaubst, daß derjenige, der vor Ewigkeiten mal den Code für Deinen > 68hc05 entwickelt hat, sich an diese Designrichtlinie gehalten und einen > sauberen "abstraction layer" verwendet hat? Ich habe den Code hier vor mir liegen. Deshalb glaube ich es nicht, ich weiß es :)
@all: Die Box mit dem neuen Prozessor anschließen und mal schauen, ob sie so tut wie die alte, habe ich bereits gemacht. Sie tut erst einmal, was sie soll. Aber ich kann unmöglich alle Permutationen der Eingangssignale abfahren. Es ist ja auch ein speicherndes, zeitinvariantes System. Deshalb möchte ich auf Unit Ebene wirklich nachweisen, dass die neue Box sich funktional identisch zur alten verhält (validieren vs. verifizieren). Wenn ich das alles händisch machen muss, beispielsweise in einer Simulationsumgebung, werde ich ewig daran sitzen. Das muss doch automatisiert gehen. Ein "einfacher" Back-to-Back Test. Oder eben halt nicht so "einfach"... Natürlich wäre es auch ein Weg, meinen C-Code wieder in Assembler zu übersetzen und mit dem Disassembly des 68HC05 zu vergleichen. Danke für den Anstoß. Eventuell ist das am Ende tatsächlich die leichtere Methode.
:
Bearbeitet durch User
Und warum nicht den 68HC05 simulieren und damit dann Testvektoren vergleichen? Es gibt mehr als einen Simulator für diese einfache CPU, u.a. den hier: https://github.com/philpem/m68emu Vermutlich könnte damit sagar ein aktueller Mikrocontroller das ursprüngliche Binary in vergleichbarer Geschwindigkeit laufen lassen (abgesehen von der Peripherie, die bei der 68HC05 Familie aber ebenfalls sehr überschaubar ist).
Steffen H. schrieb: > alle Permutationen der Eingangssignale Dann gib mal einen Blick auf die Art und Umfang der Eingangswerte und die zeitliche Komponente. (Ich hab das mal für ein refaktoriertes Modul gemacht. Es gab etwa 10 Eingangsgrößen, die meisten nur einstellige Werte, manche bis mehrere Hundert. Und hunderte Variablen, die demnach zu setzen waren, allerdings ohne zeitkomponente. Die innerste Schleife lief ein paar Mrd Mal durch und der output wurde per memcmp geprüft.)
Emulierst du dann eigentlich die Befehle des 68HC05 auf dem Cortex-M4?
Martin M. schrieb: > Emulierst du dann eigentlich die Befehle des 68HC05 auf dem Cortex-M4? Im Eingangspost steht doch genau, was er gemacht hat. Also keine Emulation. Möglich wäre das sicher auch.
900ss schrieb: > Martin M. schrieb: >> Emulierst du dann eigentlich die Befehle des 68HC05 auf dem Cortex-M4? > > Im Eingangspost steht doch genau, was er gemacht hat. Also keine > Emulation. > > Möglich wäre das sicher auch. > "und dann händisch nach C decompiliert" Wenn ich Folgendes habe: LDX #0 Loop4711: INX CPX #10 BNE Loop4711 dann gibt es mindestens 2 Varianten, wie es dann in C aussehen könnte: (vom Prinzip her) A) Die schönere Variante (mit Hirnschmalz, weil man Code verstehen muss) for (i = 0; i < 10; ++i) { … } B) Die "Raw"-Variante (mehr oder weniger mechanische Übersetzung) Execute_LDX(ExecutionEnvironment, 0) Loop4711: Execute_INX(ExecutionEnvironment) Execute_CPX(ExecutionEnvironment, 10) if(Execute_BNE(ExecutionEnvironment)) goto Loop4711; An eine Emulation zur Laufzeit hatte ich zuerst gar nicht gedacht...
Dieter S. schrieb: > Und warum nicht den 68HC05 simulieren und damit dann Testvektoren > vergleichen? > Vermutlich könnte damit sagar ein aktueller Mikrocontroller das > ursprüngliche Binary in vergleichbarer Geschwindigkeit laufen lassen Naja eine Emulation braucht deutlich mehr Speicher und ist gerne Faktor 0.01 "schneller" als das Orginal. resp. braucht im ∅ 100 - 1000 Taktzyklen für die Emulation eines einzigen Orginaltaktes. Abgesehen davon, das sich eine Emulation bei priorisierten Programmablauf (bspw. Exception/IRQ und JUMP gleichzeitig) prinzipbedingt unterscheiden muß. Die Verifikation des Simulator ist damit auch komplexer als eine funktionalle Re-Implemantation gegen die Requirements zu prüfen. Bei "Schweinereien" wie selbstmodifizierenden Code kommt allerdings kaum um (vollständige) Emulation herum.
Die Idee, das per Emulation zu lösen, habe ich schnell wieder verworfen. Mein neuer Prozessor unterstützt "nur" den THUMB Befehlssatz und der ist wirklich sehr unterschiedlich zu dem des 68HC05. (Ganz im Gegensatz zum ARM Befehlssatz - der hätte wirklich gut gepasst.) Ich werde mir schauen, dass ich meinen C-Code verifiziere. Also ihn, wie oben vorgeschlagen, wieder in Assembler übersetzen und dann mit dem Disassembly aus den originalen Binärdaten vergleichen. Wer weiß, vielleicht erwische ich mit viel Glück den Compiler, der damals Mitte der 90er im Projekt verwendet wurde. Dann ähnelt sich der Code und ich habe es leichter. Vielen Dank für Eure Tipps! Grüße Steffen
:
Bearbeitet durch User
Steffen H. schrieb: > Wer weiß, > vielleicht erwische ich mit viel Glück den Compiler, der damals Mitte > der 90er im Projekt verwendet wurde. Du weißt mehr über das Projekt als daß Du nur den Binärdump hast? Oder was verleitet Dich dazu, anzunehmen, daß das in C geschrieben wurde? So etwas wie 68xx wurde auch oft direkt in Assembler programmiert ...
Da hast Du recht. Gut möglich, dass das Projekt direkt in Assembler geschrieben wurde.
Steffen H. schrieb: > Die Idee, das per Emulation zu lösen, habe ich schnell wieder verworfen. > Mein neuer Prozessor unterstützt "nur" den THUMB Befehlssatz und der ist > wirklich sehr unterschiedlich zu dem des 68HC05. (Ganz im Gegensatz zum > ARM Befehlssatz - der hätte wirklich gut gepasst.) Was hat der Befehlssatz des Zielsystems mit einem Emulator zu tun, der üblicherweise in einer Hochsprache wie z.B. C geschrieben ist?
Ein einfacher, schneller Test mit dem weiter oben verlinktem 68HC05 Emulator auf einem Mikrocontroller: - Testsystem: STM32 Nucleo Board mit einem STM32F446RE (Cortex M4) - CPU Takt 180 MHz - keine Optimierung beim Compiler (GCC) - Speicherbedarf (RAM und Flash) ca. 34 KByte, davon 8 KByte RAM für den Speicherbereich des 68HC05 (Code/RAM/IO) - der unten stehende Testcode braucht für 100000 68HC05 Taktzyklen ca. 81 ms, also ca. 0.8 µs je 68HC05 Taktzyklus Der Testcode:
1 | lda #20 |
2 | jsr delay |
3 | forever: |
4 | bra forever |
5 | |
6 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
7 | |
8 | delay: |
9 | stx save_x |
10 | |
11 | delay_loop1: |
12 | |
13 | ldx #$FA |
14 | delay_loop2: |
15 | nop |
16 | decx |
17 | bne delay_loop2 |
18 | |
19 | deca |
20 | bne delay_loop1 |
21 | |
22 | ldx save_x |
23 | rts |
:
Bearbeitet durch User
Dieter S. schrieb: > - keine Optimierung beim Compiler (GCC) Und wie sieht das Ergebnis bei Optimierung auf Ausführungsgeschwindigkeit aus?
Der obige Testcode braucht ca. 33 ms mit "-Ofast", also ca. 0.3 µs je 68HC05 Taktzyklus. Aber es geht nicht darum wie gut die Optimierung des Compiler ist sondern darum dass ein aktueller Mikrocontroller relativ problemlos den 68HC05 schnell genug emulieren kann (ohne alle Details wie IO oder Interrupts zu berücksichtigen, es hängt immer von Einzelfall ab ob so eine Emulation auch Sinn macht). Bei viele dieser 68HC05 liegt der Taktzyklus bei 0.5 µs bis 1 µs, entsprechend 2 MHz bzw. 1 MHz (entspricht einem Quarz mit 4 MHz bzw. 2 MHz am 68HC05). Der obige 68HC05 Testcode für den Emulator ist eine ca. 20 ms Verzögerung eines 68HC05 mit 4 MHz Quarz (ca. 40000 Taktzyklen), der Emulator mit "-Ofast" braucht dafür ca. 14 ms. Und wenn es noch schneller sein soll nimmt man halt einen Mikrocontroller mit 400 MHz und mehr (z.B. einen der STM32Hxx). Anderer Emulatoren für den 68HC05 gibt es natürlich auch, etwa der aus MAME.
Decompiler sind nicht besonders zuverlässig. Als Beispiel, ich hab mal dieses Programm von mir dekompilieren lassen: https://github.com/Daniel-Abrecht/dpa-utils/blob/ba2e0cc211221e0565a09bda88234648fa0182c5/src/main/example/utf8.c https://dogbolt.org/?id=d365debe-8058-40a0-aeb6-09dfa4ef52d4 Das meiste wurde natürlich schon weg optimiert. Die verwendeten struct typen, die inline Funktionen, usw. von denen ist da natürlich nichts mehr zu sehen. Nun, wie schlagen sich die Decompiler. Hex-Rays macht das da noch am besten. Die Typen usw. stimmen so ungefähr, das würde funktionieren.
1 | __int64 v7; // [rsp+18h] [rbp-10h] BYREF |
2 | v7 = 0xBA989FF024LL; |
3 | dpa_u_puts_p_isra_0(4u, (char *)&v7 + 1); |
Ghidra castet nach Long. Exotisch, sollte aber trotzdem noch gehen.
1 | undefined8 local_10; |
2 | local_10 = 0xba989ff024; |
3 | dpa_u_puts_p_isra_0(4,(long)&local_10 + 1); |
Aber die anderen Decompiler versagen alle komplett. BinaryNinja versagt beim Casten komplett:
1 | int64_t var_10 = 0xba989ff024; |
2 | dpa_u_puts_p.isra.0(4, &*var_10[1]); |
angr castet noch falscher, und vergisst dazu noch den Offset von einem byte:
1 | unsigned long long v3; // [bp-0x10] |
2 | v3 = 801424535588; |
3 | dpa_u_puts_p.isra.0(4, &(char)v3); |
dewolf initilisiert die Variable nicht, castet nicht, und der offset passt dann auch nicht:
1 | long var_3; |
2 | dpa_u_puts_p_isra_0(4UL, &var_3 + 1L); |
Reko initialisiert erst ein feld einer nicht-existenten Variable, dann übergibt es statdessen eine uninitialisierte Variable:
1 | qwLoc17.qw0007 = 801424535588; |
2 | word64 qwLoc0F; |
3 | dpa_u_puts_p.isra.0(0x04, &qwLoc0F); |
Relyze übergibt ein 7 byte grosses char array. Das gefällt mir eigentlich. Aber leider initialisiert er es nicht:
1 | uint8_t local_0xF[7]; // [rsp-15] |
2 | dpa_u_puts_p.isra.0( 4, &local_0xF ); |
RetDec übergibt eine uninitialisierte Variable:
1 | int64_t v4; // bp-15, 0x1060 |
2 | dpa_u_puts_p_isra_0(4, &v4); |
Und was Snowman da veranstaltet, das wissen die götter:
1 | int64_t main() { |
2 | void* rsp1; |
3 | void* rsp2; |
4 | void* rsp3; |
5 | |
6 | rsp1 = reinterpret_cast<void*>(reinterpret_cast<int64_t>(__zero_stack_offset()) - 40); |
7 | dpa_u_puts_p_isra_0(1, reinterpret_cast<int64_t>(rsp1) + 1); |
8 | rsp2 = reinterpret_cast<void*>(reinterpret_cast<int64_t>(rsp1) - 8 + 8); |
9 | dpa_u_puts_p_isra_0(2, reinterpret_cast<int64_t>(rsp2) + 9); |
10 | rsp3 = reinterpret_cast<void*>(reinterpret_cast<int64_t>(rsp2) - 8 + 8); |
11 | dpa_u_puts_p_isra_0(3, reinterpret_cast<int64_t>(rsp3) + 17); |
12 | dpa_u_puts_p_isra_0(4, reinterpret_cast<int64_t>(rsp3) - 8 + 8 + 25); |
13 | return 0; |
14 | }
|
Hex-Rays und Ghidra haben das hier zwar hin bekommen, aber auch da kann schnell mal mist raus kommen.
Steffen H. schrieb: > Aber ich kann unmöglich alle Permutationen der Eingangssignale > abfahren. So macht man das auch nicht. Ich gehe mal davon aus, daß Du die Funktion des Gerätes kennst und Dir zuerst mal einen Programmablaufplan erstellt hast. Und diesen PAP kannst Du dann einfach durchsteppen. Du mußt also nicht sämtliche Eingänge permutieren, sondern nur diejenigen auswerten, mit denen von einem erlaubten Zustand in alle erlaubten weiteren Zustände gewechselt wird. Es wird dabei durchaus passieren, daß man Bugs in der alten Firmware aufspürt, die in verbotene Zustände laufen. In der Regel schmeißt man den alten Assemblercode komplett weg und entwickelt nur die reine Funktionalität nach. Man kann ja jederzeit das alte Mustergerät bedienen, um noch unklare Details zu ermitteln. Steffen H. schrieb: > Natürlich wäre es auch ein Weg, meinen C-Code wieder in Assembler zu > übersetzen und mit dem Disassembly des 68HC05 zu vergleichen. Das dürfte so ziemlich das Unsinnigste sein. Damit verliert man sämtliche Kontrollmöglichkeiten und Plausibilitätsprüfungen. Darf man mal fragen, welchen Umfang das Projekt überhaupt hat (Binärgröße, RAM-Verbrauch, Codezeilen in C)?
:
Bearbeitet durch User
Steffen H. schrieb: > Welche Möglichkeit habe ich, den Code zu verifizieren? Verifizieren bzw. Testen bedeutet im Endeffekt einen Vergleich zwischen "Soll" und "Ist". Damit man diesen Vergleich anstellen kann, muss man wissen was das "System Under Test" denn tun soll. In dem konkreten Fall könnte man sich eventuell eine Referenz erstellen, indem man am alten System entsprechende Messungen durchführt. Sprich: -Eingang E1 auf Wert X setzen, den Wert von Ausgang A1 protokollieren -Eingang E1 auf Wert Y setzen, den Wert von Ausgang A1 protokollieren und so weiter. Dafür gibt es ja auch Logic Analyzer, um solche Messungen durchzuführen. Ich hoffe mal sehr, dass Dein unbekanntes, streng geheimes System nicht allzu viele Ein- und Ausgänge hat. :-)
Ein Test nur mit Testvektoren wird in der Praxis auf keinen Fall ausreichend sein. Reale Geräte haben oft Zeitbedingungen, die eingehalten werden müssen. Z.B. wird ein Gerät mit einem AT89C2051 nicht mehr laufen, wenn man es mit einem AT89LP2052 ausrüstet. Grund ist einfach, daß der LP die Instruktionen viel schneller ausführt und damit alle Abläufe zeitlich nicht mehr stimmen. Das Programm wird natürlich rein logisch gesehen, exakt gleich ausgeführt. Nur werden Eingänge viel zu früh abgefragt und Ausgänge viel zu kurz gesetzt. Eine Blink-LED wird nur kurz aufblitzen, Baudraten sind zu hoch usw. Im Idealfall hatte das alte Programm alle Zeitbedingungen bereits über einen Scheduler implementiert. Dann muß man nur die Zeitbasis für den Scheduler anpassen und alles sollte wieder laufen. Nur wird das typisch nicht der Fall gewesen sein. Oftmals wurden Zählschleifen oder NOPs verwendet und das überall im Code verstreut.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.