Wie schützt man sich vor Stapelüberlauf (bei Cortex M)?

Wenn Sie auf einem "großen" Computer programmieren, haben Sie wahrscheinlich keine solche Frage. Es gibt eine Menge Stapel, um es zu überlaufen, müssen Sie versuchen. Im schlimmsten Fall klicken Sie in einem Fenster wie diesem auf OK und finden es heraus.

Bild

Wenn Sie jedoch Mikrocontroller programmieren, sieht das Problem etwas anders aus. Zuerst müssen Sie feststellen, dass der Stapel voll ist.

In diesem Artikel werde ich über meine eigene Forschung zu diesem Thema sprechen. Da ich hauptsächlich unter STM32 und unter Milander 1986 programmiere, habe ich mich auf sie konzentriert.

Einführung


Stellen wir uns den einfachsten Fall vor: Wir schreiben einfachen Single-Threaded-Code ohne Betriebssysteme, d. H. Wir haben nur einen Stapel. Und wenn Sie wie ich in uVision Keil programmieren, wird der Speicher irgendwie so verteilt:



Und wenn Sie, wie ich, das dynamische Gedächtnis auf Mikrocontrollern als böse betrachten, dann so:



Übrigens
Wenn Sie die Verwendung von Heap verbieten möchten, können Sie dies folgendermaßen tun:
#pragma import(__use_no_heap_region) 

Details hier

OK, was ist das Problem? Das Problem ist, dass Keil den Stapel unmittelbar hinter dem statischen Datenbereich platziert. Und der Stapel in Cortex-M wächst in Richtung abnehmender Adressen. Und wenn es überläuft, kriecht es einfach aus dem zugewiesenen Speicher heraus. Und überschreibt alle statischen oder globalen Variablen.

Besonders gut, wenn der Stack nur beim Eintritt in den Interrupt überläuft. Oder noch besser in einem verschachtelten Interrupt! Und verdirbt leise eine Variable, die in einem völlig anderen Codeabschnitt verwendet wird. Und das Programm stürzt beim Assert ab. Wenn Sie Glück haben. Sphärischer Heisenbag, man kann so eine ganze Woche mit einer Taschenlampe suchen.

Machen Sie sofort eine Reservierung, dass, wenn Sie einen Heap verwenden, das Problem nirgendwohin führt, nur anstelle globaler Variablen, die der Heap verdirbt. Nicht viel besser.

Okay, das Problem ist klar. Was zu tun ist?

MPU


Am einfachsten und naheliegendsten ist die Verwendung der MPU (mit anderen Worten Memory Protection Unit). Ermöglicht das Zuweisen unterschiedlicher Attribute zu verschiedenen Speicherelementen. Insbesondere können Sie den Stapel mit schreibgeschützten Bereichen umgeben und MemFault abfangen, wenn Sie dort schreiben.

Zum Beispiel ist in stm32f407 MPU. Leider ist es in vielen anderen "Junior" -Stm nicht der Fall. Und im Milandrovsky 1986VE1 ist es auch nicht.

Das heißt, Die Lösung ist gut, aber nicht immer erschwinglich.

Manuelle Steuerung


Beim Kompilieren kann Keil einen HTML-Bericht mit einem Aufrufdiagramm generieren (und dies standardmäßig tun) (Linker-Option "--info = stack"). Dieser Bericht enthält auch Informationen zum verwendeten Stapel. Gcc kann das auch (Option -fstack-usage). Dementsprechend können Sie sich diesen Bericht manchmal ansehen (oder ein Skript schreiben, das dies für Sie erledigt, und es vor jedem Build aufrufen).

Darüber hinaus wird zu Beginn des Berichts ein Pfad geschrieben, der zur maximalen Nutzung des Stapels führt:



Das Problem ist, dass wenn Ihr Code Funktionsaufrufe durch Zeiger oder virtuelle Methoden hat (und ich habe sie), dieser Bericht die maximale Stapeltiefe stark unterschätzen kann. Unterbrechungen werden natürlich nicht berücksichtigt. Kein sehr zuverlässiger Weg.

