Maskierte Bugs in eingebettet

Stecker sind bei der Entwicklung von Software unvermeidlich. In einem Embedd können ihre großzügigen fünf Cent auch Hardwareprobleme verursachen, aber dies ist ein separater Song. Aber rein programmierte Hinterhalte, wenn man an einem scheinbar leeren Ort festsitzt ... Für mich gibt es drei Arten von Hinterhalten.

Der einfachste Weg ist, wenn das Handbuch, der Standard oder beispielsweise das Verfahren zum Konfigurieren der Bibliothek für Eisen nicht vollständig verstanden ist. Hier ist klar: Nicht alle Bewegungen sind erschöpft, Geduld und Arbeit, weitere fünf oder zwei Experimente, und es wird zum Leben erweckt. Oszilloskop und wissenschaftlicher Typ helfen.


Auswahl eines Frequenzteilers zur Konfiguration des CAN-Busses

Schlimmer noch, wenn das Problem ein Tippfehler oder ein Fehler in der Logik ist, den Sie erst sehen können, wenn Sie zwanzig Mal mit Ihren Augen durch diesen Ort gehen und Schritt für Schritt debuggen. Dann dämmert es, ein klangvoller Schlag auf die Stirn, ein Schrei: „Nun, du bist eine Art Babai!“, Schnitt. Es funktioniert.

Und eine düstere dritte Ansicht: eine Panne, die in einer fremden Bibliothek verankert ist und mit Eisen an der Kreuzung herauskriecht. Shakespeares Leidenschaften lassen das stetige Licht eines Monitors entstehen. „Warum, es kann nicht, das System kann sich nicht so verhalten, weil es niemals kann! Nun, wirklich! Ah ?! " Nein. Empfangen, unterschreiben.

Infolgedessen ist die Realität breiter, breiter und breiter als erwartet. Einige Beispiele:

Geschichte Nr. 1. MicroSD-Flash-Laufwerk und DMA funktionieren


Anamnese


Sie müssen die Daten in eine Datei auf der SD-Karte sichern. Natürlich habe ich weder Zeit noch Lust, das Dateisystem und den SDIO-Treiber selbst zu schreiben, also nehme ich die fertige Bibliothek. Ich habe es für Eisen eingerichtet und alles funktioniert gut. Zuerst. Und dann stellt sich heraus, dass die Daten wild aufgezeichnet werden: Die Volumes sind genau, aber in den Dateien selbst werden separate Paare von Tripelbytes dupliziert und verschwinden dann ohne Regelmäßigkeit. Nicht gut!

Experimente beginnen. Ich schreibe Testdaten - alles ist in Ordnung. Ich schreibe Kampf - eine Art Teufelei. Ich ändere die Größe der Datenpuffer, die Häufigkeit ihrer Leerung, Datenvorlagen sind nutzlos. In den Puffern selbst ist immer alles hervorragend, die Daten im Speicher sind überall das, was Sie brauchen. Und trotzdem Pannen auf einem Flash-Laufwerk - hier sind sie.

Es dauerte ein paar Tage, um den Hund zu graben.

Die Diagnose


Das Problem lag in der Interaktion der Bibliothek mit DMA- Geräten.

SD-Karten haben eine Besonderheit: Sie werden nur in Blöcken von 512 Bytes geschrieben. Zu diesem Zweck puffert die Bibliothek die Daten in einem 512-Byte-Array und wird beim Füllen von dort über DMA zum Flashen gespült. Aber!

Wenn ich ein Fragment, das größer als <512xN + leerer Speicherplatz im Bibliothekspuffer> Bytes ist, auf den Datensatz übertrage, tut die Bibliothek (offensichtlich, um den Speicher nicht hin und her zu verschieben) Folgendes: Sie füllt ihren Puffer auf und schreibt ihn in Flash und die nächsten 512xN Bytes werden direkt aus meinem Puffer in meinen DMA geworfen! Nun, wenn etwas unvollendet bleibt, wird es bis zum nächsten Mal wieder selbst kopiert.

