Software-Qualitätssicherung nach ISO 26262 Zweigüberdeckung auch bei optimiertem Code ermitteln

Die Qualitätssicherung von Software in elektronischen Steuergeräten nach der ISO-Norm 26262 ist in der Praxis oft ziemlich schwierig. Vor allem bei echtzeitkritischen Multicore-Systemen stoßen kontrollflussorientierte Verfahren auf Basis von Instrumentierungscode an ihre Grenzen. Eine neue nichtinvasive Methode auf Basis einer Compiler-Erweiterung von Hightec und der Debug- und Testumgebung »Universal Debug Engine« von pls ermöglicht jetzt erstmals die Ermittlung der Zweigüberdeckung auch bei optimiertem Code.

Mit der in den letzten Jahren geradezu explodierten Komplexität der Steuerungssoftware in sicherheitsrelevanten Systemen sind Entwickler heute mehr denn je auf zuverlässige Methoden für das Überprüfen und Beurteilen der funktionalen Zuverlässigkeit des manuell erstellten Quellecodes angewiesen. Im Fokus stehen dabei aktuell vor allem kontrollflussorientierte Testverfahren. Ihr großer Vorteil ist die weitgehend automatisierbare Anwendung mit speziell erzeugten künstlichen Stimuli oder parallel zu funktionalen Tests. Dadurch werden diese Testverfahren sehr häufig als Teil von normierten Prozessen zur Qualitätssicherung von Erzeugnissen mit softwarebasierten Steuerungssystemen empfohlen bzw. gefordert (IEC 61508, ISO 26262).

Kontrollflussorientierte Verfahren stellen meist sicher, dass in den auf Hochsprachenbasis beschriebenen Steuerungsabläufen alle möglichen Verzweigungen im Kontrollfluss erreicht werden – mittels spezieller
oder durch funktionale Tests des Systems in dessen Einsatzumgebung erzeugter Stimuli. Die Korrektheit des Systemverhaltens an sich können diese Testverfahren allerdings nicht testen. Sie liefern nur Informationen, ob die Struktur des erzeugten Codes umfänglich den dafür notwendigen Funktionsumfang überdeckt. Erst die Kombination mit funktionalen Tests stellt sicher, dass keine durch Programmierfehler erzeugten redundanten Funktionen im Code vorhanden sind, die bei geänderter Stimulierung des Systems zu Fehlverhalten führen könnten. Letztendlich lässt sich mit dieser Methode aber durchaus eine qualitative Aussage bezüglich der Umsetzung der Spezifikation in den erzeugten Hochsprachencode treffen. Wenn kontrollflussorientierte Testverfahren direkt zur Messung auf Maschinencode-Ebene zum Einsatz kommen, lässt sich damit auch die Fehlerfreiheit der Umsetzung in Maschinencode überprüfen.

Die verschiedenen Abstraktionsebenen der kontrollflussorientierten Testverfahren hinsichtlich der Abdeckung des Kontrollflusses lassen sich grob in folgende Kategorien unterteilen:

  • Abdeckung auf Funktionsebene, 
  • Abdeckung auf Quelltextzeilen-Ebene, 
  • Abdeckung auf Anweisungs-/Befehlsebene, 
  • Abdeckung der Zweige – Kantenüberdeckung, 
  • Abdeckung der Pfade eines Moduls und 
  • Abdeckung der Einzelbedingungen und Entscheidungsterme. 

Zur genauen Definition der jeweiligen Kategorien gibt es von mehreren Autoren ähnliche, aber nicht vollständig gleiche Definitionen. Ausgangsbasis für nachfolgende Ausführungen ist die Einordnung nach der Norm DO-178B.

Überdeckung messen

