Forum: Compiler & IDEs Compilerbug bei AVR-GCC ?: Stack Pointer und Interrupts


von Dietmar (Gast)


Lesenswert?

Deutet folgende Befehlsfolge auf ein Problem im Bereich
Stack Pointer und Interrupts beim AVR-GCC hin ?

Vorweg eine Beschreibung ...

Die Compilerausgabedatei xxx.lss enthält zusätzlich zum
Projekt-C-Source
den übersetzten Assemblercode. Es folgt ein kleiner Auszug daraus:

     a1c:  2d b7         in  r18, 0x3d  ; 61
     a1e:  3e b7         in  r19, 0x3e  ; 62
     a20:  24 5f         subi  r18, 0xF4  ; 244
     a22:  3f 4f         sbci  r19, 0xFF  ; 255
     a24:  0f b6         in  r0, 0x3f  ; 63
     a26:  f8 94         cli
     a28:  3e bf         out  0x3e, r19  ; 62
     a2a:  0f be         out  0x3f, r0  ; 63
     a2c:  2d bf         out  0x3d, r18  ; 61

Ich verstehe das so:

a1c: // Stack Pointer (Low Byte) lesen und im r18 speichern
        !!! Hier darf ein Interruptaufruf dazwischen kommen !!!
a1e: // Stack Pointer (HighByte) lesen und im r19 speichern
a20: // vom gelesenen Stack Pointer in r18 ein LowByte subtrahieren
a22: // vom gelesenen Stack Pointer in r19 ein HighByte subtrahieren
a24: // Status bzw. Flagregister SREG lesen und in r0 speichern
(retten)
a26: // alle Interrupts sperren (SREG Bit 7)
a28: // r19 ist das HighByte des veränderten Stack Pointers und
        wird hier in den Stack Pointer zurückgeschrieben.
a2a: // r0 auf SREG zurückschreiben, um u.a. alten Interruptzustand
        wieder herzustellen. Der Interrupt kann zugelassen sein.
        !!! Hier darf aber niemals ein Interruptaufruf dazwischen
kommen,
        weil der Stack Pointer auf eine ungültige Speicheradresse
        zeigt !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
a2c: // r18 ist das LowByte des veränderten Stack Pointers und
        wird hier eindeutig zu spät in den Stack Pointer
        zurückgeschrieben.

Wie findet man solche Stellen ? Ganz einfach:
1. Datei xxx.lss mit Editor öffnen.
2. Mit der Suchfunktion (STRG F) nach dem Stichwort cli suchen und
   weitersuchen.

Hinweis: Vergleichbare Befehlsfolgen kommen in der Compilerausgabedatei

         xxx.lss auch vor, wobei r18 und r19 durch andere Register
         ersetzt sind. Wenn ich das richtig erkannt habe, taucht die
         Befehlsfolge bei Berechnungen mit Fliesskomma-Variablen und
         beim <vfprintf> auf.
         Die Befehlsfolge von a24: bis a2c: ist typischerweise
vorhanden.
         Die Befehlsfolge von a1c: bis a22: (Stack Pointer
Manipulation)
         kann auch eine andere Berechnung als eine Subtraktion
         beinhalten.

Folgendes Problem sehe ich da:
Wenn zwischen Adresse a2a: und a2c: das laufende Programm durch einen
Interruptaufruf unterbrochen wird, schreibt der AVR seine
Rücksprungadresse auf den Stack und zwar dahin, wo der
16-bit-Stack Pointer hinzeigt. Da im genannten Beispiel der Stack
Pointer
undefiniert in den Speicher zeigt, - er kann wegen des Carry-Übertrags

der Berechnung um fast eine Page (256Byte) daneben liegen - , werden
also eventuell gültige Daten von der Rücksprungadresse überschrieben.
Der AVR findet zwar nach Abarbeitung der Interruptroutine an die
richtige
Adresse a2c: zurück, aber die überschriebenen gütigen Daten sind im
Speicher verändert und unbrauchbar, weil Variablen, andere Pointer,
Rücksprungadressen von Call-Aufrufen (egal - was auch immer) sind
verändert. Somit treten in anderen Programmteilen, die diese kaputten
Daten benutzen, unvorhersehbare beliebig geartete Folgefehler auf!
Wenn meine Theorie stimmt, ist der Absturz im wahrsten Sinne des Wortes

vorprogrammiert.

