Statische Codeanalyse

Bugs und Defekte in Multitasking-Software eliminieren

22.01.2009 | Autor / Redakteur: David Kalinsky* / Martina Hafner

*Dr, David Kalinsky ist Leiter für Kundentrainings bei D. Kalinsky Associates Technical Training, USA und ein beliebter Dozent in USA, Europa und Israel. Kontakt: david@kalinskyassociates.com.
*Dr, David Kalinsky ist Leiter für Kundentrainings bei D. Kalinsky Associates Technical Training, USA und ein beliebter Dozent in USA, Europa und Israel. Kontakt: david@kalinskyassociates.com.

Dank einer neuen Generation statischer Analysetools lassen sich Multitasking-Fehler leichter aufspüren, die oft in Verbindung mit einem Echtzeitbetriebssystem auftreten und mit herkömmlichen Softwaretests nur schwer gefunden werden. Erst seit kurzem eignen sich diese Tools für das riesige Spektrum an Fehlern, die typischerweise in Echtzeit- Software auftreten, in denen es zu Interaktionen zwischen mehreren Tasks sowie mit einem RTOS kommt.

Vor etwa 30 Jahren, in meinem ersten Job im Bereich Echtzeitsysteme, leitete ich ein Softwareentwicklungsteam, das an einem Erste-Hilfe-Monitor arbeitete (das Gerät war etwa einen Kubikmeter groß, also nicht so kompakt wie die Monitore in modernen medizinischen Geräten, wie wir sie heute kennen). Eines Tages rief mich der Firmenchef zu sich und fragte mich: „David, warum schaffen Sie es nicht, dass Ihre Software auf Anhieb fehlerfrei läuft?“ Er fuhr verärgert fort: „Laut Ihren Berichten haben Sie die Software doch fertig gestellt, aber danach sind Sie noch monatelang damit beschäftigt, sie von sogenannten Bugs zu befreien. Können Sie die Software denn nicht einfach von Anfang an richtig programmieren? Sie würden sich eine Menge Frust und ich mir einen Haufen Geld sparen.“ Damals in den Siebzigern hatte ich noch keine Antwort auf diese Fragen. Ich dachte, dass er mich feuern würde und meine Karriere im Embedded-Bereich damit beendet sei.

Machen wir einen Zeitsprung in die Gegenwart. Dreißig Jahre sind inzwischen vergangen, und Embedded-Entwickler haben viel über Wirtschaftlichkeit und Management in der Softwareentwicklung gelernt. Trotzdem wird auch heute noch gut die Hälfte der Entwicklungszeit dafür aufgewendet, Fehler im Softwarecode zu finden und zu beheben.

Seit kurzem gibt es einen Hoffnungsschimmer am Horizont der Softwareentwicklung — eine neue Generation von Tools für die statische Codeanalyse. Diese analysieren Softwarecode auf Bugs und andere Defekte, ohne dass die Programme selbst ausgeführt werden müssen. Sie basieren auf bestehenden Analysatoren wie beispielsweise MISRA-C-Compilern und LINT und verwenden Algorithmen, die den Sourcecode akribisch auf Softwarefehler prüfen und gefundene Fehler zuverlässig und hochpräzise anzeigen.

Bild 1: Codedefekt, angezeigt von einem statischen Analysetool. Der statische Analysator meldet, dass ein Buffer zu klein ist.
Bild 1: Codedefekt, angezeigt von einem statischen Analysetool. Der statische Analysator meldet, dass ein Buffer zu klein ist.

Die Ausführungspfade durch die Codebasis werden voll automatisch überprüft. Dabei identifizieren die Tools komplexe Defekte sowie Fehler, die die Interaktion zwischen mehreren Prozeduren betreffen und über zahlreiche Sourcecode-Dateien verteilt sind.

Mit den neuesten Versionen dieser statischen Analysetools lassen sich sogar einige Multitaskingfehler aufspüren, die oft in Verbindung mit einem Echtzeitbetriebssystem (RTOS) auftreten. Mit herkömmlichen Softwaretests werden diese nur schwer gefunden, da sie selten und scheinbar sporadisch auftreten. Statische Analysetools jedoch ermöglichen die deterministische Identifikation solcher Bugs.

Ergänzendes zum Thema
 