Bei der Einordnung dieser Kategorien wird immer von der Messung der Überdeckung auf Quelltextebene ausgegangen. Typische Verfahren nutzen hierfür zusätzlich in den Testcode eingebauten Instrumentierungscode, mit dessen Hilfe die entsprechenden Messdaten ermittelt werden. Die Messung selbst kann dann auf Basis einer statischen Codesimulation oder durch Übersetzung des aus Test- und Instrumentierungscodes erzeugten Gesamtcodes in Maschinencode und anschließendem Testlauf zur Ermittlung der Messwerte erfolgen. Dieses Verfahren hat aber mehrere schwerwiegende Nachteile:

  • Durch die Instrumentierung, insbesondere den Einbau von Messsonden, ändert sich das Laufzeitverhalten des Systems, 
  • der für die Instrumentierung notwendige Zusatzcode vergrößert im Testfall auch die Gesamtgröße der zu testenden Applikation und 
  • eine Instrumentierung ist nur in nicht optimiertem Code sinnvoll anwendbar. 

Für echtzeitkritische Steuerungssysteme ist dieses Verfahren also nur sehr bedingt anwendbar, da für die parallele Ausführung von Überdeckungs- und Funktionstests unverändertes Zeitverhalten wie auch Speicherlayout zwingend erforderlich sind. Für den Test auf solchen echtzeitkritischen Systemen scheinen Verfahren wesentlich besser geeignet, welche die zur Überdeckungsmessung benötigten Daten nichtinvasiv, also ohne den Einbau von zusätzlichem Code ermitteln – allerdings müssen dann zur Gewinnung der benötigten Messdaten andere Verfahren zur Anwendung kommen. So lässt sich die Kontrollflussinformation beispielsweise als Bestandteil der Debug-Information zusätzlich vom Compiler bereitstellen und bei der Auswertung der Trace-Informationen zur Ermittlung der Überdeckung heranziehen.

Code-Optimierung verzerrt Maschinencode

Die Code-Optimierung kann die Struktur des Maschinencodes derart verändern, dass sich die Basisblöcke der Quellcode-Ebene danach nicht mehr eindeutig auf Basisblöcke der Maschinencode-Ebene abbilden lassen. So werden bei der Optimierung Basisblöcke zum Beispiel vervielfältigt, zusammengefasst oder eliminiert, unter anderem bei Schleifentransformationen, -spaltungen, -vereinigungen oder -expansionen, »If-then-else«-Transformationen (z.B. Eliminieren eines Anweisungszweigs) oder Umwandlungen von Basisblöcken in Tabellenzugriffe.

Beim Test lässt sich die Programmstruktur – also die Basisblöcke, deren Programmadresse und -größe sowie ihre Verknüpfung – statisch und dynamisch ermitteln. Dabei kann das Testtool auch die Debug-Informationen aus der Programmdatei nutzen, die zum Beispiel im »DWARF«-Format vorliegen und dann unter anderem die Zuordnungen von Zeilennummern zu Programmadressen und die Programmadressen der Basisblöcke enthalten – nicht jedoch die Größe der Basisblöcke und deren Verknüpfungen. Letztere lassen sich zwar durch Disassemblieren der Maschinencode-Anweisungen und anschließende Interpretation, Emulation oder Simulation ermitteln. Diese statische Code-Analyse gestaltet sich aber sehr aufwendig und wird daher nur selten durchgeführt.

Die meist dynamischen Trace-Informationen enthalten die Adressen der tatsächlich ausgeführten Anweisungen und die Reihenfolge der Ausführung. Aus diesen Informationen lässt sich eine Sicht auf mögliche Basisblöcke erzeugen. Weil jedoch nur die tatsächlich ausgeführten Blöcke erkennbar sind, ist keine Aussage über nicht ausgeführte Basisblöcke möglich – und damit auch keine verlässliche Überdeckungsanalyse.

Die Debug-Informationen im Programm reichen normalerweise dafür aus, die Überdeckung der Quellcodezeilen (C0-Coverage) zu ermitteln. Für die Entscheidungsüberdeckung (C1-Coverage) aus Trace-Daten sind jedoch zusätzlich die Verknüpfungen zwischen den Basisblöcken nötig, die in den DWARF-Informationen jedoch wie schon beschrieben nicht vorhanden ist. Aus dem Anweisungs-Trace ist zwar abzuleiten, welche Programmverzweigung genommen wurde, und bei direkten Sprüngen lässt sich zudem mit einigem Aufwand ermitteln, welche Verzweigungen nicht genommen wurden. Spätestens bei indirekten Sprüngen mit nahezu beliebig vielen Sprungzielen ist de facto jedoch nicht mehr rekonstruierbar, wie viele Verzweigungen tatsächlich existieren und wohin sie führen.

