Programmoptimierung 100% strukturelle Codeabdeckung von sicherheitskritischen Systemen

Autor / Redakteur: Ricardo Camacho * / Michael Eckstein

Sicherheitskritische Systeme wie ADAS befördern Passagiere ohne Fahrer, ermöglichen es Autopiloten, Menschen über unseren Himmel zu fliegen, und halten Patienten mit medizinischen Geräten am Leben. Das Leben von Menschen hängt von diesen Systemen ab – daher ist eine strukturelle Codeabdeckung unerlässlich.

Firmen zum Thema

Bild 5: Beispiel Parasoft DTP Dashboard Code Coverage Reporting.
Bild 5: Beispiel Parasoft DTP Dashboard Code Coverage Reporting.
(Bild: Parasoft)

Dabei handelt es sich um die Identifizierung von Code, der ausgeführt und protokolliert wurde, um festzustellen, ob das System angemessen getestet worden ist. Die Genauigkeit der Abdeckung in sicherheitskritischen Systemen hängt von der Sicherheitsintegritätsstufe (SIL), ASIL in der Automobilindustrie, und der Entwicklungssicherheitsstufe (DAL) in der Avionik ab. Wenn ich von Genauigkeit spreche, beziehe ich mich auf die strukturellen Elemente im Code. In Embedded-Systemen werden diese in der Regel auf die Code-Anweisung, die Verzweigung und die geänderten Zustandsentscheidungen heruntergebrochen, und man kann auch bis zu einer viel feineren Granularität vordringen, etwa den Objektcode oder die Assemblersprache.

Auch wenn Sie vielleicht von anderen Arten von Abdeckungsmetriken wie Funktion, Aufruf, Schleife, Bedingung, Sprung, Entscheidung usw. hören oder lesen - für sicherheitskritische Embedded-Systeme muss man aktuell nur über Anweisung, Verzweigung, MC/DC und Objektcode Bescheid wissen. Die anderen erwähnten Typen sind eine Teilmenge und werden entsprechend behandelt.

Bildergalerie
Bildergalerie mit 5 Bildern

