Stacküberläufe erkennen

Der Programmstack in einem Programm dient der Ablage von temporären Informationen wie z.Bsp. Adressen oder Variablen. Dies schließt z.Bsp. auch sämtliche internen Funktionen ein, die ihrerseits natürlich ebenfalls temporäre Daten sichern oder speichern müssen. Es handelt sich um einen Stapelspeicher der vom Ende des Arbeitsspeichers des Controllers hin zum Anfang wächst. Ein Stack ist daher vergleichbar mit einem Notizzettelspicker. Man legt eine Information ab indem man eine Notiz auf den Spicker steckt, dann eine Weitere usw.

Möchte man an die unterste Notiz entfernen, muss man erst alle darüberliegenden Notizen vom Spicker ziehen. Der Stackwert ist daher sozusagen eine virtuell gezogene Grenze „es dürfen nur xy Notizen auf den Spicker, dann ist schluss“. Bei der Angabe der Stackgröße im Luna-Programm durch

avr.stack = 32

legt man somit fest wo er im Speicher endet. Diese Information ist entscheidend, da hiermit die verschiedenen Speicherbereiche intern abgegrenzt werden. Wird nun durch einen Programmierfehler oder durch einen zu klein dimensionierten Stack das Ende des Stacks überschritten, werden die Daten dann in dem z.B. dynamisch verwalteten Arbeitsspeicherbereich oder gar bis zu den Variablen am Anfang des Arbeitsspeichers geschrieben. Dies führt dann zu schweren Programmfehlern und sporadischen Abstürzen.

Bei solch einem Ereignis spricht man von einem Stack-Überlauf (stack overflow).

Da man im Vorfeld nur abschätzen kann wie viel Bytes an Stack benötigt werden, ist es notwendig eine genaue Information darüber zu haben, ob der Stack bei seiner festgelegten Größe auch tatsächlich ausreichend dimensioniert ist. Hierfür bietet Luna eine entsprechende Überwachungsfunktion an, eine StackOverflow-Exception.

Wenn man sein Programm testen möchte, prüft man alle möglichen Programmzustände die das Programm einnehmen kann mit aktivierter StackOverflow-Exception auf dem Controller.

Die Aktivierung erfolgt durch Implementation des entsprechenden Exception-Handlers:

Exception StackOverflow
  'Programmtext zur Informationsausgabe
EndException

Tritt keine Exception auf, ist der Stack ausreichend dimensioniert und man kann sicher gehen, dass kein Stacküberlauf stattfindet.

Zum besseren Verständnis der Funktion des Stacks hier eine Analogie mithilfe eines kurzen Quelltextes und des genannten Notizspickers. Hierbei repräsentiert eine Notiz ein Byte und der Stack ist der Spicker.:

const F_CPU=20000000
avr.device = atmega328p
avr.clock  = F_CPU
 
avr.stack = 32 'Der Stack (Spicker) wird auf maximal
               '32 Bytes (Notizen) eingestellt.
 
unterprogramm(123) 'Der Programmzeiger des Prozessors ruft das unten stehende Unterprogramm
                   '"procedure unterprogramm()" auf. Bevor der Aufruf erfolgt, wird jedoch der
                   'Parameter (hier eine Zahl) "123" auf dem Spicker und anschließend die
                   'Speicheradresse des nachfolgenden Befehls "do" auf dem Spicker abgelegt.
                   'Wechseln sie nun zur Zeile "procedure unterprogramm()"
 
do 'es liegen jetzt keine weiteren Notizen mehr auf dem Spicker.
loop
 
procedure unterprogramm(a as byte)
  'es liegen nun 2 Notizen auf dem Spicker.
  'Die oberste Notiz enthält die Rücksprung-Adresse, die darunterliegende enthält die Zahl 123.
  'Die Zahl hat hier den Namen "a" erhalten und bei der Nutzung wird diese Notiz (=Speicherzelle)
  'gelesen oder geschrieben. Hierbei muss die obere Notiz nicht vom Spicker heruntergenommen werden,
  'da der Compiler Maschinencode erzeugt, der "weiß" wo sich diese Speicherzelle (Notiz) im
  'Speicher befindet. Es kann also "abgezählt" und direkt darauf geschrieben oder davon gelesen
  'werden. Alle Notizen auf dem Spicker bleiben dabei immer an ihrer Stelle.
 
  nop 'ihre Befehle im Unterprogramm
 
  'hier ist das Unterprogramm beendet,
  'Der Rückkehrbefehl "return" wird bei "endproc" automatisch verarbeitet.
  'Dieser "zieht" nun die zwei Notizen vom Spicker und springt
  'auf die Adresse die auf der obersten Notiz steht.
  'Die Notiz mit der Zahl wird dabei verworfen.
  'Wechseln sie nun zum Befehl "do" oben.
endproc

Folgendes Programm ruft einen Stacküberlauf hervor, indem eine Funktion sich selbst erneut aufruft. Bei jedem Aufruf wird die notwendige Information zum Rücksprung auf dem Stack abgelegt (Die Rücksprungadresse, also die Adresse im Programmspeicher zum Befehl der dem Aufruf folgt.).

const F_CPU=20000000
avr.device = atmega328p
avr.clock  = F_CPU
avr.stack = 32
 
uart.baud = 19200
uart.recv.enable
uart.send.enable
 
print 12;"stack overflow example"
 
overflow()
 
do
loop
 
procedure overflow()
  print "overflow(), SP = ";str(avr.StackPointer)
  waitms 50
  overflow() 'ruft sich selbst erneut auf
endproc
 
Exception StackOverflow
  print "*** StackOverFlowException ***"
EndException