Debug-Informationen anreichern

Um die in solchen Fällen sonst übliche, gleichermaßen aufwendige wie fehleranfällige statische Code-Analyse zu vermeiden, bietet es sich an, die Basisblock-Informationen zu nutzen, die dem Compiler nach allen Programmtransformationen zur Verfügung stehen. Dazu zählen unter anderem Basisblöcke nach Funktion, Identifikationsnummer, Adresse und Größe sowie die Nachfolger eines Basisblocks und die Anzahl der Basisblöcke. Diese Informationen reichen aus, um einen Graphen der Basisblöcke zu erstellen und aus den Verknüpfungen zwischen Basisblöcken und Anweisungs-Trace eine Entscheidungsüberdeckung herauszufiltern. Bei der Ausgabe des Assemblercodes in einer Debug-Section werden diese Debug-Informationen dann gespeichert und vom Debug-Tool zur Coverage-Ermittlung ausgewertet. Da die Debug-Section zur Laufzeit nicht geladen wird, beeinflussen die zusätzlichen Informationen die Codegenerierung, den Speicherverbrauch und das Laufzeitverhalten nicht.

Mit diesen zusätzlichen Kontrollflussinformationen und den Tracedaten sind nichtinvasive Überdeckungsmessungen möglich. Je nach Architektur der Zielhardware stehen dabei verschiedene Trace-Schnittstellen für das Aufzeichnen der Programmausführung zur Verfügung. Prinzipiell sind für die Berechnung der Zweigüberdeckung (Branch Coverage) Code-Tracedaten ausreichend, welche die Aufzeichnung der Programmausführung in die Eintrittspunkte eines Basisblockes enthalten. Meist berechnen die Tools die Überdeckung mit Daten aus kontinuierlichem Programm-Trace. Tabelle 1 zeigt, welche Durchführungsmethode die Norm ISO 26262 für Überdeckungsmessungen empfiehlt.