Ich weiss leider nicht, wie man dem Compiler das Problem abgewöhnen
kann.
Deshalb richtet sich dieser Hinweis an die Compiler-Experten und
Autoren.

Es sollte im Assemblercode besser folgende Reihenfolge eingehalten
werden:
1. 16-bit-Stack Pointer komplett lesen
2. 16-bit-Stack Pointer komplett manipulieren, wobei
   das Ergebnis noch nicht in den Stack Pointer zurückgeschrieben
wird.
3. SREG-Zustand lesen (retten)
4. Interrupt sperren
5. 16-bit-Stack Pointer komplett jetzt zurückschreiben
6. geretteten SREG-Zustand ins SREG zurückschreiben, d.h. erst jetzt
   erfolgt gegebenfalls eine Interruptfreigabe.

Mit dieser neuen Reihenfolge ist sichergestellt, dass kein Interrupt
mitten in das Setzen einer neuen Stack Pointer Adresse springen kann.
Der Interrupt wäre zwar über einige Prozessortaktzyklen länger
gesperrt,
jedoch hat die Sicherheit der Stack Pointer Definition oberste
Priorität.
Querverweis: Im AVR-Datenblatt wird der Stackpointer grundsätzlich
bei gesperrtem Interrupt gesetzt.
----------------------------------------------------------------------
Gedanklich hier noch vergleichbares anderes Beispiel, was aber kein
Compilerfehler, sondern einen denkbaren Programmierfehler in der
Anwendung aufzeigen soll, wenn ein Programmierer bei der Verwendung von

Interrupts nicht aufpaßt. Ich möchte ergänzend klarstellen, dass
meine Anwendung diesen Programmierfehler in C-Source nicht enthält.
Wer will, kann das folgende Beispiel als ergänzende Sichtweise zum
vorhandenen Thema im AVR-GCC-Tutorial sehen und verwenden.

Hier nun das Beispiel1 (richtig programmiert):
Wenn der Wert einer Mehrbyte-Variable, -Pointer, z.B. float,long oder
int
vom Hauptprogramm berechnet wird und vom Interruptprogramm gelesen
wird, muss der Interrupt im Hautprogramm während der Variablenzuweisung

gesperrt sein, damit der Interrupt die Zuweisung nicht unterbricht.
Die Zuweisung einer Mehrbyte-Variable besteht ja aus mehreren
Assemblerbefehlen, die einzeln Byte für Byte überschreiben. Das ist auf

einem 8bit-µC völlig normal.

Hier nun das Beispiel2 (falsch programmiert):
Wenn der Interrupt die Zuweisung unterbrechen darf, wäre die Folge,
dass ein Byte der Mehrbyte-Variable den alten Wert hat,
aber das andere Byte bereits den neuen Wert hat. Numerisch ergibt das
völlig einen unsinnigen Variablenwert. Wenn es z.B. den Exponenten
einer float-Variable trifft, ist das mindestens eine numerische
Katastrophe. Wenn damit das Interruptprogramm dann weiterechnet,
passiert in der Anwendung ein unberechenbarer Blödsinn.
Das gemeine ist, dass solche Laufzeitfehler selten auftreten, weil der

Interrupt ja zeitlich die Befehlsfolge der Mehrbyte-Zuweisung exakt
treffen muss. Aber man kann sich zu 100% darauf verlassen, dass der
Interrupt irgendwann garantiert ins Schwarze trifft, weil Interrupts
asynchron zur Softwarelaufzeit auftreten.

Dieses theoretische Beispiel ist gedanklich problemlos auf den
Stack Pointer übertragbar.

----------------------------------------------------------------------
Allgemeine Info zu meiner Anwendung:
Auf die Stack Pointer Problematik wurde ich aufmerksam, weil
gelegentlich
- etwa alle 10..15min - ein numerischer Softwareabsturz bei meiner
Anwendung auftaucht, den ich mir momentan nicht anders erklären kann.
Als Interruptquelle verwende ich einen netzsynchronen externen
100Hz-Interrupt. Zum Softwaredebugging von Variablen verwende ich den
printf des Compilers. Es ist zwar mein erstes Projekt auf dem ATMEL
AVR.
Jedoch habe ich seit 1979 auf den verschiedensten Prozessoren in
Assembler und anderen Sprachen programmiert.
----------------------------------------------------------------------

Gruss
Dietmar

von Jörg Wunsch (Gast)


Lesenswert?

Du brauchst dazu nicht erst umständliche Disassembler-Listings
bemühen, der generierte Compilercode steht dir offen.