Schwierige Stapelplatzierung


Diese Methode habe ich in diesem Artikel kennengelernt . Der Artikel handelt von Rost, aber die Hauptidee ist folgende:



Bei Verwendung von gcc kann dies über den " Double Link " erfolgen.

Und in Keil kann die Position der Bereiche mithilfe Ihres eigenen Skripts für den Linker geändert werden (Streudatei in Keils Terminologie). Öffnen Sie dazu die Projektoptionen und deaktivieren Sie "Speicherlayout aus Zieldialog verwenden". Dann wird die Standarddatei im Feld "Streudatei" angezeigt. Es sieht ungefähr so ​​aus:

 ; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00020000 { ; load region size_region ER_IROM1 0x08000000 0x00020000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; RW data .ANY (+RW +ZI) } } 

Was ist als nächstes zu tun? Mögliche Optionen. In der offiziellen Dokumentation wird vorgeschlagen , Abschnitte mit reservierten Namen zu definieren - ARM_LIB_HEAP und ARM_LIB_STACK. Dies hat jedoch zumindest für mich unangenehme Konsequenzen: Die Größe des Stapels und des Heaps muss in der Scatter-Datei festgelegt werden.

In allen von mir verwendeten Projekten werden die Stapel- und Heap-Größen in der Assembler-Startdatei festgelegt (die Keil beim Erstellen des Projekts generiert). Ich möchte es nicht wirklich ändern. Ich möchte nur eine neue Scatter-Datei in das Projekt aufnehmen, und alles wird gut. Also bin ich einen etwas anderen Weg gegangen:

Spoiler
 #! armcc -E ; with that we can use C preprocessor #define RAM_BEGIN 0x20000000 #define RAM_SIZE_BYTES (4*1024) #define FLASH_BEGIN 0x8000000 #define FLASH_SIZE_BYTES (32*1024) ; This scatter file places stack before .bss region, so on stack overflow ; we get HardFault exception immediately LR_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load region size_region ER_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } ; Stack region growing down REGION_STACK RAM_BEGIN { *(STACK) } ; We have to define heap region, even if we don't actually use heap REGION_HEAP ImageLimit(REGION_STACK) { *(HEAP) } ; this will place .bss region above the stack and heap and allocate RAM that is left for it RW_IRAM1 ImageLimit(REGION_HEAP) (RAM_SIZE_BYTES - ImageLength(REGION_STACK) - ImageLength(REGION_HEAP)) { *(+RW +ZI) } } 


Dann sagte ich, dass sich alle Objekte mit dem Namen STACK in der Region REGION_STACK befinden sollten und dass sich alle HEAP-Objekte in der Region REGION_HEAP befinden sollten. Und alles andere ist in der Region RW_IRAM1. Und er ordnete die Regionen in dieser Reihenfolge an - den Beginn der Operation, den Stapel, den Haufen, alles andere. Die Berechnung besteht darin, dass in der Assembler-Startdatei der Stapel und der Heap unter Verwendung dieses Codes festgelegt werden (d. H. Als Arrays mit den Namen STACK und HEAP):

Spoiler
 Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit PRESERVE8 THUMB 


Okay, könnten Sie fragen, aber was gibt uns das? Und hier ist was. Beim Verlassen des Stapels versucht der Prozessor nun, nicht vorhandenen Speicher zu schreiben (oder zu lesen). Und auf STM32 tritt eine Unterbrechung aufgrund einer Ausnahme auf - HardFault.

Dies ist aufgrund der MPU nicht so praktisch wie MemFault, da HardFault aus vielen Gründen auftreten kann, der Fehler jedoch zumindest laut und nicht leise ist. Das heißt, es tritt sofort auf und nicht nach einer unbekannten Zeitspanne wie zuvor.

Das Beste ist, wir haben nichts dafür bezahlt, keine Overhead-Laufzeit! Wow. Es gibt jedoch ein Problem.

Dies funktioniert bei Milander nicht.