RTOS-Spezialkurse mit Dr. David Kalinsky in Deutschland

Ergänzendes zum Thema
 

Expertenmeinung des Autors

Kriterien für die Codeabdeckung: Line und Path Coverage

Ein wichtiges Kriterium bei der Identifizierung und Beseitigung von Softwarefehlern ist die Codeabdeckung. Sie definiert, welcher Teil der Codebasis in welcher Tiefe untersucht wird, und lässt sich unterteilen in Line Coverage (Anweisungsabdeckung) und Path Coverage (Pfadabdeckung).Die Line Coverage misst den Anteil aller Codezeilen, die auf Defekte untersucht werden. In vielen Embedded-Projekten liegt das Ziel bei einer Line Coverage von 100%, was sich aufgrund technischer Einschränkungen jedoch häufig nicht vollständig umsetzen lässt. Doch selbst wenn 100% Line Coverage erreicht werden, ist noch längst nicht sichergestellt, dass alle Defekte im Code identifiziert werden. Hier ein Beispiel:

void npd_switcher(int cond) {

char * c = 0;

char d[] = “a”;

if (cond) {

c = d;

*c = ‘b’;

}

*c = ‘c’;

}

In diesem Beispiel wird über einen einzelnen Testfall, der npd_switcher(1); aufruft, jede Codezeile der Funktion ausgeführt. Es wird zwar eine Line Coverage von 100% erreicht, doch es befindet sich immer noch ein gravierender Fehler im Code. Wenn die Eingangsvariable cond den Wert 0 hat, versucht die letzte Zuweisungsanordnung in der Funktion, die Adresse 0 zu beschreiben; ein Null-Pointer-Zugriff ist jedoch nicht erwünscht. Trotz 100 Prozent Line Coverage bleibt ein grundlegender Fehler unentdeckt.

Die effektivere Methode der Abdeckung ist die Path Coverage. Sie misst den Anteil aller Pfade durch jede Funktion einer Applikation, die auf Fehler überprüft werden sollen. Stellen Sie sich vor, Sie erzeugen einen Ablaufplan für eine Applikation, in dem jeder Entscheidungspunkt (if, while, switch, etc.) eine Verzweigung im Ablaufplan darstellt. Die obige Funktion npd_switcher(); hat zwei Pfade. Der erste Pfad nimmt bei if(cond) die Abzweigung TRUE und ist der gleiche Pfad, der für diesen Code 100% Path Coverage erzielt hat. Der zweite Pfad folgt bei if(cond) der Abzweigung FALSE. Es ist ein realer Pfad, auch wenn es für die ELSE-Bedingung keinen Code gibt. Tatsächlich ist dieser zweite Pfad derjenige, der den vorher unentdeckten Null-Pointer-Zugriff enthält.

Wenn nun die Path-Coverage-Methode so effizient ist, warum kommt sie dann nicht immer zum Einsatz? Die Antwort ist einfach: Die technische Umsetzung ist nahezu unmöglich, und der Arbeitsaufwand wäre unglaublich hoch. Die Anzahl von Pfaden in einer Softwarefunktion kann exponentiell zu der Anzahl an Zuständen und Entscheidungen im Code ansteigen. Schleifen können zu einer sehr großen Anzahl von Pfaden führen.

Um die Path Coverage Methode effektiv einzusetzen, muss man also die Anzahl der zu überprüfenden Pfade begrenzen. Nehmen wir als Beispiel die Norm DO-178B Level „A“ – der höchste Level, bei dem ein Softwareausfall in einem sich in der Luft befindlichen Flugzeug katastrophale Folgen hätte. Diese Norm fordert bei Software für Luftfahrtanwendungen den Abdeckungsgrad „Modified Condition/Decision Coverage“ (MC/DC). Im Wesentlichen besagt dies, dass an jeder Abzweigung in der Software jeder Zustand und jede Entscheidung auszuführen sind und damit mindestens einmal jeden möglichen Wert annehmen.

Vor- und Nachteile dynamsicher Analysetools

Tools zur Überprüfung der Codequalität lassen sich in dynamische und statische Analysetools unterteilen.

