Simulation von Fehlersituationen: Fault Injection einfach automatisieren

Autor / Redakteur: Thomas Dirsch * / Sebastian Gerstl

Software für sicherheitskritische Anwendungen erfordert 100%-ige Anweisungsüberdeckung durch dokumentierte Tests. „Fault Injection“ erlaubt hier eine einfache Prüfung auf mögliche Fehlersituationen.

Anbieter zum Thema

Bild 1: 
TESSY Code Coverage-Perspektive.
Bild 1: 
TESSY Code Coverage-Perspektive.
(Bild: Razorcat)

Sicherheitskritischer Code ist in der Regel geprägt durch Programmiertechniken wie Diversität, Defensivität, Selbsttests und ständige Überprüfungen. Das ist auch gut so, denn das System soll sicher sein – doch stellt dies insbesondere den Testingenieur vor Herausforderungen, die auch Rückwirkungen in die Entwicklung haben. Normen wie die ISO 26262, EN/IEC 61508, EN 50128 oder IEC 62304 verlangen Sicherheitsnachweise wie beispielsweise eine 100% Anweisungsüberdeckung (Statement Coverage) durch einen dokumentierten Test.

Die Tests der Funktionalität, basierend auf den Anforderungen, lassen sich zumeist einfach mit etablierten Testwerkzeugen durchführen. Das Dilemma ist, dass dabei die Anweisungsüberdeckung nicht immer zu 100% erfüllt werden kann. In manchen Situationen kann sie das auch nicht sein, da im Funktionstest nicht alle Fehler auftreten und dadurch die Sicherheitsvorkehrungen in der Software nicht vollständig abgearbeitet werden. Darum muss der Testingenieur Fehlersituationen künstlich herbeiführen.

Bildergalerie

Ziel: 100% Anweisungsüberdeckung

In einem System- oder HiL-Test ist es einfach, Fehlersituationen zu erzeugen, da äußere Interfaces durch Stimulation des Testsystems gesteuert und dadurch Fehlersituationen reproduzierbar getestet werden können. Dies erhöht zwar die Anweisungsüberdeckung, aber erfüllt diese immer noch nicht zu 100%, da hier die internen Sicherheitsvorkehrungen in der Software, wie zum Beispiel die Diversität, nicht beeinflusst werden kann. Eine weitere Schwierigkeit stellt die eingesetzte Technik für die Messung der Anweisungsüberdeckung dar. Die Messungen erfolgen durch eine Instrumentierung des Quellcodes, was das dynamische Verhalten des Systems beeinflussen kann.

In der Praxis wird die Anweisungsüberdeckung hauptsächlich in der Testphase „Unit Test“ ermittelt, da beim Test einzelner Funktionen auch die internen Sicherheitsvorkehrungen in der Software getestet werden können. Durch Anwenden von normgerechten Messmethoden wird bereits ein hohes Maß an Testqualität und damit eine hohe Abdeckungsrate der getesteten Funktion erreicht. In einem hoch-sicherheitskritischen Code werden allerdings immer einige Prozentwerte zur vollständigen Anweisungsüberdeckung fehlen. Die Sicherheitsfunktionen wie Diversität und Anwendung von defensiven Programmiertechniken können nur vollständig getestet werden, wenn die entsprechende Fehlersituation hergestellt wird. Eine weitere Schwierigkeit ist, dass bei schweren Fehlersituationen der sichere Zustand des Systems dadurch erreicht wird, dass das Programm in eine Endlosschleife läuft und darauf gewartet wird, dass der Watchdog anschlägt und das System in einen sicheren Zustand versetzt.

Wird der Test auf dem Mikrocontroller ausgeführt, so kann die Hardware nicht in jeden notwendigen „defekten“ Zustand versetzt werden, da die Register der Peripherieeinheiten nur entsprechend ihrer Hardwareimplementierung eingestellt werden können. Als Folge werden nicht alle Fehlerzustände in der Software erreicht. Bei der Verwendung eines Simulators wird je nach Leistungsumfang des Simulators die Peripherie ebenso simuliert und man steht vor der gleichen Schwierigkeit, wie bei einem Test mit dem Mikrocontroller. Die Simulation der Peripherie kann zumeist zwar konfiguriert oder abgestellt werden, dann geht allerdings das Verhalten der Hardware verloren. Z.B. wird nach einem Schreibbefehl an die Peripherie ein Registerbit durch die Hardware gesetzt. Ohne Simulation liegen die Register der Peripherie in einem RAM-Speicher und verhalten sich nur noch wie eine Variable.

