fotolia-78942607-l.jpg

red150770 – Fotolia

Die Programmiersprache C ist aus gutem Grund bei Embedded-Entwicklern äußerst beliebt: Sie eignet sich für hardwarenahen Code, hochwertige Compiler gibt es praktisch für jeden Prozessor und C ist flexibel genug, um dichten und leistungsstarken Code zu schreiben. Leider ist C auch risikobehaftet: Wegen der großen Flexibilität unterlaufen selbst den besten Programmierern Fehler. Die Sprache erlaubt viele sehr effiziente Konstrukte, die aber empfindlich auf unerwartete Eingabewerte reagieren: Berüchtigt sind zum Beispiel Buffer-Overflows oder Null-Pointer-Exceptions, aber auch Speicherlöcher, Nutzung von uninitialisiertem Speicher und Use-after-Free-Fehler schleichen sich schnell ein.

Weil der Standard für ein zulässiges C-Programm liberal ausgelegt ist, können die Compiler viele Fehler gar nicht erkennen. Zudem steckt der Standard voller Zweideutigkeiten. So kann Code, der perfekt mit einem Compiler funktioniert, beim Einsatz eines anderen versagen.

Diese Nullzeiger-Dereferenzierungs-Warnung eines statischen Analysetools zeigt den Pfad durch den Code, der den Fehler auslöst.

Diese Nullzeiger-Dereferenzierungs-Warnung eines statischen Analysetools zeigt den Pfad durch den Code, der den Fehler auslöst.Grammatech

MISRA-C:2012 für Embedded-Anwendungen

Neben sorgfältigen Tests und modernen statischen Analysetools wie Codesonar lässt sich das Risiko durch enge Vorgaben eingrenzen, welche Konstrukte im Code erlaubt sind und welche nicht. Diesen Ansatz verfolgen mehrere Codierungsstandards, darunter ist MISRA-C der wohl ausgereifteste und populärste. Die Motor Industry Software Reliability Association will damit sicheren, zuverlässigen und portierbaren ISO-C-Programmcode erreichen. Seit der Einführung der ersten Auflage im Jahr 1998 und einer anschließenden Revision in 2004 hat er sich sehr breit etabliert und findet heute Einsatz in Bereichen über Automotive hinaus, bis hin zur Luft- und Raumfahrt, in Medizingeräten und Industriesteuerungen.

Die neueste Auflage MISRA-C:2012, auf die sich dieser Beitrag bezieht, deckt neben der ISO-C-Sprachversion C90 auch das jüngere C99 ab. MISRA-C:2012 gilt als wesentliche Verbesserung zum vorhergehenden Standard. Die Guidelines sind aufgeteilt in 143 statistisch prüfbare Regeln und 16 Richtlinien, die Entwicklungsmaßnahmen und -prozesse betreffen. Bei den Regeln handelt es sich in der Mehrzahl um Verbote für den Einsatz bestimmter Code-Konstrukte oder Praktiken, die von oberflächlich bis hin zu tiefgehend reichen.

Moderne statische Code-Analyse

Einer der wichtigsten Aspekte von MISRA-C ist die Unterstützung der automatisierten statischen Analysewerkzeuge, um Verstöße gegen den Standard aufzudecken. Weil der Tool-Support so wichtig ist, sollte man die Objektarten verstehen, die Analysetools entdecken können. Während einige Tools nur oberflächliche syntaktische Eigenschaften des Codes wahrnehmen, erwerben hochentwickelte Werkzeuge tiefes semantisches Wissen über das gesamte Programm.

Eckdaten

Embedded-Code in C zu entwickeln ist zwar beliebt und effizient, aber auch fehlerträchtig. Hier helfen statische Analysetools und bewährte Standards wie MISRA-C:2012. Sie spüren viel mehr fehlerhafte und verdächtige Stellen auf, als es ein Compiler kann. Wichtig ist, dass diese Werkzeuge exakt arbeiten, wenige False Positives erzeugen und dennoch möglichst viele Fehler finden.

MISRA-C:2012 markiert jede Regel mit ihrer Anwendbarkeit in der statischen Analyse. Regeln sind als „Single Translation Unit“ gekennzeichnet, wenn ein Tool den Verstoß nur durch einen Blick auf die Kompilierungseinheit finden kann, oder als „System“ wenn der Analyzer alle Kompilierungseinheiten, die zum Programm beitragen, prüfen muss, um eine Verletzung aufzuspüren. Als Single Translation Unit markierte Regeln sind relativ einfach durchsetzbar. So bieten inzwischen viele Compiler einen Modus, um solche Verstöße als Warnungen zu melden. Verletzungen von Systemregeln sind viel schwerer zu erkennen.

Wichtiger ist die Entscheidbarkeit der Regel. Eine als bestimmbar gekennzeichnete Regel bedeutet, dass statische Analysetools alle Verletzungen ohne Fehlalarme (False Positive) entdecken können; dazu gehören die meisten der oberflächlichen syntaktischen Regeln. Im Gegensatz dazu bedeutet eine unbestimmbare Regel, dass es für ein statisches Analysetool nachweislich unmöglich ist, alle Verletzungen ohne einen Fehlalarm zu entdecken. Das bedeutet keineswegs, dass die statische Analyse für solche Regeln nicht empfohlen ist, sondern nur, dass Tools manche Verstöße nicht auffinden und auch False-Positive-Ergebnisse liefern können.

Möglichst viele Fehler erkennen

