Embedded-Software-Design Leitfaden zu Statischer Analyse

Embedded-Entwickler stehen unter größerem Druck als Programmierer in anderen Industrien: Sie müssen Anwendungen programmieren, die durchgängige und vorhersagbare Leistung über eine steigende Anzahl an heterogenen Hardware-/Prozessor-Konfigurationen bringen, die immer neue Features beinhalten, und stehen dabei vor vielen weiteren Herausforderungen.

Die Softwareentwicklung für Embedded Systeme ist einigen Besonderheiten unterworfen. Viele Methoden, die für andere Entwickler alltäglich sind, verbieten sich hier. So birgt extern entwickelter Code besondere Risiken aufgrund verschachtelter Lieferketten, eingeschränktem Zugang zum Quellcode der Bibliotheken, und der Angst vor Insidern, die heimlich Schwachstellen einschleusen. Multicore-Hardware ist immer problematisch, aber Embedded-Softwareentwickler müssen potenzielle Parallelitätsprobleme (Concurrency) besonders gut kennen, die zu unvorhersehbarem Verhalten führen oder unbeabsichtigte Angriffsflächen bieten können.

Auch die zunehmende Netzwerkfähigkeit von eingebetteten Systemen bedeutet eine große Herausforderung. Sei es Code für Netzwerkrouter, medizinische Geräte oder Home-Security-Systeme – jedes Gerät mit Netzwerkanschluss bietet eine Angriffsfläche für raffinierte Cyber-Attacken. Darüber hinaus unterliegt leistungskritische Software in Avionik, Automobilen und Konsumgeräten einer steigenden Anzahl an Standards für Codequalität und Sicherheit, zum Beispiel DO-178B/C, MISRA und ISO 26262. Für zusätzliche Komplexität und ein größeres Fehlerrisiko sorgt die »Explosion« der Embedded-Codebasis. Nach Einschätzungen der Industrie wachsen die Codebasen von eingebetteten Anwendungen um beinahe 30 Prozent pro Jahr. Das wird üblicherweise mithilfe einer Mischung aus Wasserfall- und agilen Verfahren gelöst. Unabhängig von der Philosophie steigt für jedes Teammitglied der Druck bezüglich schnellerer Zyklen für neue Features, kürzerer Reaktionszeit auf Kundenwünsche und zur Korrektur von bekannten Fehlern.

Automatisierte Fehlererkennung

Dennoch sollte der Wunsch nach Schnelligkeit nicht auf Kosten der Codeentwicklung gehen. Schließlich beinhalten C und C++, die gängigsten Sprachen für Embedded-Software besondere Risiken für die Entwickler, denn Mängel und Unklarheiten in der Sprachspezifizierung können unerwünschtes und unerwartetes Verhalten in der Ausführung mit sich bringen. Weil aber eine Embedded-Anwendung in unterschiedlichen Hardware/Firmware-Umgebungen laufen muss, kann ein Test auf unerwartetes Verhalten schwierig sein.

Wegen dieser Faktoren haben Embedded-Entwicklungsteams formalisierte Codier- und Testpraktiken übernommen. Zusätzlich zum standardmäßigen QA-Test können automatisierte Werkzeuge den Quellcode und die Ausführung einer Anwendung schon früh in der Entwicklung prüfen, wenn Fehler einfacher zu korrigieren sind, und Berichte erstellen, um die Einhaltung von Standards so effizient wie möglich zu gestalten.

Unabhängig davon, welchen Code Embedded-Entwicklungsteams schreiben, ist die Fehleridentifizierung zu einem frühen Zeitpunkt wertvoll für die Ausführung und Leistung des Codes. Aber wie wertvoll? Von allen Tools und Strategien, mit denen sich der Entwicklungsprozess von Embedded-Software verbessern lässt, bietet die automatisierte Quell-/Binary-Analyse mit die höchsten ROI-Werte (Return on Invest). Sie ist unbestritten effizienter als ein manueller Prozess. Ihr größter Wert ist allerdings, was sie zu welchem Zeitpunkt findet. Das belegt eine Studie des US-amerikanischen NIST (National Institute of Standards and Technology) von 2002. Demnach benötigt man in der Entwicklung rund fünf Stunden, um einen einzelnen Fehler zu entfernen, in einer Produktionsumgebung jedoch durchschnittlich 15 Stunden. Zu einem ähnlichen Ergebnis kommt auch einer Studie von IBM (Bild 1).

