Beim Entwickeln von Fahrzeug-Steuergeräten spielen Tests in Hinblick auf die funktionale Sicherheit eine wichtige Rolle. Kontrollen in verschiedenen Stufen helfen dem Entwickler, die Konformität mit den ASIL-Standards zu gewährleisten. Hardware-Debugging kommt hierbei eine entscheidende Rolle zu.
In modernen Fahrzeugen ist eine zunehmende Zahl an Steuergeräten (Electronic Control Unit, ECU) verteilt. Sie stellen verschiedene Funktionen bereit, von grundlegenden Fahrfunktionen über Sicherheitsmaßnahmen bis hin zu Komfort- und Unterhaltungsfunktionen. Fehler in den ECUs können sich je nach Funktion unterschiedlich stark auf die Sicherheit der Insassen auswirken. Aus diesem Grund teilt man die ECUs entsprechend den verschiedenen Automotive-Safety-Integrity-Level(ASIL)-Stufen ein.
Ein Beispiel: Das Steuergerät für die elektrische Lenkung hat einen sehr starken Einfluss auf die Sicherheit, da eine Fehlfunktion der Lenkung zu schweren Verletzungen oder sogar zum Tod eines Insassen führen kann. Daher ist dieses ECU als ASIL-D klassifiziert, der höchsten Stufe der Sicherheit.
Beim Entwickeln einer ECU mit einer solchen ASIL-Klasse ist es nötig, die Sicherheit von Anfang an zu berücksichtigen. Der Entwicklungsprozess erfüllt gemäß ASIL-D die Anforderungen, die in der Norm ISO26262 festgelegt sind. Aus diesem Grund sollte das gesamte Softwaredesign einer angemessenen Entwicklungsmethodik folgen. So sind die verwendeten Tools möglicherweise zu qualifizieren, um nachzuweisen, dass sie für den Zweck geeignet sind.
Anschließend testen Entwickler die ECU samt der entsprechenden Software auf mehreren Ebenen mit verschiedenen Methoden. Hierzu zählen unter anderem Unit-Testing, Systemintegrationstests und Hardware-in-the-Loop-Tests. Viele der Tests können Entwickler mithilfe eines Hardware-Debuggers unterstützen, um – wie es die ISO26262 empfiehlt – so nah wie möglich an der realen Hardware zu testen.
Heutige Hochleistungs-ECUs implementieren in der Regel nicht nur eine Art von Anwendung, sondern mehrere. Diese können unterschiedliche Stufen der funktionalen Sicherheit haben. Zum Beispiel kann eine ECU neben einer ASIL-D-Applikation ebenfalls eine Quality-Management(QM)-Level-Applikation implementieren. Quality-Management, also ohne besondere Anforderungen an die funktionale Sicherheit. Was bedeutet das für das Entwickeln der Software?
In dem Fall gibt es zwei Möglichkeiten, die Anforderungen der ISO26262-Norm zu erfüllen: Entweder werden alle Komponenten so behandelt, als ob sie die höchsten ASIL-Anforderungen erfüllen müssten, oder es ist Störungsfreiheit (FFI, Freedom from Interference) zu garantieren. Das bedeutet, es muss sichergestellt sein, dass der QM-Teil den ASIL-Teil in derselben ECU nicht stören kann (Bild 1).
Hierzu muss zum einen die FFI in Bezug auf die Speichernutzung gewährleistet sein. Also darf der QM-Teil nicht in der Lage sein, den Speicher zu beschädigen, der einem ASIL-Teil zugewiesen ist. Der zweite Aspekt ist Timing & Execution. Hier ist beispielsweise zu gewährleisten, dass das Ausführen einer QM-Software nicht das Ausführen einer ASIL-Software blockieren kann. Der dritte Aspekt, Informationsaustausch, bezieht sich auf eine gestörte Datenkommunikation zwischen einem Sender und einem Empfänger, zum Beispiel durch Einfügen ungültiger Daten oder durch Blockieren eines Kommunikationspfads.
Für das erfolgreiche Entwickeln der sicherheitskritischen Applikationen einer ECU muss bereits der Compiler entsprechend qualifiziert sein, zum Beispiel die Compiler-Toolsets von Tasking. Doch selbst für einen qualifizierten Compiler sind – wie für alle genutzten Tools – gemäß ISO26262 die Risiken zu bewerten. Auch jeder Compiler hat Fehler. Alle bekannten sind im sogenannten Errata-Sheet aufgelistet. Bei sicherheitskritischen Applikationen ist der Quellcode zu analysieren und zu prüfen, ob er von bekannten Compiler-Bugs betroffen sein könnte. Meist geschieht das manuell. Es gibt jedoch Tools, wie den »TriCore Inspector« von Tasking, der Quellcode automatisch auf alle bekannten Compiler-Probleme untersucht und einen entsprechenden Bericht ausgibt. Entwickler können den Bericht entweder zum Anpassen des Quellcodes verwenden oder einfach an den Risikobewertungsbericht anhängen.
Nach dem Compiler ist der Code selbst auf Fehler zu überprüfen, unter anderem in Bezug auf die Anforderungen an die FFI. Hier helfen Tools wie der »Safety Checker« von Tasking, vergleichbar mit einer statischen Code-Analyse, die völlig Compiler-unabhängig ist. Der Entwickler beschreibt dem Tool die beabsichtigten Zugriffsrechte aller Sicherheits- und QM-Partitionen im System, also Lese-, Schreib- und Ausführungsrechte für bestimmte Speicherbereiche. Anschließend prüft das Tool den gesamten Quellcode und versucht, potenzielle Lecks zu identifizieren. Also eventuell mögliche Interferenzen zwischen den Partitionen, zum Beispiel durch unsicheres, unzureichend abgesichertes Verwenden von Pointern, globalen Variablen oder gemeinsam genutzten Speichern.
Dabei geht man davon aus, dass man das Trennen zwischen den Partitionen nicht mit Hardware-Methoden, also Hilfsmitteln wie einer Memory Protection Unit (MPU) oder einem Hypervisor, erreicht. Entweder sind solche Methoden nicht geplant oder verfügbar, oder sie sind noch nicht aktiviert. Im letzteren Fall hilft das Tool bei der Fehlersuche oder beim Vorbereiten der Software auf eine MPU. Anstatt eine MPU-Ausnahme nach der anderen zu debuggen, sobald die MPU aktiviert ist, kann man die Software im Voraus vorbereiten. Und das, ohne die Software tatsächlich auf einer realen Hardware auszuführen.
Der nächste Testschritt in der ECU-Entwicklung ist der Unit-Test. In den meisten Fällen führen Entwickler Unit-Tests auf Quellcode-Ebene und auf einem Host-PC aus. Das bedeutet, dass man den zu testenden Quellcode in ein Test-Framework verpackt. Hier lassen sich sogenannte Stubs hinzufügen. Es handelt sich um zusätzlichen Code, der während des Testablaufs eine andere Code-Komponente ersetzt, zum Beispiel um eine noch nicht implementierte Komponente oder Hardware-abhängige Komponenten wie I/Os zu simulieren. Zusammen mit den Testfällen wird das gesamte Paket kompiliert und auf dem Host-Rechner, wie einem Windows- oder Linux-PC, ausgeführt. Das Ergebnis ist ein Testbericht, der im Wesentlichen ein »bestanden«/»nicht bestanden« für alle Testfälle angibt, normalerweise zusammen mit einem Code-Coverage-Report.
Weil das Ausführen auf dem PC erfolgt, ist das grundsätzliche Ausführen auf der realen Hardware nicht mit abgedeckt. Somit kann es sein, dass die Tests unter Umständen keine identischen Ergebnisse liefern.Deshalb empfiehlt die ISO26262: »Die Testumgebung für Software-Unit-Tests soll so weit wie möglich der Zielumgebung entsprechen.« Warum also nicht den Test direkt auf dem Ziel ausführen?
Entwickler setzen Hardware-Debug-Tools traditionell zum Entwickeln und der Fehlersuche bei Treibern, Board/Hardware Bring-up, Boot-Prozessen und vielem mehr ein. Also für das »minimalinvasive«, Hardware-nahe Entwickeln von Embedded-Software. Neben diesen Standardmethoden bieten Hardware-Debugger ebenfalls Methoden zum Steuern von Softwaretests auf dem Zielsystem. Hierbei verbindet der Debugger die eigentliche Ziel-Hardware über Standard- Debug-Schnittstellen mit dem Zweck, Embedded-Software so nah wie möglich auf der eigentlichen Hardware zu entwickeln und zu testen. Das hilft speziell im Hinblick auf die Sicherheitsanforderungen, das heißt FFI, Ausführung und Timing sowie Informationsaustausch. Hierzu bieten sich einige spezifische Testanwendungsfälle an.
Beim Set-up für einen Unit-Test wird der Quellcode der zu testenden Software für das Zielgerät cross-compiliert und nicht instrumentiert. Somit lässt sich der ursprüngliche Produktionscode auf dem Zielgerät testen.
Die eigentliche Steuerung des Targets zum Ausführen des Tests, also das Herunterladen des Codes, der Aufruf der Unit, also der zu testenden C-Funktion, das Setzen der Testeingangsvektoren und das Rücklesen der Testergebnisse erfolgt über den zugrunde liegenden Debugger, wie »winIDEA« mit »BlueBox« von Tasking (Bild 2).
Wie beim Ausführen auf einem Host-PC werden hier ähnliche Testergebnisse generiert: Pass/Fail-Ergebnisse für jeden Testfall und ein Code-Coverage-Report. Allerdings wird hier die Codeabdeckung auf der Grundlage einer Hardware-Trace-Aufzeichnung und, wie bereits erwähnt, ohne jegliche Quellcode-Instrumentierung gemessen.
Für ein besseres Verständnis, wie ein Unit-Test auf einem realen Ziel ohne Code-Instrumentierung ausgeführt wird, soll der Unit-Test für die C-Funktion »calculateFuel- Efficiency« als konkretes Beispiel (Bild 3) dienen. Beim Einsatz eines Debuggers ist nicht die gesamte Applikation bis zum Funktionsaufruf auszuführen. Der Debugger kann den Instruction Pointer der CPU direkt auf den Funktionseinstieg setzen. Gemäß den C-Aufrufkonventionen richtet der Debugger den Stack-Frame für die Funktion ein und startet dann die CPU. Ist ein Stubbing oder eine Dateninjektion erforderlich, zum Beispiel wenn Unterfunktionen aufgerufen werden, stoppt der Debugger die CPU. Anstatt die Unterfunktionen aufzurufen, überspringt die CPU beide Funktionen und injiziert stattdessen den gewünschten Rückgabewert direkt in das dafür vorgesehene CPU-Register.
Anschließend wird die CPU bis zur Funktionsrückgabe ausgeführt – hier liest der Debugger den Ergebniswert aus, der sich anhand einiger Pass/Fail-Kriterien überprüfen lässt. All das funktioniert mit unverändertem Produktionscode.
Nach dem Unit-Test geht es weiter zu den Tests auf Systemebene, zum Beispiel innerhalb einer Hardware-in-the-Loop-Konfiguration. In diesem Fall kann ein Debugger sehr nützlich sein, um Fehler in das System zu injizieren und so die Auswirkungen auf die FFI in Bezug auf Speicherkorruption, Ausführung und Informationsaustausch zu testen. Man nutzt den Debugger, um On-the-fly-Manipulationen an On-Chip-Ressourcen wie CPU-Kernregistern sowie Speicher vorzunehmen und anschließend die Auswirkungen zu untersuchen (Bild 4). Aber auch an externen Schnittstellen können Fehler eingespeist werden: Add-on-Module für CAN/LIN und Analog/Digital-Signale, die Entwickler direkt an die Debugger-Hardware und das Zielsystem anschließen, können sie fehlerhafte Daten in einen Analog-Digital-Konverter (ADC) oder in externe, über CAN oder SPI angeschlossene Sensoren, injizieren.
So lässt sich beispielsweise überprüfen, ob eine Fehlfunktion einer QM-Software Auswirkungen auf die Ausführung einer ASIL-Funktion haben kann. Wie das System auf solche Manipulationen reagiert, beobachten Entwickler zum Beispiel mithilfe von On-Chip-Traces, dem Aufzeichnen von Abläufen in der Software in Echtzeit über Protokollieren von Ausführungszeiten im Bereich von Taktzyklen. Die Hardware-Debugger-Verbindung zwischen PC und realer Ziel-Hardware ist dabei essenziell. Sprich, eine entsprechende On-Chip-Debug-Trace-Schnittstelle auf dem in der ECU verbauten Prozessor muss vorhanden und herausgeführt sein
Doch On-Chip-Traces ermöglichen nicht nur, das Verhalten der Software zu überwachen. Weil Hardware-Traces absolut non-intrusive sind, also keinen Einfluss auf die Laufzeit haben, eignen sie sich ideal für die Timing-Analyse. Timing-Tests sollten Entwickler unbedingt als Teil der Systemintegrationstests durchführen. Die zunehmende Last auf die Betriebssystem-Tasks wirkt sich auf die gesamte Timing-Planung aus und später lassen sich kritische Zeitvorgaben nicht mehr einhalten.
Bei Timing-Analysen in Hinblick auf Sicherheit und FFI ist es zudem sinnvoll, einen Blick auf die Timing-Margins zu werfen: Im Grunde lässt sich die Robustheit des gesamten Software-Timings überprüfen. An einem Anwendungsfall lässt sich zeigen, was ein kombiniertes Debug- und Trace-Tool leisten kann: Auf einer CPU laufen drei Tasks, ein 100-ms-, 50-ms- und ein 10-ms-Task – der 10-ms-Task hat die höchste Priorität. Hierbei werden der Runnable des 10-ms-Task immer mehr Funktionen hinzugefügt und dabei die Auswirkung auf die Antwortzeit der 100-ms-Task gemessen.
Instrumentierungscode zur Runnable realisieren, um die Laufzeit zu verlängern. Ein Debugger ist in der Lage, den Instrumentierungscode während des Testlaufs zu ändern. So lässt sich die Laufzeit der Runnable variieren, ohne die Software neu zu erstellen.
Ein beispielhaftes Ergebnis ist in Bild 5 dargestellt: Die grüne Kurve zeigt die Antwortzeit des 100-ms-Task gegenüber der Laufzeit der 10-ms-Runnable. Angenommen, der 100-ms-Task hat die zeitliche Einschränkung, dass die Antwortzeit 85 ms nicht überschreiten darf: Die Zeitbeschränkung lässt sich erreichen, solange die Laufzeit der 10-ms-Runnable unter 4,5 ms bleibt. Interessant ist außerdem, dass die Antwortzeit an einem bestimmten Punkt fast exponentiell ansteigt und an dem Punkt auch die CPU-Last nicht mehr zunimmt. Das ist ein klares Indiz dafür, dass das Betriebssystem-Scheduling nicht mehr wirklich zuverlässig funktioniert, da das System bereits überlastet ist.
Der Hardware-Debugger ist zunehmend ein Prozesswerkzeug – die Basisfunktionen eines Debuggers finden ihre gewohnte Anwendung und lassen sich mit mächtigen Analysefunktionen ergänzen. Alle vorgestellten Anwendungsfälle und Tools können Entwickler zu einer Continuous-Integration-Pipeline zusammenführen, die explizit auf das Testen der Softwaresicherheit ausgerichtet ist. So lässt sich ein Testablauf erstellen, von der statischen Code-Analyse über Unit-Tests auf dem Zielsystem bis zu Debug- und Trace(Timing)-Tests.
Natürlich ist es nicht sinnvoll, all die Tests bei jedem Commit durchzuführen. Möglich wäre zum Beispiel eine Strategie, die vorsieht, dass die Tests auf Codeebene lediglich nachts laufen oder lediglich bei Software Releases, und die Integrations-Tests jedes Mal, wenn Softwarezweige im Master-Trunk zusammengeführt werden.
Der Autor
Armin Stingl, ist Chief Solutions Officer bei iSystem, einem Tasking-Unternehmen