Selbst wenn statische Analysetools nicht alle Verletzungen von unbestimmbaren (undecidable) Regeln aufspüren können, ist ihr Einsatz wichtig, um so viele Verstöße wie möglich aufzudecken, denn hier verbergen sich die meisten kritischen Fehler. Dafür enthält der Standard zwei besonders relevante Klauseln – eine Regel und eine Richtlinie:

  • Regel 1.3: „Es darf kein undefiniertes oder kritisches unspezifiziertes Verhalten eintreten“
  • Richtlinie 4.1: „Laufzeit-Fehler müssen minimiert werden“

Es sind wohl um die zwei wichtigsten Klauseln im gesamten Standard, die beide auf die Achillessehne der C-Programme abzielen. Undefiniertes Verhalten ist im ISO-Standard für C (Annex J im C99-Dokument) ausdrücklich besprochen und deckt viele Aspekte der Sprache ab. Oft sind C-Programmierer überrascht, dass es dem Standard völlig entspricht, wenn ein C-Programm bei undefiniertem Verhalten gar nichts tut. Manchmal wird das spöttisch als Catch-Fire-Semantik bezeichnet, weil der Compiler die Freiheit hat, quasi den Computer in Brand zu setzen. Da Compilerschreiber keine Pyromanen sind, lassen sie ihre Compiler bei undefiniertem Verhalten die am sinnvollsten erscheinende Aktion ausführen. Vernünftigerweise melden die Übersetzungsprogrammme solche Kompilierungsfehler. Ist undefiniertes Verhalten aber nicht auffindbar, dann hat der Compilerverfasser gar keine Wahl.

Diese Warnung über einen Tainted-Buffer-Zugriff erklärt dem Entwickler, wie sich sein fehlerhafter Codes auswirkt.

Diese Warnung über einen Tainted-Buffer-Zugriff erklärt dem Entwickler, wie sich sein fehlerhafter Codes auswirkt.Grammatech

Undefiniertes Verhalten in der Praxis

Undefiniertes Verhalten ist keine Seltenheit; der C99-Standard nennt 191 Varianten, darunter sogar scheinbar harmlose Dinge. Selbst der gründlichste Programmierer kann kaum alle diese Fallen umgehen. Sie sind aber immer ein Grund zur Besorgnis, weil selbst trivial erscheinende Ursachen viele der schlimmsten Fehler auslösen können, bis hin zu Buffer Overruns und Underruns, Invalid Pointer Indirection, Use After Free, Double Close, Data Races, Division by Zero und Nutzung von uninitialisiertem Speicher. Keinen davon markiert der MISRA-Standard als verboten, stattdessen sind alle durch die Regel 1.3 und Richtlinie 4.1 abgedeckt.

Unspezifiziertes Verhalten ist weniger gefährlich, hat aber auch seine Tücken. In diesem Fall bestimmt der Standard zulässige Verhaltensweisen, überlässt aber dem Compilerverfasser die Wahl, welche er anwenden will. Das gibt ihm Spielraum für die Auswahl der Interpretation mit der besten Leistung, bedeutet aber, dass der Code beim Übersetzen mit verschiedenen Compilern eine andere Semantik aufweisen kann. Das kann sogar passieren wenn man denselben Compiler mit verschiedenen Optionen aufruft.

MISRA-Tools

Geht es um statische Analyse für die Kompatibilität mit MISRA-C, dann empfiehlt sich ein Tool, das Verletzungen der oberflächlichen syntaktischen Regeln genauso wie die schwerer aufzuspürenden Bugs entdeckt. Um zu verstehen, wie moderne statische Analysetools funktionieren, hilft ein Einblick in deren Funktionsweise: Diese Werkzeuge erstellen ein Modell des Programms und führen darauf Abfragen durch, um Unregelmäßigkeiten aufzuspüren.

Selbst relativ einfache Programme haben oft schon komplexe Aufrufabfolgen (Call Graph). Statische Analysetools untersuchen jeden möglichen Ablauf.

Selbst relativ einfache Programme haben oft schon komplexe Aufrufabfolgen (Call Graph). Statische Analysetools untersuchen jeden möglichen Ablauf.Grammatech

Um das Modell zu erzeugen, analysieren die Tools den Code und erzeugen einen Satz an Darstellungen, die die wichtigen Aspekte der Programmsemantik erfassen. Diese Darstellungen ähneln den von den Compilern eingesetzten Darstellungen und beinhalten abstrakte Syntaxbäume (AST, Abstract Syntax Tree), Symboltabellen, Programmablaufgraphen (CFG, Control Flow Graph), Typ-Hierarchien und den Aufrufgraphen. Während das Tool oberflächliche Code-Objekte durch einfachen Mustervergleich (Pattern Matching) auf dem AST berechnen können, erfordert das Aufspüren tieferer semantischer Fehler ausgetüftelte Algorithmen. Diese ahmen eine echte Ausführung des Programms nach, nutzen aber statt konkreter Werte für Variable ein Set an Gleichungen, das den abstrakten Programmstatus abbildet.

Praxistauglich

Ein statisches Analysetool ist nutzlos, wenn niemand es anwendet. Sinnvoll ist daher eine Integration in den Entwicklungsprozess. Die Teams müssen ihren Code einfach analysieren können und bei der Korrektur von aufgefundenen Fehlern zusammenarbeiten. Hierfür bietet sich ein Client-Server-Modell an, mit dem Entwickler die Analysen auf ihren eigenen Workstations ausführen und die Ergebnisse an einen dauerhaften Speicher senden. Spezialisten für Qualitätssicherung können die Fundstellen dann sichten, markieren und an Entwickler zur Behebung anweisen. Dieselbe Architektur ermöglicht auch regelmäßige oder durch Änderungen automatisch ausgelöste Analysen bis hin zu Berichten an das Management, um Fortschritt und Qualität zu beobachten sowie die Kompatibilität mit anwendbaren Standards nachzuweisen.