Dynamische Analysetools werden schon seit vielen Jahren eingesetzt. Sie überprüfen ausführbaren Code zur Laufzeit und melden Codefehler, die bei der Ausführung des Programms auftreten. Diese Tools eignen sich hervorragend zum Erkennen von Defekten, wie dynamischen Speicherzugriffsfehlern und Ressourcen-Leaks.

Dynamische Analysetools zeichnen sich durch eine niedrige Falsch-Positiv-Rate aus. Wenn diese Tools also einen Fehler melden, ist es ein tatsächlicher Fehler. Diese Tools irren sich selten, denn sie überprüfen den Code und sehen sämtliche Informationen über die Zustände der Software und deren Eingangswerte, während das Programm ausgeführt wird. Die Analyseergebnisse sind entsprechend relevant – es werden tatsächliche Fehler erkannt, die während der Ausführung der realen Software aufgetreten sind, und nicht nur Fehler, die auftreten könnten, wenn 500 Anwender 500 Jahre lang auf 500 Tastaturen herumtippen.

Doch dynamische Analysetools haben auch Schwachstellen. Fehler werden erst in einer späten Phase des Softwareentwicklungsprozesses gefunden, da diese Tools eine ausführbare Version der Programme benötigen. Subtilere Fehler, d.h. solche, die in integrierten Softwareteilen auftreten, werden erst nach der vollständigen Integration der Software entdeckt. Darüber hinaus deckt die dynamische Analyse nur die Testfälle ab, die tatsächlich ausgeführt wurden. Wurden also Testfälle für eine Line Coverage von nur 95% unter den wachsamen Augen eines dynamischen Analysetools ausgeführt, dann würden diesem Tool wahrscheinlich viele andere Defekte entgehen.

In der Embedded-Softwareentwicklung ist es schwierig, sporadisch auftretende Fehler mit dynamischen Analysetools zu identifizieren. Das gilt auch für Fehler, die durch die Unterbrechung von Tasks in einer präemptiven Multitaskingumgebung verursacht werden, wenn ein Softwareentwickler einen kritischen Abschnitt nicht ausreichend geschützt hat.

Statische Analysetools als ergänzende Werkzeuge

Statische Sourcecode-Analysetools analysieren die Software-Codebasis auf Defekte, ohne die Programme auszuführen, die aus der Software entstehen. Sie können die Ausführungspfade in der Codebasis vollautomatisch abdecken. Statische Analysetools identifizieren Bugs und Defekte in einer frühen Phase des Entwicklungsprozesses, denn sie benötigen keine ausführbare Version des Codes, der untersucht werden soll. Mit statischer Analyse lassen sich im Extremfall sogar Bugs finden, noch bevor der Code erfolgreich kompiliert wurde. Manuell erzeugte Testfälle sind nicht erforderlich, da die Analysealgorithmen selbst die relevanten Pfade durch den zu testenden Code identifizieren, ebenso wie die für eine Analyse geeigneten Datenwerte. Die statische Analyse ist eine verhältnismäßig schnelle Methode. Für eine Codebasis mit mehreren Millionen Zeilen fallen nur wenige Stunden an. Die schnelle Analyse von Code eines Entwicklers direkt an dessen Arbeitsplatz dauert nur ein paar Sekunden.

In der Embedded-Softwareentwicklung können statische Analysetools sporadisch auftretende Fehler entdecken, wie z.B. solche, die durch die Unterbrechung von Tasks in einer präemptiven Multitaskingumgebung verursacht werden, wenn ein Softwareentwickler einen kritischen Abschnitt nicht ausreichend geschützt hat. Die Fehlersuchalgorithmen eines statischen Analysetools können diese Art von Bugs konsistent und wiederholbar auffinden, selbst wenn der Bug nicht konsistent oder wiederholbar zur Programmlaufzeit auftritt.

Die beiden Toolkategorien – dynamische und statische Analysetools – stehen somit also nicht in Konkurrenz zueinander, sondern sie ergänzen sich: Statische Analysetools sind in Bereichen stark, die für dynamische Analysetools problematisch sind, und umgekehrt.

Vielfalt und Komplexität von Softwarefehlern