Außerdem hättest du die Frage ziemlich kurz stellen können.  (Sorry,
ich habe das Lesen des länglichen Artikels abgebrochen, nachdem ich
glaube erkannt zu haben, was dir nicht gefällt -- falls du danach noch
mehr Fragen gestellt hast, musst du dich nochmal melden.)

Nein, das ist kein Problem.  Auch wenn es nirgends dokumentiert ist,
hat Atmel dem Compilerschreiber mal irgendwann bestätigt, dass die
Interruptfreigabe in der Tat und garantiert erst einen Befehl
verspätet erfolgt, d.h. der Befehl direkt nach einer Zuweisung an das
SREG, bei dem das I-Bit wieder gesetzt wird, wird selbst bei einem
anhängigen Interrupt noch ausgeführt.

Ich hoffe das beantwortet deine Frage (außer natürlich der nach deinen
sporadischen Softwareabstürzen, aber die musst du wohl oder übel
selbst finden ;-).

von Erwin Stegmaier (Gast)


Lesenswert?

Hallo Dietmar,

ich habe das gleiche Problem wie du, dass meine printfs immer wieder
mal gestört werden, und auch manchmal der AVR eine reboot macht.

Nun meine Frage: Hast du dein Problem gelöst? War ein SW-Bug von dir,
oder hast du den Fehler immer noch?

Ich bin auf das gleiche Problem gestossen, und suche noch nach der
Lösung. Ein workaround war gestern Abend noch folgendes: Ich habe alle
Variable als staisch deklariert, bzw. ausserhalb den C-Funktionen. Dann
aht der Compiler keinen Stack manipulieren müssen.
Bisher konnte ich keinen Reboot mehr provozieren.

Nun bin ich mir nicht sicher, ob mein Programm ein problem hat, oder
doch vielelicht manchmal nach dem cli und dem SPL-setzten sich doch
nicht ein int dazwischen schummelt.

Vermutlich muss ich da mal einen Fang-Code machen, um das genau zu
untersuchen.

Gruß Erwin

von D. H. (slyd)


Lesenswert?

Hallo allerseits,

Ich habe das lange Posting jetzt nicht ganz gelesen, jedoch kann ich
bestätigen wie Erwin es sagt, dass der AVRGCC bei Verwendung von
Strings im allgemeinen (egal ob nun bei printf oder was eigenem)
manchmal "fehlerhaften" Code produziert der zum "restart" des AVRs
führt.
Wahrscheinlich ein Sprung zu einer falschen adresse o.ä.


Also die Situation: Zunächst funktioniert das Programm wunderbar - kann
durchaus ein größeres Programm sein was bei -os etwa 4kB belegt.
Dann fügt man an einer - absolut unkritischen Stelle im Programm, z.B.
in der Main Funktion - einen Aufruf wie writeUART_P("ASDF"); hinzu.
Die Bedeutung dieser Funktion dürfte klar sein. Die Funktion selbst
wird an vielen Stellen im Programm verwendet und funktioniert
normalerweise problemlos.


Nachdem man diesen einen zusätzlichen Aufruf hinzugefügt hat,
funktioniert es aber auf einmal nicht mehr! Das programm stürzt dann
schon kurz nach dem Start ab und der AVR springt zum Anfang des
Programms zurück!


Das Lustige daran ist: Fügt man z.B. direkt nach oder vor diesem neuen
Aufruf nur ein einzelnes NOP hinzu - klappts oftmals wieder!
Das geht aber nicht immer - machmal muss man mehrere Nops auch ganz wo
anders hinzufügen...

So und nun kommts:
Schaltet man die Optimierung ab - also o0 - funktioniert es immer!
Aber egal ob o1, o2, oder os -  dann geht es ab und zu nicht!


An meinem Code dürfte es also nicht liegen, denn der funktioniert in
99% der Fälle einwandfrei und bei abgeschalteter Optimierung sowieso.
(Nur passt dann das Timing an einigen stellen nicht mehr so gut und der
Code wird ziemlich aufgebläht)


Habe leider gerade keinen Code an dem ich das demonstrieren kann (darf
ich noch nicht veröffentlichen - kommt aber bald)
Vllt. mach ich demnächst mal ein kleines Beispiel dazu fertig.


Woran kann das liegen?
Nervt nämlich extrem!

Ich arbeite normalerweise mit PICs und dem C18 - da habe ich sowas
seltsames noch nie gehabt...


MfG,
Dominik