Der nachgewiesene Wert einer frühen Fehlererkennung im Software-Entwicklungszyklus und die Belastungen für die Programmierteams untermauern, dass die automatisierte Codeanalyse eine der kosteneffizientesten Investitionen für Unternehmen ist, um Entwicklungen schneller freizugeben und die Produktivität der Entwickler zu steigern. Denn: Wird die automatisierte Codeanalyse mit dem ALM-Prozess (Application Lifecycle Management) integriert, formalisiert sie die Identifizierung von schwer auffindbaren Fehlern und/oder Schwachstellen und fügt diese dem Bug-Tracking-System hinzu, wo sie priorisiert und eliminiert werden können.

Anforderungen an die Analyse

Das statische Code-Analysetool »CodeSonar« von GrammaTech hat mehr als 500 Millionen Codezeilen verarbeitet, um fehlerintolerante Anwendungen (zum Beispiel im Mars-Rover Curiosity der NASA) zu schützen. Der Hersteller definiert einige wesentliche Anforderungen für automatisierte Code-Analysetools: 

  • Analyse von Binärdateien:

Obwohl extern geschriebener Code beinahe in jeder Anwendung zum Einsatz kommt, haben Entwickler oft keine Möglichkeit, diesen zu analysieren, weil sie keinen Zugriff auf seinen Quellcode haben. Ohne diesen können automatisierte Analysewerkzeuge nur Vermutungen betreffend Qualität und Sicherheit des externen kommerziellen oder Open-Source-Codes anstellen.

Nach Schätzungen von VDC Research handelt es sich tatsächlich bei etwa 30 Prozent des Codes in Embedded-Anwendungen um externe kommerzielle Software – darum ist Quellcode oft nicht verfügbar. Ein automatisiertes Tool, das Binärdateien analysiert, kann diesen gefährlichen »blinden Fleck« ausschalten. Bild 2 zeigt ein Beispiel einer Befehls-Injektions-Schwachstelle – sie wurde in ein Programm namens »UnrealIRCd« eingeschleust. Die Zeile 5602 ist ein Aufruf an die »System()«-Funktion, deren Parameter von Daten aus einer Netzwerkverbindung stammen. CodeSonar fand den Fehler sowohl im Quellcode als auch im kompilierten Code.

  • »Nativer« Support für Standards:

Weltweit setzen sich Standards wie MISRA, DO-178B oder ISO 26262 durch. Diese Standards kommen oft in Kombinationen in Embedded-intensiven Industrien wie Automotive, Aerospace, Medizingeräte und Industriesteuerungen zum Einsatz. Unternehmen in diesen Märkten müssen gerüstet sein, um nicht nur Verstöße der vordergründigen syntaktischen Regeln, sondern auch aus undefiniertem Verhalten entstandene ernste Bedrohungen zu identifizieren, wie es der Standard MISRA C:2012 vorschreibt. Während sich einige dieser Ereignisse durch Testläufe spezifizieren lassen, spüren nur die fortschrittlichsten statischen Analysewerkzeuge die subtileren Ereignisse auf. Das Codebeispiel in Bild 3 enthält eine vereinfachte Version des Codes mit dem »infinite Loop«-Bug, der in Microsofts »Zune Player« gefunden wurde. Am letzten Tag eines Schaltjahres wäre der Wert von »days« genau 366, und die Schleife würde nicht beendet.

Bild 4 zeigt einen »Type Mismatch«: Die Variable »tenths« ist als vorzeichenbehaftete Integerzahl angegeben, aber der arithmetische Ausdruck führt zu einem vorzeichenlosen Wert. Solche Widersprüche sind durch den MISRA-Standard verboten, weil sie zu einem stillen Abschneiden (Truncation) und zu stillen Überläufen führen können, die unter Umständen unerwartetes Verhalten zur Folge haben. 

  • Integrierte Sicherheit: 

