Programmierung

Laufzeitfehler finden

In der Artikelserie zur Fehlerbehandlung habe ich bereits demonstriert, wie Sie erwartete Fehler behandeln können. Aber wie soll man einen Fehler behandeln, der noch gar nicht als solcher identifiziert ist? CONZEPT 16 bringt einige Mechanismen mit, um die Ursache von unerwarteten Laufzeitfehlern zu finden.

Leider bleibt auch die solideste Software von Fehlern nicht verschont. Den einen nicht berücksichtigen Fall der letztendlich zu einem Fehler führt, kennt mit Sicherheit jeder Programmierer. Selbst wochenlange Testphasen und die ausgeklügeltste Fehlerbehandlung sind kein Garant für die Fehlerfreiheit einer Software.
Um einen Fehler beheben zu können, bedarf es natürlich erst einmal der Feststellung eines solchen und der Kenntnis über dessen Ursache. Die einzigen Fehler die automatisch festgestellt werden können sind Laufzeitfehler, also Fehler wie „Deskriptor ungültig“ oder „Division durch Null“. Ein guter Anhaltspunkt um der Quelle des Übels auf die Spur zu kommen, ist die Position im Quelltext bei der der Fehler auftrat.
Diese Information wird, falls keine besonderen Mechanismen zur Unterdrückung von Laufzeitfehlern genutzt werden, bei Auftreten eines Laufzeitfehlers in einem grafischen Client bereits angezeigt. Es erscheint eine Fehlermeldung. In der Regel wird aber der Anwender derjenige sein, der diese Meldung zu Gesicht bekommt und nicht der Entwickler. Damit der Entwickler diesen Fehler beheben kann, muss aber auch er darüber informiert sein.

Fehlerfunktion

Für diesen Zweck bietet CONZEPT 16 die Funktion ErrCall(), mit der eine Funktion zur Reaktion auf Laufzeitfehler gesetzt werden kann. Innerhalb dieser Fehlerfunktion kann der Fehler beispielweise protokolliert und per E-Mail an den Entwickler oder Support gesendet werden.

Beispiel

sub Func
{
  // Löst Laufzeitfehler „Division durch Null“ aus
  if (1 / 0 = 0) { … }
}

main
{
  // Fehlerfunktion setzen
  ErrCall('Proc:Err');
  // Verarbeitung
  Func();
  // Fehlerfunktion leeren
  ErrCall('');
}

Wird der Laufzeitfehler ausgelöst, wird die Funktion „Err“ in der Prozedur „Proc“ aufgerufen. Innerhalb der Funktion kann über das System-Objekt die Position des Fehlers identifiziert werden:

sub Err
()

  local
  {
    tErrCode   : int;
    tErrText   : alpha;
    tErrProc   : alpha;
    tErrLine   : int;
    tErrSource : alpha;
  }

{
  // Fehlerwert
  tErrCode   # _Sys->spErrCode;
  // Fehlertext
  tErrText   # _Sys->spErrText;
  // Prozedur und –funktion in der der Fehler auftrat
  tErrProc   # _Sys->spErrProc;
  // Zeile in der der Fehler auftrat
  tErrLine   # _Sys->spErrLine;
  // Evtl. inkludierte Prozedur in der der Fehler auftrat
  tErrSource # _Sys->spErrSource;

  // Fehler protkollieren
  …
  // E-Mail versenden
  …
}

Mit dem folgenden Konstrukt besteht sogar die Möglichkeit die Laufzeitfehlermeldung von CONZEPT 16 zu unterdrücken und in der Fehlerfunktion eine eigene Meldung anzuzeigen:

main
{
  ErrCall('Proc:Err');

  // Behandlung aller Laufzeitfehler aktivieren
  ErrTryCatch(_ErrAll, true);
  try
  {
    Func();
  }
  // Behandlung aller Laufzeitfehler deaktivieren
  ErrTryCatch(_ErrAll, false);

  ErrCall('');
}

Funktionsaufrufe

Bei stark verschachtelten, rekursiven oder iterativen Funktionsaufrufen kann es vorkommen, dass die Position im Quelltext alleine nicht ausreicht um die Ursache für den Fehler zu finden. In diesem Fall besteht die Möglichkeit den Kontext anhand der Funktionsaufrufe, dem Call stack, zu ermitteln.

Mit der Funktion DbgDump() können die aktuellen Funktionsaufrufe in eine Datei geschrieben gewerden. Die Protokollierung der Funktionsaufrufe kann mit der Funktion DbgControl() aktiviert und deaktiviert werden.

Beispiel

sub Func3
()
{
  // Löst Laufzeitfehler „Division durch Null“ aus
  if (1 / 0 = 0) { … }
}

sub Func2
()
{
  Func3();
}

sub Func1
()
{
  Func2();
}

sub Func
{
  // Protokollierung der Funktionsaufrufe aktivieren
  DbgControl(_DbgCallStackOn);

  // Stark verschachtelte Funktionsaufrufe
  Func1();

  // Protokollierung der Funktionsaufrufe deaktivieren
  DbgControl(_DbgCallStackOff);
}

main
{
  ErrCall('Proc:Err');
  Func();
  ErrCall('');
}

sub Err
{
  DbgDump('C:\MyApp-CallStack.txt', _DbgDumpCallStack);
}

Die geschriebene Datei enthält anschließend folgenden Inhalt:

### CALL STACK DUMP BEGIN
[1] Proc:Err
[2] Proc:Func3
[3] Proc:Func2
[4] Proc:Func1
### CALL STACK DUMP END

Die Funktion DbgDump() kann auch unabhängig von der mit ErrCall() gesetzten Fehlerfunktion benutzt werden, wenn die Position im Quelltext bereits bekannt ist.

Die Protokollierung der Funktionsaufrufe sollte nur zur Identifizierung von Fehlern eingesetzt werden, da sie das Laufzeitverhalten der aufgerufenen Funktionen negativ beeinflusst.

Fazit

Mit dem Setzen einer Fehlerfunktion mit ErrCall() und dem Speichern der Funktionsaufrufe mit DbgDump() haben Sie praktische Funktionen zur Identifizierung von Laufzeitfehlern zur Hand. Die Beseitigung obliegt aber leider immer noch dem Entwickler ;).

3 Kommentare

3 Kommentare “Laufzeitfehler finden”

  1. Es ist immer die gleiche Subroutine und die Zeilennummer ändert sich mit, wenn die Prozedur länge oder kürzer wird.
    Die Subroutine liest eigentlich nur die Custom-Eigenschaft eines Objekts und liefert sie zurück.
    Ich habe bereits versucht in der ErrCall-Prozedur gleich zu Beginn die SpErr-Eigenschaften auszulesen und lokal zu merken, aber ohne Erfolg.
    (ErrCall-Prozedur schickt eine Mail an mich mit den Daten)

  2. Das sollte natürlich nicht so sein. Wenn dieses Verhalten nur sporadisch auftritt, nicht reproduzierbar ist und die beeinflußenden Faktoren nicht bekannt sind, ist es für uns aber leider auch schwer der Ursache auf den Grund zu gehen. Können Sie das Verhalten auf bestimmte Bedingungen reduzieren?

  3. Leider sind die SpErr…-Werte nicht immer konsistent
    Ich habe immer noch sporadisch bei Kunden einen Fehler -173
    in einer Subroutine, die gar keinen globalen Datenbereich enthält und als Zeile erhalte ich die letzte Zeile der Prozedur (die mehr als 1500 Zeilen hinter dem Ende der Subroutine liegt)

Kommentar abgeben