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?
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 ...?
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?
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.
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?
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.
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.
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?
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.
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.
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 :)
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.
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).
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.)
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.
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 ...
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?
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.
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
__int64v7;// [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
undefined8local_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_tvar_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
unsignedlonglongv3;// [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
longvar_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
word64qwLoc0F;
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_tlocal_0xF[7];// [rsp-15]
2
dpa_u_puts_p.isra.0(4,&local_0xF);
RetDec übergibt eine uninitialisierte Variable:
1
int64_tv4;// bp-15, 0x1060
2
dpa_u_puts_p_isra_0(4,&v4);
Und was Snowman da veranstaltet, das wissen die götter:
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.
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.