Fault Injection – Fehlerinjektion im Quellcode

Deshalb werden Techniken wie die „Fault Injection“ angewendet: Dazu werden im Quellcode, etwa durch den Einsatz von Makros, Fehler in die Anwendung injiziert, um auf diese Weise ihr Verhalten testen zu können. Die Fault Injection wird in der Entwicklung mit Sicht auf das System und die Systemanforderungen durchgeführt, ohne die inneren Sicherheitsvorkehrungen der Software zu berücksichtigen, so dass eine unvollständige Anweisungsüberdeckung erst bei der Entwicklung der Unit Tests auftritt. Der Testingenieur erkennt diesen Mangel und kann die notwendigen Fault Injections implementieren.

Eine manuelle Fehlerinjektion per Makros o.ä. hat aber den Nachteil, dass sie manuell durchgeführt und verwaltet wird und anschließend im produktiven Quellcode verbleibt – dies ist bei hoch-sicherheitskritischen Anforderung oftmals nicht erwünscht. Denn in einem normgerechten Entwicklungsprozess sollte eine Quellcodeveränderung wieder alle Instanzen durchlaufen, d.h. die Änderungen werden freigegeben, in das Versionskontrollsystem übertragen und durchlaufen einen Codereview. Diese zusätzliche Iteration benötigt Zeit und verursacht weitere Kosten.

Bei professionellen Unit Test-Werkzeugen wird die Fault Injection deshalb ohne Quellcodeänderung implementiert. Hier wird die Messung der Anweisungsüberdeckung auch durch Instrumentierung des Quellcodes realisiert, jedoch ist dieser dynamisch für die Testdurchführung und verbleibt nicht im produktiven Quellcode. Durch das Abschalten der Messung der Anweisungsüberdeckung ist bei einem erneuten Testlauf durch das gleiche Testergebnis beider Testdurchführungen zusätzlich gewährleistet, dass diese Instrumentierung keinen Einfluss auf den Ablauf der Funktion nimmt.

Beleuchten wir eine oft genutzte Funktionalität eines Speichertests anhand eines sehr einfachen Beispiels näher. Betrachten wir Codebeispiel 1: In Zeile 6 wird der Speicher beschrieben und in Zeile 7 der geschriebene Wert überprüft. Bei Mikrocontrollern mit Cache steht möglicherweise zwischen beiden Zeilen weiterer Code, um den Cache zu löschen, damit ein erneuter Zugriff auf den Speicher gewährleistet ist. Da davon auszugehen ist, dass der Speicher fehlerfrei ist, ist der Code in Zeile 10 und somit der True-Pfad des if-Ausdruckes nicht erreichbar.

Eine Fehlerinjektion, gesteuert über ein Präprozessordefine FAULTINJECTION, kann in dem zuvor gezeigten Codeabschnitt z.B. ab Zeile 6 wie im darunter abgebildeten Beispiel 2 gezeigt aussehen. Wird das Define FAULTINJECTION gesetzt, so kann auch diese Fehlersituation getestet und die Anweisungsüberdeckung zu 100% erfüllt werden. Die zusätzliche Testvariable tstFaultInjection steuert, ob die Fehlersituation für einen Testfall eintritt oder nicht. Bei der Erzeugung des binären Codes für das Endprodukt ist das Define FAULTINJECTION nicht gesetzt, und die Fault Injection sollte nicht vorhanden sein.

Da nicht immer alle Fault Injections aktiv sein können, werden diese für das System weiter unterteilt und über verschiedene Defines und Testvariable gesteuert. Je nach Umfang des Systems steigt die Anzahl der Fault Injections, was wiederum eine entsprechende Verwaltung notwendig macht.

Artikelfiles und Artikellinks

(ID:45070688)