Rund die Hälfte des Codes in der Embedded-Entwicklung ist immer noch handgeschriebenes C. Was einerseits eine tolle Sprache ist: einfache Schnittstelle zu Hardwaregeräten, für beinahe jeden Prozessor einsatzbereit, und flexibel genug für sehr dichten und leistungsstarken Code. Andererseits ist C auch sehr riskant: Aufgrund ihrer großen Flexibilität können Programmierern Fehler einfach unterlaufen. Wegen der liberalen Auslegung des Standards für ein zulässiges C-Programm finden Compiler viele unterschiedliche Fehler nicht. Zudem ist die Programmiersprache gespickt mit Mehrdeutigkeiten. So kann ein Code, der perfekt mit einem Compiler funktioniert, beim Einsatz eines anderen Compilers fehlschlagen, weil jeder auf einer unterschiedlichen, aber gültigen Interpretation des Standards basiert. Das macht C-Programme anfällig für ernste Speicherzugriffsfehler, wie zum Beispiel Buffer Overruns und Null-Pointer-Ausnahmen, und andere Fehler wie Speicherlöcher, Nutzung von uninitialisiertem Speicher oder Use-After-Free-Fehler.
Beim Einsatz von C ist also enorme Vorsicht gefordert. Sorgfältige Tests und der Einsatz von hoch entwickelten statischen Analysetools wie Code-Sonar haben sich in den vergangenen Jahren bewährt. Eine weitere Methode ist ein Zugriffsverbot auf die gefährlicheren Konstrukte während der Programmierung, wie ihn bereits mehrere Programmierstandards verfolgen. Der ausgereifteste und populärste darunter ist MISRA C – ursprünglich von der Motor Industry Software Reliability Association entwickelt und heute in den Branchen Automotive, Luft-/Raumfahrt, Medizinelektronik und Industriesteuerungen etabliert. Sein Ziel ist die Förderung von sicheren, zuverlässigen und portierbaren Programmen, die in ISO-C für Embedded-Systeme geschrieben wurden.
Die neueste Auflage MISRA C:2012 gilt als eine Verbesserung zum vorhergehenden Standard. Sie deckt C99 ab, wobei die Anwendbarkeit für C90 erhalten bleibt. Ihre Guidelines sind aufgeteilt in 143 statistisch prüfbare Regeln und 16 „Richtlinien“, die Entwicklungs-Maß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.
Verstöße gegen MISRA-Regeln aufdecken
MISRA C legt das Hauptaugenmerk auf die Unterstützung von automatisierten statischen Analyse-Tools, um Ver-
stöße gegen den Standard aufzudecken. Tool-Support ist enorm wichtig. Darum sollten Entwickler die Objekt-Arten verstehen, die Analyse-Tools auffinden können. Einige bemerken nur oberflächliche syntaktische Eigenschaften des Codes, während hoch entwickelte Werkzeuge tiefes semantisches Wissen über das gesamte Programm bieten. Diese Unterscheidung ist bedeutend, weil manche Regeln einen sehr tiefgehenden Ansatz verlangen.
MISRA C:2012 markiert jede Regel mit ihrer Anwendbarkeit in der statischen Analyse. Regeln werden 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 beim Übersetzen beteiligt sind, prüfen muss, um eine Verletzung zu kennzeichnen. Als Single Translation Unit markierte Regeln sind relativ einfach durchsetzbar. Viele Compiler bieten einen Modus, um solche Verstöße als Warnungen zu melden.
Bedeutender ist die Entscheidbarkeit der Regel: Eine als „bestimmbar“ gekennzeichnete Regel besagt, dass es für statische Analyse-Tools möglich ist, alle Verletzungen ohne False-Positive-Ergebnisse (Fehlalarme) zu entdecken; dazu gehören die meisten der oberflächlichen syntaktischen Regeln. Im Gegensatz dazu bedeutet eine „unbestimmbare“ (undecidable) Regel, dass es für ein statisches Analyse-Tool nachweislich unmöglich ist, alle Verletzungen ohne einen Fehlalarm aufzufinden. Aber das bedeutet keinesfalls, 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.
Auch wenn statische Analyse-Tools nicht alle Verletzungen von unbestimmbaren Regeln aufspüren können, ist ihr Einsatz entscheidend, um so viele Verstöße wie möglich aufzudecken. Denn hier verbergen sich gerne die meisten kritischen Fehler. Zur Vermeidung enthält MISRA C eine besonders wichtige Regel und ebenso eine wichtige Richtlinie, die beide auf die Achilles-Sehne der C-Programme abzielen: Regel 1.3 besagt: „Es darf kein undefiniertes oder kritisches unspezifiziertes Verhalten eintreten“, und in Richtlinie 4.1 steht: „Laufzeitfehler müssen minimiert werden“.
Undefiniertes Verhalten wird im ISO-Standard für C (Annex J in C99) ausführlich besprochen und deckt viele Aspekte der Sprache ab. Wenn ein C-Programm bei undefiniertem Verhalten gar nichts tut, ist das völlig standardkonform. Es wird oft als „Catch Fire“-Semantik bezeichnet, weil der Compiler den Computer in Brand setzen kann. Darum lassen die Compilerschreiber ihre Compiler die am sinnvollsten erscheinende Aktion ausführen. Vernünftigerweise melden die Übersetzungsprogramme solche Kompilierungsfehler. Ist undefiniertes Verhalten nicht auffindbar, dann hat der Compilerverfasser im Grunde genommen keine Wahl außer anzunehmen, dass dies nicht passieren kann.
Undefiniertes Verhalten lässt sich auch vom gründlichsten Programmierer nur schwer verhindern. Trotzdem ist es immer ein Grund zur Besorgnis, weil es viele der schlimmsten Fehler auslösen kann, zum Beispiel Buffer Overruns und Underruns, Invalid-Pointer-Indirections, 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 sie von der Regel 1.3 und Richtlinie 4.1 abgedeckt, auch wenn jeder dieser Fehler eine Verletzung des Standards bedeutet.
Weniger gefährlich ist das unspezifizierte Verhalten trotz seiner Tücken. In diesem Fall bestimmt der Standard rechtliche 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 bei der Übersetzung von verschiedenen Compilern eine andere Semantik aufweisen kann. Das ist sogar möglich, wenn der Verfasser denselben Compiler mit verschiedenen Optionen aufruft.
Statische Analyse erleichtert Konformität
Auch wenn einfache statische Analyse-Tools einige der offensichtlicheren Fehler finden können, spüren nur die ausgereiften Tools die subtileren Ereignisse auf. Geht es um ein statisches Analyse-Tool für die Kompatibilität mit MISRA C, dann empfiehlt sich ein Tool, das Verletzungen der oberflächlichen syntaktischen Regeln genauso wie die schwerer auffindbaren Bugs entdeckt.
Ein Einblick in die Funktionsweise moderner statischer Analyse-Tools wie Code-Sonar hilft, diese zu verstehen: Die Werkzeuge erstellen ein Modell des Programms und führen darauf Abfragen durch, um Unregelmäßigkeiten aufzuspüren. Das Modell wird erzeugt, indem die Tools den Code analysieren und einen Satz an Darstellungen schaffen, die die wichtigen Aspekte der Programm-Semantik erfassen. Diese Darstellungen ähneln den von den Compilern eingesetzten Darstellungen und beinhalten abstrakte Syntaxbäume (Syntax Trees, AST), Symboltabellen, Programmablauftabellen (Control-Flow-Graphs, CFGs), Typ-Hierarchien und den Aufrufgraphen (Call Graph). Während oberflächliche Objekte des Codes durch einfachen Mustervergleich (Pattern Matching) auf dem AST berechnet werden können, erfordert das Aufspüren tieferer semantischer Fehler ausgetüftelte Algorithmen. Diese ahmen eine echte Ausführung des Programms nach, nutzen aber anstelle von konkreten Werten für Variable ein Set an Gleichungen, das den abstrakten Programmstatus abbildet.
Ein hoch entwickeltes Analyse-Tool, das ernste Fehler findet, ist durchaus in der Lage, auch Verletzungen der syntaktischen Regeln aufzudecken. Aber, die meisten der einfachen statischen Analyse-Tools, die syntaktische Verletzungen finden, finden im Allgemeinen die tieferen Fehler nicht. Wird es nicht angewendet, ist das beste statische Analyse-Tool nutzlos. Sinnvoll ist seine Integration in den Entwicklungsprozess. Entwicklerteams sollten die Code-Analyse damit einfach ausführen und bei der Korrektur von aufgefundenen Fehlern zusammenarbeiten können – idealerweise als Client-Server-Modell. So ist es für die Qualitätssicherung einfach, die Fundstellen zu sichten, zu markieren und zur Behebung anzuweisen. Zudem ermöglicht diese Architektur regelmäßige oder durch Änderungen automatisch ausgelöste Analysen bis hin zu allgemein verständlichen Berichten an das Management, um Fortschritt und Qualität zu beobachten, oder noch wichtiger, um Kompatibilität mit anwendbaren Standards nachzuweisen.
Anforderungen für statische Analyse-Tools
Genauigkeit: Das Tool kann einen Code genau in der gleichen Art wie der Compiler zerlegen. Alle Compiler sind unterschiedlich. Analyse-Tools, die das nicht in Betracht ziehen, können falsche Ergebnisse liefern.
Analyse über das gesamte Programm: Das Tool sollte den Informationsfluss zwischen Abläufen und Grenzen der Kompiliereinheit verfolgen.
Analyse betrachtet Datenfluss, Zusammenhänge und Pfade: Präzise Fehlerfindung und Berichterstattung.
Löschen von unausführbaren Pfaden: Damit kann das Tool die Anzahl der False-Positive-Ergebnisse reduzieren. Die besten Tools nutzen hoch entwickelte Technologien wie SMT-Solver.
Native MISRA Checker: Mithilfe von MISRA C:2012 Checkern wird das Einhalten des Standards sichergestellt. Partnerschaften oder lediglich die Einhaltung vorheriger Versionen des Standards bieten keine adäquate Leistung.