Derzeit gibt es eine Tendenz zur Einführung der Multiple Condition Coverage (MCC), die genauer ist als MC/DC (Modifizierte Bedingungsabdeckung (Modified Condition Decision Coverage). MCC erfordert eine viel größere Anzahl von Testfällen, nämlich zwei hoch die Anzahl der Bedingungen. Da die Industriestandards ISO 26262, DO-178, IEC 62304, IEC 61508 oder EN 50128 offiziell nicht MCC empfehlen oder vorschreiben, gehe ich in diesem Beitrag nicht darauf ein.

Anweisungs- und Verzweigungs-Abdeckung

Die Anweisungsabdeckung ist das einfachste Unterfangen und stellt jede Codezeile in einem Programm dar. Allerdings können Codeanweisungen unterschiedliche Komplexitätsgrade aufweisen. Eine Verzweigungsanweisung stellt beispielsweise eine "if then else"-Bedingung im Code dar. Anweisungen wie case oder switch werden als Verzweigung interpretiert. Um eine Verzweigung abdecken zu können, muss die Ausführung sowohl des wahren als auch des falschen Entscheidungspfads abgedeckt werden.

Bei höheren Sicherheitsanforderungen ist unter Umständen eine modifizierte Bedingungsüberdeckung (Modified Condition / Decision Coverage, MC/DC) erforderlich. Gibt es mehrere Bedingungen in einer Entscheidung, und jede Bedingung muss unabhängig getestet werden, kann die Komplexität von Verzweigungen zunehmen. Das bedeutet für Abdeckungskriterien, dass jede Bedingung in der Entscheidung nachweislich unabhängig das Ergebnis der Entscheidung beeinflusst.

Für diese Analyse kann eine Wahrheitstabelle verwendet werden (siehe unten). Außerdem hat jede Bedingung in einer Entscheidung im Programm alle möglichen Ergebnisse mindestens einmal berücksichtigt, und jede Entscheidung im Programm alle möglichen Ergebnisse mindestens einmal einbezogen.

Im folgenden Beispiel mit vier Bedingungsanweisungen gibt es 16 mögliche Testfälle. MC/DC benötigt für dieses Beispiel nur fünf. Nehmen Sie die Anzahl der Bedingungen und addieren Sie 1.

Objektcode-Abdeckung

Für die strengsten sicherheitskritischen Anwendungen, wie etwa in der Avionik, schreibt der Prozessstandard DO-178B/C Level A die Objektcode-Abdeckung vor. Dies ist darauf zurückzuführen, dass ein Compiler oder Linker zusätzlichen Code generiert, der nicht direkt auf Quellcode-Anweisungen zurückzuführen ist.

Das macht die Durchführung einer Abdeckung auf Baugruppenebene zwingend notwendig. Neben rigoroser Strenge sind mit dieser Aufgabe hohe Arbeitskosten verbunden. Abhilfe bieten Lösungen, die hier ansetzen wie Parasoft ASMTools, eine automatisierte Lösung zum Erlangen der Objektcode-Abdeckung.

In den meisten Fällen ermittelt man die Codeabdeckung durch Instrumentieren des Codes: Dazu versieht man den Anwendercode mit zusätzlichem Code, um während der Ausführung festzustellen, ob die betreffende Anweisung, Verzweigung oder MC/CD ausgeführt wurde.

Abhängig vom embedded Ziel oder Gerät ist es möglich, die Abdeckungsdaten im Dateisystem zu speichern, in den Speicher zu schreiben oder über verschiedene Kommunikationskanäle wie die serielle Schnittstelle, den TCP/IP-Port, USB und sogar JTAG zu senden.

Partielle Instrumentierung

Zu beachten ist, dass die Code-Instrumentierung den Code aufbläht, und dass diese Vergrößerung des Codes unter Umständen die Fähigkeit beeinträchtigt, den Code zum Testen auf die Zielhardware mit begrenztem Speicher zu laden. Umgehen lässt sich dies, indem man einen Teil des Codes instrumentiert:

  • 1. Führen Sie Ihre Tests aus und erfassen Sie die Abdeckung.
  • 2. Instrumentieren Sie den anderen Teil des Codes.
  • 3. Führen Sie Ihre Tests erneut aus.
  • 4. Erfassen Sie die Abdeckung.
  • 5. Führen Sie die Abdeckung aus der vorherigen Testausführung zusammen.

Je nach Zielvorgaben sind hoffentlich nicht zu viele instrumentierte Partitionen zu durchlaufen, denn es kann sehr zeit- und kostenaufwendig sein, dieselben Tests immer wieder zu wiederholen. Es sei erwähnt, dass die Instrumentierung auch negative Auswirkungen auf das Timing und die Leistung haben kann.

Code-Abdeckung für embedded sicherheitskritische Systeme

Für Anforderungen an die Codeabdeckung, wie eine vorgeschriebene 100%ige Anweisungs-, Verzweigungs- und MC/DC-Abdeckung oder eine optionale und persönlich gewünschte 80%ige Abdeckung, gibt es verschiedene Testmethoden, um ans Ziele zu kommen. Die gängigsten Methoden sind: Systemtests, Unit-Tests und manuelles Testen. Die Kombination der Abdeckungsmetriken aus diesen verschiedenen Verfahren ist üblich. Aber wie genau wird die Codeabdeckung ermittelt?

Die Ermittlung der Codeabdeckung durch Systemtests ist eine ausgezeichnete Methode, um festzustellen, ob ausreichend getestet wurde. Der Ansatz besteht darin, alle Systemtests anzuwenden und dann zu untersuchen, welche Teile des Codes nicht ausgeführt wurden. Nicht ausgeführter Code deutet darauf hin, dass möglicherweise neue Testfälle erforderlich sind, um den unberührten Code zu testen, in dem womöglich ein Fehler lauert, und hilft bei der Beantwortung der Frage, ob man genug getestet hat.

Wenn ich während der Systemtests eine Codeabdeckung durchgeführt habe, lag die durchschnittliche Abdeckung bei 60%. Ein Großteil der 40% des nicht ausgeführten Codes ist auf defensiven Code in der Anwendung zurückzuführen. Defensiv bedeutet Code, der nur dann ausgeführt wird, wenn das System in einen Fehler oder einen problematischen Zustand gerät, der schwierig zu erzeugen sein könnte. Bis ein Zustand wie ein Speicherleck, eine Verfälschung oder ein anderer Fehler auftritt, den ein Hardwareausfall verursacht hat, können Wochen, Monate oder Jahre vergehen.

Es gibt auch defensiven Code, der in den eigenen Programmierrichtlinien vorgeschrieben ist und den Systemtests niemals ausführen können. Aus diesen Gründen kann man mit Systemtests keine 100%ige strukturelle Codeabdeckung erreichen, sondern dafür muss man auf andere Testmethoden wie manuelle und/oder Unit-Tests zurückgreifen.

Zu beachten ist, dass Prozessstandards das Zusammenführen von Abdeckungsmetriken erlauben, die aus verschiedenen Testmethoden gewonnen wurden.

Abdeckung aus Unit-Tests

Wie erwähnt, können Unit-Tests einen ergänzenden Ansatz zu Systemtests für eine 100%ige Abdeckung bilden. Auch wenn die Codeabdeckung durch Unit-Tests eine der populärsten Methoden ist, liefert sie keinen Aufschluss darüber, ob das System ausreichend getestet wurde, da der Schwerpunkt auf der Unit-Ebene (Funktion/Prozedur) liegt.

Das Ziel ist hier, eine Reihe von Unit-Testfällen zu generieren, die die gesamte Unit mit dem erforderlichen Abdeckungsgrad testen (Anweisung, Verzweigung und MC/DC), um eine 100%ige Abdeckung für diese einzelne Unit zu erhalten. Dies wird für jede Einheit wiederholt, bis die gesamte Codebasis abgedeckt ist. Um das Beste aus den Unit-Tests herauszuholen, sollte man sich jedoch nicht nur auf die Codeabdeckung konzentrieren, was sich generell durch 'Sunny-Day'-Szenarien als Testfälle realisieren lässt. Sondern es macht Sinn, die Unit auch in ‚Rainy Day‘ Szenarien zu testen, um Robustheit, Sicherheit und Rückverfolgbarkeit der Anforderungen auf niedriger Ebene sicherzustellen. Lassen Sie die Codeabdeckung ein Nebenprodukt Ihrer Testfälle sein und ergänzen Sie die Abdeckung, wo erforderlich.

Um die Codeabdeckung durch Unit-Tests zu beschleunigen, bietet Parasoft C/C++test konfigurierbare und automatisierte Funktionen zur Testfallgenerierung. Damit können Testfälle automatisch generiert werden, um die Verwendung von Null-Zeigern, Min-Mid-Max-Bereichen, Grenzwerten und vielem mehr zu testen. Diese Automatisierung ist sehr hilfreich, schließlich liefert sie in wenigen Minuten einen beachtlichen Umfang an Codeabdeckung. Wie bei Systemtests ist es jedoch schwer, durch den Einsatz von Defensivcode oder formaler Sprachsemantik eine 100%ige Codeabdeckung zu erreichen. Auf der granularen Ebene einer Einheit kann defensiver Code in Form einer Default-Anweisung in einem Switch (Parameter) vorkommen. Wenn jeder mögliche Fall in einem Switch erfasst wird, bleibt die Default-Anweisung unerreichbar. Im folgenden Beispiel wird der Return 0 (Rückgabewert 0); niemals ausgeführt, da die while-Anweisung (1) unendlich ist.

Wie also erhält man also eine 100%ige Abdeckung für diese speziellen Fälle? Dazu ist es notwendig, manuelle Methoden einzusetzen. Der Nutzer kann die Anweisung als abgedeckt markieren oder notieren, indem er einen Debugger verwendet, den Aufrufstapel ändert und die return 0; Anweisung ausführt. Beobachten Sie die Ausführung visuell und dokumentieren Sie zumindest den Dateinamen, die Codezeile und die Codeanweisung, die nun als abgedeckt gilt. Diese durch manuelle/visuelle Inspektion und Berichte durchgeführte Abdeckung lässt sich zur Ergänzung der durch Unit-Tests erfassten Abdeckung verwenden. Die Addition beider Abdeckungsberichte ist zum Nachweis einer 100%igen strukturellen Codeabdeckung anwendbar.

Umfassende Code-Abdeckung: Gesamterfassung über alle Testverfahren hinweg

Hat eine Systemtestabdeckung stattgefunden, die einbezogen werden soll, können alle drei Abdeckungsberichte (System, Unit, Manual) verwendet werden, um eine 100%ige Abdeckung und Übereinstimmung zu zeigen und nachzuweisen.

Die strukturelle Codeabdeckung kann zur Beantwortung dieser Frage beitragen: Habe ich genügend Tests durchgeführt? Es kann sich auch um eine Konformitätsanforderung handeln, die zu erfüllen ist. Das Ziel, eine Codeabdeckung zu erlangen ist ein zusätzliches Mittel, um die Sicherheit und Zuverlässigkeit des Codes zu gewährleisten. Sie ist der Beweis dafür, dass Tests vorgenommen wurden, die Fehler identifiziert haben. Ein häufiger Fehler, den die Codeabdeckung leicht identifizieren kann, und der in den früheren Abschnitten nicht erwähnt wurde, ist die Aufdeckung von totem Code. Das ist Code, der in keiner Weise aufgerufen oder herangezogen wird; wahrscheinlich wurde er aufgrund geänderter Anforderungen oder versehentlich vergessen.

Die Abdeckung kann auch durch verschiedene Testmethoden (System-, Unit-, Integrations-, manuelle und API-Tests) erzielt werden. Durch die kumulative Abdeckung dieser Methoden lässt sich eine 100%ige Codeabdeckung nachweisen. Basieren die Kriterien auf dem SIL-, ASIL- oder DAL-Level, muss man möglicherweise verschiedene Abdeckungsebenen (Anweisung, Verzweigung, MC/DC und Objektcode) durchführen. Hier bieten die automatisierten Software-Testlösungen und Methoden von professionellen Anbietern wertvolle Unterstützung bei der 100%igen strukturellen Codeabdeckung.

* Ricardo Camacho ist Technical Editor bei Parasoft.

(ID:47729959)