Und alles wäre in Ordnung, aber der DMA-Controller verlangt, dass die Daten im Speicher abgelegt werden, der an einer 4-Byte-Grenze ausgerichtet ist. Der Bibliothekspuffer ist immer so ausgerichtet, dass die Sprache dies garantiert. Aber mit welcher Adresse beginnen nach dem Kopieren eines Teils der Daten die verbleibenden 512xN mit einem kleinen Byte bei mir - Gott weiß. Und die Bibliothek überprüft dies überhaupt nicht: Die Adresse wird so wie sie ist an den DMA-Controller übergeben.

"Sie haben etwas Unbeholfenes geschickt ... Ein Hund mit ihm." Die Steuerung setzt die unteren 2 Bits der übertragenen Adresse stillschweigend zurück. Und startet die Übertragung.


Die Adresse, anfangs kein Vielfaches von 4, wird durch ein Vielfaches ersetzt - voila, bis zu den letzten drei Bytes aus dem Bibliothekspuffer werden von mir in die Datei neu geschrieben, und die gleiche Anzahl von Bytes aus meinem Puffer geht spurlos verloren. Infolgedessen ist die Gesamtdatenmenge korrekt, die Vorgänge laufen reibungslos, aber die Festplatte ist Unsinn.

Behandlung


Ich musste unmittelbar vor dem Aufrufen der Hardware-Aufnahmefunktion einen weiteren Puffer hinzufügen. Wenn die Schreibadresse kein Vielfaches von 4 ist, werden die Daten zuerst darauf kopiert. Gleichzeitig erhöhte sich die Durchschnittsgeschwindigkeit aufgrund einer vernünftigen Wahl der Puffergröße. Natürlich hat es Gedächtnis gekostet, aber was sind 4 Kilobyte für einen guten Zweck, wenn Sie zu Ihrer Verfügung haben - grenzenlose 192!

Geschichte Nr. 2. Rantime und ein Haufen


Prolog


Nach der nächsten Änderung begann das Programm zu fallen, und irgendwie fiel es sehr schwer und warf den Prozessor in den Hard Fault- Handler. Und er warf es gleich nach dem Start dorthin, noch bevor die Ausführung zu main () kam, das heißt, keine einzige Zeile meines Codes hatte Zeit zum Ausführen.

Der erste Eindruck ist: "Der Biber ist tot, der Chip soll ersetzt werden." Und dann gab der Programmierer die Eiche. Aber nein, die alte Version der Firmware funktioniert stabil, aber die neue Version fällt stetig in einige dunkle Montagetiefen zwischen dem Start und meinem Code. Ich hatte keine Vermutungen, was für eine Häresie das war.

Kapitel 1


Hat dem Internet geholfen, zu sehen, wie man zumindest einige zusätzliche Informationen erhält. Das Verfahren zum Parsen der Konsequenzen eines harten Ausfalls wurde gegoogelt: Status der Register, Dump-Stack. Dopilil. Ich habe es benutzt.

Es stellte sich heraus, dass es aufgrund eines Betriebsfehlers auf dem Bus abstürzt. Ich entschied, dass dies wieder ein unausgewogener Zugang war - ein Problem des gleichen Typs wie in der ersten Geschichte, aber aus einer anderen Perspektive. Das Gegenteil ist jedoch der Ort, an dem der Fehler aufgetreten ist. Und es entstand in der Laufzeitbibliothek, dh im Code, der theoretisch an einem sonnigen Tag wie die blauen Flecken der Katze geleckt wurde.

Die Fortsetzung der Analyse zeigte, dass der Fehler eine Folge des Versuchs ist, lokale statische Variablen zu initialisieren.

Lyrischer Exkurs
In Anbetracht des zerlegten Codes fand ich übrigens gleichzeitig die Antwort auf eine Frage, die ich mir manchmal stellte, die aber zu faul war, um sofort zu googeln: Wie wird die Situation gelöst, wenn zwei oder mehr Threads gleichzeitig versuchen können, eine solche Variable zu initialisieren? Es stellte sich heraus, dass in diesem Fall der Compiler die Initialisierung mit Semaphoren arrangiert, um sicherzustellen, dass jeweils nur ein Thread die gesamte Prozedur durchläuft und der Rest wartet, bis der erste abgeschlossen ist. Dieses Verhalten wurde seit C ++ 11 standardisiert. Wussten Sie schon? Ich nicht.