von A.K. (Gast)


Lesenswert?

"mach ich demnächst mal ein kleines Beispiel dazu fertig"

Das wäre hilfreicher als allgemein gehaltene Anschuldigungen an die
Adresse des Compilers. Die erweisen sich nämlich fast immer als
Rohrkrepierer.

Das von Joerg skizzierte CLI Verhalten ist ganz offiziell in der
Instruction Set Reference dokumentiert. Und die ist deutlich älter als
Dietmars Klageschrift. Also muss er sich hier mangelnde Recherche
vorwerfen lassen, denn das ist ja wohl die erste Adresse in der man
suchen sollte.

von Erwin Stegmaier (Gast)


Lesenswert?

Hallo zusammen,

ich ahbe mal eine etwas eigenwillige Untersuchung gemacht, um die
Aussage, dass es sich nicht ume inen Compilerfehler handeln kann, zu
unterstützen.

Hab einfach den sprintf wieder reingebaut, geschaut, ob Falschausgaben
kommen, und die kamen dann auch, zwar nicht reproduzierbar, aber sie
kamen, und dann habe ich im List-File alle cli gesucht, die
Befehlssequenzen im Hexfile umgepatched und noch einmal getestet.
Diesesmal sollte sich ja an der Funktionalität überhaupt nichts ändern,
denn selbst die Checksummen im Hexfile (ich hatte Glück, alle
cli-Befehle waren in der Mitte der Hexzeilen, so musste ich die
Checksummen nicht anpassen) waren ja gleich.
Nur die Ausführung von SPL = R31 und Status zurückschreiben waren in
der Reihenfolge vertauscht, also so, wie sie laut Lehrbuch auch sein
sollten, zuerst beide Register SPH und SPL umsetzen und dann den Int
wieder freigeben. Dann habe ich nochmals geschaut, und der Fehler war
noch da! Es liegt also an etwas anderem! Die Befehlssequenz mit dem cli
und dem Zurückschreiben ist es also definitiv nicht! So ein Bug wäre
sicherlich schon früher aufgefallen!

Jetzt muss ich eine Routine schreiben, die ein Stackmonitoring macht.

von Peter D. (peda)


Lesenswert?

"Das von Joerg skizzierte CLI Verhalten ist ganz offiziell in der
Instruction Set Reference dokumentiert."

War mir doch gleich so, daß ich das irgendwo gelesen hab.

Es gibt z.B. auch die Folge:
1
cli();
2
// ... Freigabe des Aufwachinterrupts
3
sei();
4
sleep();

Damit ist garantiert, daß kein Interrupt vor dem Sleep zuschlagen kann
und man nie wieder aufwacht.


Peter

von Peter D. (peda)


Lesenswert?

@Dominik

"An meinem Code dürfte es also nicht liegen, denn der funktioniert in
99% der Fälle einwandfrei und bei abgeschalteter Optimierung
sowieso."

In der Regel verbrennt man sich mit solchen Aussagen mächtig die
Zunge.

Solche Fehler sind typisch für ungültige Pointer (Schreiben über die
Feldgrenze hinaus, schreiben als falscher Typ, fehlende
Initialisierung).
Damit werden Returnadressen zerstört und manchmal kann der Sprung
trotzdem wieder im richtigen Kontext landen.

Typische Fehlerkandidaten sind scanf, itoa, usw.


Peter

von Peter H. (pehe)


Lesenswert?

Hallo,

der Thread ist zwar schon älter, aber ich möchte trotzdem einen Hinweis 
loswerden.

Das beschriebene Verhalten ist bei mir ebenfalls aufgetreten, und zwar 
im <__epilogue_restores__> von sprintf(), hier der Codeausschnitt mit 
Kommentaren:

    in   r0, 0x3f    ; SREG sichern
    cli              ; alle Interrupts sperren
    out  0x3e, r29   ; SP_H setzen
    out  0x3f, r0    ; SREG restore (auch Interrupts)
    out  0x3d, r28   ; SP_L setzen

Sofort nach dem SREG Restore springt der Programmzeiger nach

    18: f5 c1  rjmp .+1002  ; <__vector_12>

d.h. der anstehende Interrupt wird sofort ausgeführt, und nicht erst 
einen Befehl später nach seiner Freigabe.

Aber: Diese Beobachtung habe ich im Simulator von VMLAB gemacht.
Wenn die von Jörg und Peter zitierte Aussage von Atmel richtig ist, was 
wir wohl voraussetzen können, dann hat der VMLAB Simulator hier einen 
Bug.
Möglicherweise auch andere Simulatoren ?