Testabdeckungsstufe ASIL A ASIL B ASIL C ASIL D 
Statement Coverange (C0)++ ++ +
Branch Coverage (C1)+++++ ++ 
Tabelle 1: Von der ISO 26262 empfohlene Methoden für Überdeckungsmessungen (++ »sehr zu empfehlen (highly recommended«, + »zu empfehlen (recommended)«

Da alle möglichen Trace-Schnittstellen kontinuierlich Programm-Trace aufzeichnen können, sind sowohl nichtinvasive C0- als auch C1-Abdeckungsmessungen entsprechend der ISO-26262-Anforderungen möglich. Für eine nichtinvasive Überdeckungsmessung mit der Testabdeckungsstufe C3 ist ein zusätzlicher erweiterter Daten-Trace erforderlich, an dessen Vollständigkeit hohe Anforderungen geknüpft sind. Diese Abdeckungsstufe in Form der auf Hardware-Trace-Daten basierenden nichtinvasiven Abdeckungsmessung zu realisieren, erscheint derzeit praktisch nicht umsetzbar, C3 bleibt damit vorerst Analysetools vorbehalten, die auf Instrumentierung basieren. Eine mögliche Schwachstelle des beschriebenen nichtinvasiven Verfahrens ist die Hardwareschnittstelle für das Aufzeichnen der Tracedaten. Um die Anforderungen an diese Schnittstelle vorab definieren zu können, ist die für den Nachweis der maximal erreichbaren Abdeckung notwendige Tracedatenmenge möglichst genau abzuschätzen. Zur Stimulierung der Messanordnung sind hierbei zwei prinzipielle Varianten möglich.

Testergebnisse aufbereiten

Die erste Variante ist eine Testanordnung, die der normalen Prozessumgebung in der Zielapplikation des Steuergeräts entspricht. Um die maximale Abdeckung nachweisen zu können, ist es erforderlich, auch die Extremzustände der Software zu erreichen. Deshalb wird die Messanordnung sowohl durch die normalen wie auch extremen Betriebszustände des Gesamtsystems stimuliert. Sinnvoll ist hierfür typischerweise eine Testanordnung mit externem Trace-Speicher im Gigabyte-Bereich. Und die Trace-Schnittstelle der Wahl ist hier eine serielle Hochgeschwindigkeitsschnittstelle auf Basis des Aurora-Interface, die einen hohen Durchsatz bei gleichzeitig zuverlässiger 
Kopplung des Zielsystems ermöglicht.

Bild 1 zeigt eine entsprechende Anordnung auf Basis des »Universal Access Device 3+« von PLS mit Aurora-Tracepod und 4 GByte externem Trace-Speicher.

Alternativ kann der Tester in Betracht ziehen, die Testanordnung mittels spezieller Software zu stimulieren. Mit Softwarelösungen wie TPT (Time Partition Testing) von Piketec beispielsweise sind einzelne Modelle modellbasiert stimulierbar, wodurch sich die erforderliche Tracedatenmenge zum Nachweis der maximalen Überdeckung dramatisch reduzieren lässt. Für das Aufzeichnen reicht dann der On-Chip-Trace-Speicher moderner On-Chip-Trace-Hardware aus. Hilfreich kann auch ein anderes Feature dieser Funktionseinheit sein, das mittels Echtzeitfilter die Programmausführung nur in bestimmten Adressbereichen aufzeichnet. Die Überdeckung einzelner Softwaremodule lässt sich wahlweise auch mit Simulator-basierten Messungen ermitteln.

Wichtiger Bestandteil einer kompletten Toolumgebung zum Nachweis der Programmabdeckung ist die effektive Visualisierung der Messergebnisse (Bild 2).

Um die geforderten Nachweise der durchgeführten Abdeckungsmessungen innerhalb des Gesamtprozesses zur Software-Qualitätssicherung zu erbringen, sind außerdem komplette Berichte inklusive aller Details zu erstellen, die für die spätere Nachvollziehbarkeit der durchgeführten Messungen und deren Interpretation relevant sein könnten (Bild 3).

In Summe weist der nichtinvasive, auf der Messung des Maschinencodes basierende Überdeckungstest folgende Vorteile auf: Die Methode ist bei elektronischen Steuergeräten uneingeschränkt auch direkt in der Zielarchitektur einsetzbar. Zudem bildet die Messung alle 
Effekte ab, die in der späteren Prozessumgebung gegebenenfalls auftreten können. Damit liefert sie Ergebnisse, die im Hinblick auf mögliche zusätzliche Effekte zum Beispiel durch Compiler und Hardware einen höheren Überdeckungsgrad erzielen als die rein auf die Struktur des Original-Hochsprachencodes ausgerichteten Überdeckungstests mittels Code-Instrumentierung. Leider hat dieser Test aber auch einige Nachteile: Die gemessenen Resultate weichen gegebenenfalls von den Erwartungswerten ab, die aus Hochsprachensicht zu erwarten sind. Außerdem sind mit dieser Methode derzeit keine Überdeckungstests mit einer Testabdeckungsstufe größer C1 realisierbar.

Eine neue Methode, die Zweigüberdeckung auch bei optimiertem Code zu ermitteln, liefert hier deutlich verbesserte Ergebnisse. Sie basiert auf zusätzlich durch den Compiler generierten Debug-Informationen über den Kontrollfluss, die mit architekturspezifischen Verfahren für das Beobachten und Aufzeichnen der Programmausführung direkt auf dem Steuergerät kombiniert werden. Eine erste praktische Umsetzung der auch in Simulationslösungen anwendbaren Methode wurde mit einer Compiler-Erweiterung der Firma Hightec und der Debug- und Testumgebung »Universal Debug Engine« der Firma PLS bereits realisiert.

Trotz dieser Fortschritte ist und bleibt die Qualitätssicherung von Software in elektronischen Steuergeräten nach ISO-Norm 26262 in der Praxis aber ein ziemlich schwieriges Unterfangen.

Über die Autoren:

Dr. Stefan Weisse ist Mitbegründer und Entwicklungsleiter von PLS und Andreas Gajda ist dort Software-Architekt.