Der Trend zur Vernetzung vergrößert die potenziellen Angriffsflächen für Hacker. Diese Attacken werden typischerweise ausgelöst, wenn ein unbefugter Anwender Daten über einen Eingangskanal (z.B. Netzwerkport) sendet. Programmierer können sich gegen diese Schwachstellen wehren, indem sie Eingangsdaten als potenziell gefährlich (tainted) bewerten und sie validieren, bevor die Anwendung damit arbeiten darf.

Weil der Datenfluss über die gesamte Anwendung manuell nachzuverfolgen ist, sind diese gefährlichen Angriffe schwer zu lokalisieren. Ein automatisiertes Analysetool, das die Daten nach potenziell gefährlichem Input untersucht, verkürzt die dafür aufzuwendende Zeit und steigert die Effektivität. Sicherzustellen, dass importierte Daten nicht gefährlich sind, verringert letztendlich das Risiko, dass kompromittierte Software Endkunden erreicht und der Hersteller dafür haften muss. Das Beispiel in Bild 5 zeigt das Format »String Injection Vulnerability« in »wu-ftpd«. Diese Schwachstelle ermöglicht einem entfernten Angreifer, beliebigen Code auf dem Server auszuführen, indem ein Exploit-String auf den »SITE EXEC«-Befehl des FTP-Protokolls übergeben wird. Die rote Unterstreichung zeigt, dass der zugeordnete Wert gefährlich ist. 

  • Concurrency Checking: 

Das Programmieren von Multithreaded-Anwendungen ist äußerst komplex und kann aufgrund der geteilten Daten zu schwer auffindbaren Fehlern führen. Der Trend zu Multicore-Design verlangt Multithreaded-Software, um die Vorteile derartiger Hardware auszuschöpfen.

Moderne statische Analyselösungen sprechen Concurrency-Probleme in C- und C++-Programmen an, aber speziell für Java gibt es bislang keine umfassende Lösung. Dabei wird Java als drittpopulärste Sprache für eingebettete Systeme heute von 28 Prozent der Entwickler genutzt. Programmierer, die ihren Code nicht erfolgreich vor Fehlern wie Race-Conditions und Deadlocks in C/C++ und Java schützen, werden unweigerlich mit Produktausfällen konfrontiert. Als Beispiel zeigt der Code in Bild 6 eine Unstimmigkeit bei den Methoden, wie eine Klasse ein Feld anspricht. Bei manchen Klassen sind die Zugänge synchronisiert, bei anderen nicht. Diese Fehlerart ist einfach zu übersehen, kann aber zu mysteriösen Symptomen führen, die schwer zu reproduzieren und festzustellen sind. 

  • Tiefgreifende Analyse: 

C und C++, die gängigsten Sprachen für Embedded-Anwendungen, sind aufgrund eigener Unzulänglichkeiten sehr anfällig für gefährliche Fehler. Trifft diese Eigenschaft auf eine steigende Vielfalt an Kombinationen aus Ziel-Hardware/-Firmware, kann das zu unberechenbarem Verhalten führen. Der Schlüssel für die Fehlerbehebung im Code ist die Auswahl eines Tools, das tief in die Codebasis eindringt und zugleich eine niedrige False-Positive-Rate erzielt.

Werkzeuge, die mithilfe eines einheitlichen Datenflusses und der symbolischen Ausführungsanalyse ein gesamtes Programm untersuchen, finden mehr potenzielle Fehler und Schwachstellen. Dadurch wird im Allgemeinen qualitativ höherwertigere Software ausgeliefert. Zudem sind Teams mit einem Tool mit modernen Darstellungsmöglichkeiten für Fehler wie zum Beispiel Visualisierung besser gerüstet, um das genaue Codeverhalten in ihren Embedded-Anwendungen nachzuvollziehen.

Das Codebeispiel in Bild 7 zeigt einen in Feldern aufgeteilten SSL/TLS-Fehler. In diesem Beispiel warnt das statische Analysewerkzeug das Entwicklungsteam, dass der unterlegte Coding-Abschnitt niemals erreicht werden kann. Der Fehler ist: Das »goto« in Zeile 35 ist bedingungslos, dadurch wird das Statement auf Zeile 36 immer übersprungen. Diese Art von Fehler schleicht sich leicht aufgrund eines schlechten Cut-and-Paste oder eines Versehens beim Auflösen eines Konflikts bei der Versionskontrolle ein.

Über den Autor:

Jim Shissler ist Pressesprecher von GrammaTech.