Gruß,
Peter H.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Ja, da hat offenbar VMLAB einen Bug.  Wird aber leider nicht mehr
gepflegt, da das Projekt für Enrique nicht so zum Fliegen gekommen
ist, wie er sich das mal vorgestellt hatte.

von Oliver (Gast)


Lesenswert?

>Aber: Diese Beobachtung habe ich im Simulator von VMLAB gemacht.

Richtig. VMLAB simuliert da falsch. Leider, dann das schränkt dessen 
Nutzbarkeit doch ganz erheblich ein.

Andere Simulaturen sind andere Simulatoren - da besteht kein 
Zusammenhang. AVRStudio z.B. macht es richtig.

Oliver

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Es könnte natürlich mal jemand Enrique fragen, ob er nicht vielleicht
wenigstens diesen Bug fixen möchte.  Sonst ist der Simulator ja
wirklich nicht schlecht, und das Freischalten der Interrupts um einen
Befehl zu verzögern kann eigentlich kein großer Aufwand sein.  Beim
CLI musste er es ja ohnehin schon implementieren, denn dort ist es
auch so dokumentiert.

von Oliver (Gast)


Lesenswert?

Ich hatte den Fehler vor einiger Zeit mal im VMLAB-Forum angesprochen 
(als es das Forum noch gab), da gabs aber keine Reaktion.

Noch schöner wäre natürlich, den source-code als OpenSource-Projekt zu 
veröffentlichen. Da gibt es nämlich schon noch ein paar Macken mehr, die 
man sich auch mal ansehen könnte.

Oliver

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Oliver wrote:

> Noch schöner wäre natürlich, den source-code als OpenSource-Projekt zu
> veröffentlichen. Da gibt es nämlich schon noch ein paar Macken mehr, die
> man sich auch mal ansehen könnte.

Wenn du dich mit Delphi rumschlagen willst...  Kannst ja Enrique mal
fragen.  Vielleicht gibt er dir den Code ja auch zur Pflege weiter,
ohne ihn gleich komplett opensource zu machen, wenn du dich im Gegenzug
verpflichtest, Bugfix-Releases zu machen?

Ich habe Enrique vor zwei Jahren treffen können, als ich dienstlich
in Madrid unterwegs war.  Ist durchaus ein netter Mensch.

von Oliver (Gast)


Lesenswert?

>Wenn du dich mit Delphi rumschlagen willst...

Nöööö.....

Wobei, den Simulationsfehler ausbügeln würde wohl noch gehen, wenn ich 
denn Delphi überhaupt hätte, aber Windows-Oberflächen mit Delphi, nein 
danke. Da müssten dann schon "Profis" ran.

Oliver

von Dirk (Gast)


Lesenswert?

Delphi gibt es hier als freie Version :)

http://www.turboexplorer.com/downloads

Damit hättest du es zumindest.

von Peter H. (pehe)


Lesenswert?

Hallo,

das wäre schon eine tolle Sache, wenn sich jemand findet, der VMLAB 
weiterentwickelt und die Bugs beseitigt. Habe selber leider nicht 
ausreichend Zeit dafür. Die Simulationsumgebung von VMLAB ist super, 
insbesondere daß man eigene Komponenten hinzufügen kann. Ich habe meine 
Temperaturmodule und Schieberegister auf diese Weise an den ATmega 
rangehängt, und es funktioniert!

Für das diskutierte Problem mit der Interruptfreigabe im VMLAB Simulator 
habe ich mir ein kleines Workaround gebastelt, und einen neuen Thread 
dafür aufgemacht, damit es nicht mehr als Compilerbug auftaucht:
Beitrag "VMLAB Interrupt zu früh, Stackpointer falsch"

Viele Grüße,
Peter H.

von m_bedded (Gast)


Lesenswert?

ist es nicht die FAQ 9.9.28 aus dem avr-libc-user-manual, was die 
Ursprungsfrage beantwortet:
9.9.28 Why are interrupts re-enabled in the middle of writing the stack 
pointer?

Es interressiert mich wirklich, wie Dominik und andere angebliche 
printf-Probleme gelöst haben. Mein sprintf_P() macht mich momentan 
verrückt: Beitrag "gcc-avr: sprintf_P kracht (mit 3 oder mehr Parameter)"

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
Noch kein Account? Hier anmelden.