Ja Bei der Milandra (ich interessiere mich hauptsächlich für 1986BE1 und BE91) sieht die Speicherkarte natürlich anders aus. In STM32 gibt es vor dem Start des Operateurs nichts, und auf der Milandra liegt vor dem Operateur der Bereich des externen Busses.

Aber selbst wenn Sie keinen externen Bus verwenden, erhalten Sie keinen HardFault. Oder vielleicht bekommen. Oder vielleicht bekommen, aber nicht sofort. Ich konnte keine Informationen zu diesem Thema finden (was für Milander nicht überraschend ist), und die Experimente ergaben keine verständlichen Ergebnisse. HardFault trat manchmal auf, wenn die Stapelgröße ein Vielfaches von 256 war. Manchmal trat HardFault auf, wenn der Stapel zu weit in den nicht vorhandenen Speicher ging.

Aber es spielt keine Rolle. Wenn HardFault nicht jedes Mal auftritt, werden wir durch einfaches Verschieben des Stapels an den Anfang des RAM nicht mehr gerettet. Und um ganz ehrlich zu sein, ist STM auch nicht verpflichtet, gleichzeitig eine Ausnahme auszulösen. Die Cortex-M-Kernspezifikation scheint nichts Konkretes darüber zu sagen.

Selbst bei STM ist es eher ein Hack, nur nicht sehr schmutzig.

Sie müssen also nach einem anderen Weg suchen.

Zugriff auf den Haltepunkt in der Aufzeichnung


Wenn wir den Stapel an den Anfang des RAM verschieben, ist der Grenzwert des Stapels immer der gleiche - 0x20000000. Und wir können einfach einen Haltepunkt auf den Datensatz in dieser Zelle setzen. Dies kann mit dem Befehl erfolgen und sogar mithilfe der INI-Datei im Autorun registriert werden:

 // breakpoint on stackoverflow BS Write 0x20000000, 1 

Dies ist jedoch kein sehr zuverlässiger Weg. Dieser Haltepunkt wird jedes Mal ausgelöst, wenn der Stapel initialisiert wird. Es ist leicht, es versehentlich zu schlagen, indem Sie auf "Alle Haltepunkte töten" klicken. Und er wird Sie nur in Gegenwart eines Debuggers schützen. Nicht gut.

Dynamischer Überlaufschutz


Eine schnelle Suche zu diesem Thema führte mich zu Keils Optionen --protect_stack und --protect_stack_all. Nützliche Optionen schützen leider nicht vor dem Überlaufen des gesamten Stapels, sondern vor dem Einfügen einer weiteren Funktion in den Stapelrahmen. Zum Beispiel, wenn Ihr Code die Grenzen eines Arrays überschreitet oder mit einer variablen Anzahl von Parametern fehlschlägt. Gcc kann das natürlich auch (-fstack-protector).

Das Wesentliche dieser Option ist folgender: "Schutzvariable" wird jedem Stapelrahmen hinzugefügt, dh einer Schutznummer. Wenn sich diese Nummer nach dem Beenden der Funktion geändert hat, wird die Fehlerbehandlungsfunktion aufgerufen. Details hier .

Eine nützliche Sache, aber nicht ganz das, was ich brauche. Ich brauche eine viel einfachere Prüfung - damit bei der Eingabe jeder Funktion der Wert des SP-Registers (Stack Pointer) gegen einen zuvor bekannten Mindestwert geprüft wird. Aber schreiben Sie diesen Test nicht mit Ihren Händen am Eingang zu jeder Funktion?

Dynamische SP-Steuerung


Glücklicherweise hat gcc die wunderbare Option "-finstrument-functions", mit der Sie eine benutzerdefinierte Funktion aufrufen können, wenn Sie jede Funktion eingeben und wenn Sie jede Funktion verlassen. Dies wird normalerweise zur Ausgabe von Debugging-Informationen verwendet, aber was ist der Unterschied?

Noch glücklicher ist es, dass Keil die gcc-Funktionalität absichtlich kopiert und dort dieselbe Option unter dem Namen "--gnu_instrument" ( Details ) verfügbar ist.

Danach müssen Sie nur noch diesen Code schreiben:

Spoiler
 //   ,    //   ,         scatter- extern unsigned int Image$$REGION_STACK$$RW$$Base; //    ,   static const uint32_t stack_lower_address = (uint32_t) &( Image$$REGION_STACK$$RW$$Base ); //         extern "C" __attribute__((no_instrument_function)) void __cyg_profile_func_enter( void * current_func, void * callsite ) { (void)current_func; (void)callsite; ASSERT( __current_sp() >= stack_lower_address ); } //   -   extern "C" __attribute__((no_instrument_function)) void __cyg_profile_func_exit( void * current_func, void * callsite ) { (void)current_func; (void)callsite; } 


Und voila! Nach Eingabe jeder Funktion (einschließlich Interrupt-Handler) wird nun eine Überprüfung auf Stapelüberlauf durchgeführt. Und wenn der Stapel überläuft, gibt es eine Bestätigung.

Eine kleine Erklärung:
  • Ja, natürlich müssen Sie mit einem gewissen Spielraum auf Überlauf prüfen, da sonst die Gefahr besteht, dass Sie über den Stapel "springen".
  • Image $$ REGION_STACK $$ RW $$ Base ist eine besondere Magie, um mithilfe der vom Linker generierten Konstanten Informationen über Speicherbereiche abzurufen. Details (obwohl stellenweise nicht sehr verständlich) hier .


Ist die Lösung perfekt? Natürlich nicht.

Erstens ist diese Prüfung alles andere als kostenlos, der Code schwillt um 10 Prozent an. Nun, der Code funktioniert langsamer (obwohl ich ihn nicht gemessen habe). Ob es kritisch ist oder nicht, liegt bei Ihnen; Meiner Meinung nach ist dies ein angemessener Preis für die Sicherheit.

Zweitens funktioniert dies höchstwahrscheinlich nicht, wenn vorkompilierte Bibliotheken verwendet werden (aber da ich sie überhaupt nicht verwende, habe ich sie nicht überprüft).

Diese Lösung eignet sich jedoch möglicherweise für Multithread-Programme, da wir die Überprüfung selbst durchführen. Aber ich habe diese Idee nicht wirklich durchdacht, deshalb werde ich sie vorerst behalten.

Zusammenfassend


Es stellte sich heraus, dass es funktionierende Lösungen für stm32 und Milander gab, obwohl ich für letzteres mit etwas Aufwand bezahlen musste.

Für mich war das Wichtigste ein kleiner Paradigmenwechsel des Denkens. Vor dem oben genannten Artikel habe ich überhaupt nicht gedacht, dass Sie sich irgendwie vor Stapelüberlauf schützen könnten. Ich habe dies nicht als ein Problem wahrgenommen, das gelöst werden muss, sondern als ein bestimmtes natürliches Phänomen - manchmal regnet es und manchmal läuft der Stapel über. Nun, es gibt nichts zu tun, man muss die Kugel beißen und tolerieren.

Und ich merke im Allgemeinen ziemlich oft für mich (und für andere Leute), dass ich - anstatt 5 Minuten in Google zu verbringen und eine triviale Lösung zu finden - seit Jahren mit meinen Problemen lebe.

Das ist alles für mich. Ich verstehe, dass ich nichts grundlegend Neues entdeckt habe, aber ich habe keine fertigen Artikel mit einer solchen Entscheidung gefunden (zumindest Joseph Yu selbst bietet dies nicht direkt in einem Artikel zu diesem Thema an). Ich hoffe, dass sie mir in den Kommentaren sagen werden, ob ich Recht habe oder nicht und was die Fallstricke dieses Ansatzes sind.

UPD: Wenn Keil beim Hinzufügen einer Scatter-Datei eine unverständliche Warnung ausgibt, z. B. "AppData \ Local \ Temp \ p17af8-2 (33): Warnung: # 1-D: Die letzte Zeile der Datei endet ohne Zeilenumbruch" - diese Datei selbst jedoch nicht wird geöffnet, da es nur vorübergehend ist. Fügen Sie dann einfach den Zeilenumbruch mit dem letzten Zeichen in der Streudatei hinzu.

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


All Articles