Mit Tools zur statischen Sourcecode-Analyse lassen sich viele verschiedenartige Fehler und Defekte, die die Softwarequalität und -sicherheit beeinträchtigen, präzise identifizieren. Die nachfolgende Liste zeigt einige der Fehlerarten, die mit den erhältlichen, kommerziellen statischen Sourcecode-Analysatoren identifiziert werden können:

  • Null-Pointer-Zugriffe
  • Benutzung von freigegebenem Speicher
  • Doppelte Freigabe eines Buffers
  • Array-Indexfehler
  • Fehlanpassung von new/delete
  • Stack-Überlauf
  • Heap-Überlauf
  • Rückgabe eines Pointers auf lokale Variablen
  • Logisch inkonsistenter Code
  • Ungesicherte Verwendung von Nutzerdaten
  • Nicht initialisierte Variablen
  • Unerlaubte Verwendung von Negativwerten
  • Übergabe großer Parameter ‚by value’
  • Unterallokation dynamischer Daten
  • Memory Leaks
  • File-Handle Leaks
  • Netzwerkressourcen-Leaks
  • Nicht verwendete Werte
  • Nicht behandelte Return-Codes
  • Verwendung ungültiger Iteratoren

Fehler und Defekte werden auf einem GUI dargestellt, das ähnlich aufgebaut ist wie die Benutzeroberflächen, die Softwareentwickler von Source-Level-Debuggern kennen.

Oft sind die Fehler und Defekte, die mit statischen Analysetools identifiziert wurden, nicht auf eine einzelne Codezeile oder Codefunktion begrenzt. Viele der subtileren Defekte sind „interprozedural“. Das heißt, ein Fehlerszenario kann sich über mehrere Schritte entwickeln, wobei sich einige dieser Schritte in einer Codefunktion befinden und weitere Schritte – oder letztendlich ein Ausfall selbst - in einer anderen Codefunktion.

Manche der entdeckten Fehler sind hochkomplex. Die GUIs statischer Analysetools stellen daher jeden Fehler in einem eigenen Fenster dar. Die an der Entstehung des Defektes beteiligten Codezeilen werden gekennzeichnet und erklärt. Werden mehrere Fehler in wenigen Codezeilen derselben Einzelfunktion gefunden, erhält jeder Fehler ein eigenes Fenster. Bei einem Defekt, der mehrere Softwareprozeduren betrifft, wird jede relevante Prozedur in einem separaten GUI-Fenster dargestellt.

Fehler in RTOS-basierendem Code

Bild 2: eine Race Condition im Temperaturdaten-Bereich verursacht Anzeigefehler
Bild 2: eine Race Condition im Temperaturdaten-Bereich verursacht Anzeigefehler

Seit einigen Monaten werden statische Analysetools vermehrt auch in RTOS-Taskingmodellen eingesetzt. Sie eignen sich damit auch für das Auffinden von Multitaskingfehlern, die beim Einsatz eines RTOS oft auftreten können, wie z.B Race Conditions, Task Blocking, Deadlocks, Lockouts. Eine Race Condition ist eine Wettbewerbssituation, die auftritt, wenn Tasks ohne Locking-Mechanismus auf gemeinsame Daten zugreifen, wie in Abbildung 2 dargestellt.

Die Task “Anzeige” muss hier ausgeführt werden und greift auf den gemeinsamen Datenbereich zu, der gerade von der Task „Messung“ beschrieben wird. Beim Ausführen der Software kann dies sporadische Datenkorruptionen verursachen. Es kommt zu einer sogenannten Race Condition, hier dargestellt als Anzeigefehler: In einer Abfolge von Anzeigewerten von 100 und 99 erscheint ein Wert von 199. Ein statisches Analysetool überprüft in diesem Fall den Sourcecode auf Daten, auf die mehrere Tasks zugegriffen haben, von denen mindestens eine ein Schreibvorgang war. Das Tool prüft, dass das Lesen und Schreiben nicht als atomare Operationen stattfindet. Dann wird der Code für alle diese Datenbereiche nochmals durchsucht, um herauszufinden, welches Software-Lock vor dem Zugriff auf diese Daten normalerweise gesperrt und nach dem Zugriff wieder freigegeben wird. Das Lock bezeichnet man in diesem Fall als „Bewacher“ der Daten. Anschließend wird der Code nochmals durchsucht, um herauszufinden, wo Zugriffe auf diese Daten möglich sind, d.h. wo der „Bewacher“ die Daten nicht ausreichend schützt. Fehler dieser Art werden als Softwaredefekt angezeigt.