Kapitel 2


Sobald die Laufzeit mit der Erstellung von Variablen beschäftigt ist, kann er nach Abschluss des Programms auch Destruktoren aufrufen (auch wenn das Programm die Arbeit nie tatsächlich abschließt, was für Mikrocontroller die absolute Norm ist). Dazu muss er irgendwo Informationen zu allen Variablen speichern, die er initialisiert hat.

Das ist genau dort, wo solche Informationen in einer Art interner Liste gespeichert sind, ist auch die Laufzeit gesunken. Da die Funktion malloc (), über die Speicher für die Elemente dieser Liste zugewiesen wurde und die gemäß dem Standard Blöcke erzeugt, die garantiert mindestens an der 8-Byte-Grenze ausgerichtet sind , nach einer n-ten Anzahl erfolgreicher Aufrufe ein Stück erzeugt, das an dieser Grenze nicht ausgerichtet ist.



Änderungen im neuen Firmware-Code brachen malloc ?! Aber wie ist das überhaupt möglich? Ich habe malloc nicht genau neu definiert, ich selbst brauche es nirgendwo anders!

Nützlich in den Compiler-Optionen, um nach einigen Schlüsselwörtern zu suchen, helfen, aber es wurde überall klar gesagt: malloc () garantiert die Ausgabe des Speichers entlang der Grundgrenze ausgerichtet. Oder Nullzeiger, falls nicht genügend Speicher vorhanden ist .

Kapitel 3


Lange Zeit blieb ich sinnlos im Code, setzte Haltepunkte, litt und verstand nichts, bis es irgendwann nicht mehr stocherte und ich mir die von malloc zurückgegebenen Adressen genau ansah. Zuvor sollte bei der gesamten Analyse festgestellt werden, ob die letzte Ziffer der Adresse 0x4 ist. Und jetzt begann er, die Adressen, die durch aufeinanderfolgende Anrufe bei malloc ausgegeben wurden, vollständig miteinander zu vergleichen.

Und oh, ein Wunder!

Alle erfolgreichen Anrufe gaben Adressen aus dem RAM-Speicher aus (0x20000000 und älter für diesen Stein) und nahmen von Anruf zu Anruf nacheinander zu. Und der erste erfolglose gab 0x00000036 zurück. Das heißt, die Adresse ist nicht nur nicht ausgerichtet, sondern befand sich auch überhaupt nicht im Adressraum des RAM! Der Prozessor versuchte dort etwas zu schreiben und fiel natürlich hin.

Und selbst wenn malloc () gemäß dem Standard gehandelt und 0 zurückgegeben hätte, wenn nicht genügend Speicherplatz vorhanden gewesen wäre, hätte dies überraschenderweise nichts im Sinne eines Programmabsturzes geändert (es sei denn, die Ursache des Fehlers wäre zuvor geklärt worden). Der von malloc zurückgegebene Wert wird weiterhin in keiner Weise überprüft, sondern wird sofort aktiviert. Dies ist zur Laufzeit.

Nachwort


Die Größe des Heapspeichers in der Konfigurationsdatei wurde erhöht, und alles wurde behoben.

Aber vor diesem Moment habe ich nicht einmal an die Lautstärke gedacht. Ob sich die Hölle mir ergeben hat, dachte ich. Wie auch immer, ich habe alle Variablen und Objekte entweder statisch oder auf dem Stapel. Nur durch Trägheit habe ich 0x300 Bytes darunter gelassen, da in allen Vorlagenprojekten ein gewisses Volumen unter dem Heap zugewiesen ist. Aber nein, die C ++ - Laufzeit benötigt nach den Standards der Controller immer noch dynamisch zugewiesenen Speicher und dies in beachtlichen Mengen.

Lebe und lerne.

Source: https://habr.com/ru/post/de453944/


All Articles