Bild 3: Beispiel für Task-Blocking. Eine Task hält über einen langen Zeitraum ein Lock, wodurch eine Task höherer Priorität sehr lange blockiert wird.
Bild 3: Beispiel für Task-Blocking. Eine Task hält über einen langen Zeitraum ein Lock, wodurch eine Task höherer Priorität sehr lange blockiert wird.

Ein weiterer Gleichzeitigkeitsfehler, den viele statische Analysetools beheben können, ist das Task Blocking: Eine Task hält über einen langen Zeitraum ein Lock, wodurch eine Task höherer Priorität sehr lange blockiert wird. Es handelt sich dabei um eine Variante der Prioritätsinversion. Abbildung 3 zeigt ein Beispiel mit POSIX.

Andere Tasks (POSIX ‘Threads’) höherer Priorität, die den Mutex sperren wollen, könnten in diesem Beispiel bis zu drei Sekunden blockiert werden. Dies ist natürlich eine verhältnismäßig einfache Form der Prioritätsinversion und weit entfernt von der automatischen Identifizierung einer „unbegrenzten Prioritätsinversion“ - ein viel subtileres Phänomen, doch ein gravierender Softwaredesignfehler.

Bild 4: beispielhaftes Task-Deadlock
Bild 4: beispielhaftes Task-Deadlock

Mit statischer Analyse lassen sich auch verschiedene Arten von Task-Deadlocks erkennen; beispielhaft in Abb. 4 dargestellt.Wenn die Software tatsächlich ausgeführt wird, tritt die Verklemmungssituation vielleicht nur selten auf, doch ein statisches Analysetool erkennt bei jeder Analyse das Deadlock-Potential. Das Tool sucht erst nach einer zirkulären Abhängigkeit zwischen Tasks und anschließend nach einer inkonsistenten Reihenfolge von gemeinsamen Locks, welche die Tasks miteinander verbinden.

Bild 5: Beispiel für einen Lockout-Fehler. Wenn diese Task vergisst, ihr Lock wieder freizugeben, müssen andere Tasks für unbestimmte Zeit darauf warten.
Bild 5: Beispiel für einen Lockout-Fehler. Wenn diese Task vergisst, ihr Lock wieder freizugeben, müssen andere Tasks für unbestimmte Zeit darauf warten.

Auch eine vierte Klasse von Gleichzeitigkeitsfehlern lässt sich mit statischer Analyse auffinden, die sogenannten Lockouts. Abbildung 5 zeigt einen typischen defekten Code.

Wenn diese Task (‘Thread’) vergisst, ihr Lock wieder freizugeben, müssen andere Tasks für unbestimmte Zeit darauf warten. Die Task selbst könnte sogar blockiert werden, wenn sie später versucht, das Lock wieder zu verwenden.

Was statische Analysetools heute noch nicht leisten können

Statische Analysetools können heute schon etliche Multitaskingfehler entdecken und werden in der Zukunft sicher noch effizienter arbeiten. Sie können z.B. noch keine Nachrichten nachverfolgen, die über den Queueing-Mechanismus eines Betriebssystems zwischen den Tasks ausgetauscht werden. Das Nachrichtenversenden wird besonders in hochkomplexen Systemen vermehrt eingesetzt, z.B. für den Einsatz in verteilten und Multicore-Umgebungen, doch sind statische Analysatoren heute technisch noch nicht in der Lage, Datenwerte nachzuverfolgen, wenn sie über eine Messaging-Funktion zwischen den Tasks ausgetauscht werden.

Andererseits könnte das Auffinden von nur einer Race Condition, einem Deadlock oder einem Lockout die Ausgaben für ein statisches Analysetool rechtfertigen, wenn man den enormen finanziellen und zeitlichen Aufwand für das Aufspüren solcher Defekte mit herkömmlichen Test- und Debugtools betrachtet.

Literaturhinweise:

Boehm, B., “Software Engineering Economics”,

1981, ISBN: 0138221227

DO-178B, „Software Considerations in Airborne Systems and Equipment Certification“, RCTA, December 1992.

Kommentar zu diesem Artikel abgeben

Schreiben Sie uns hier Ihre Meinung ...
(nicht registrierter User)

Kommentar abschicken

 

Copyright © 2017 - Vogel Business Media