43844.jpg

Grammatech

Moderne Autos sind in gewisser Hinsicht nichts anderes als Computersysteme mit Rädern und einem Motor. Der augenfälligste Computer im Auto mag das Infotainment-System sein. Doch gibt es noch eine Vielzahl weiterer Systeme wie das Motormanagement, Bremssysteme, Sicherheitsausstattungen und viele zusätzliche Features, die auf Computern basieren und auf die Modernisierung vorhandener Alt-Software angewiesen sind. Alles, was Software enthält, ist anfällig für Programmierfehler, die gravierende Ausfälle zur Folge haben können. Fehler dieser Art können sämtliche Arten von Fehlfunktionen verursachen, deren mögliche Folgen von schlichten Komforteinbußen bis zu ernsten Gefährdungen reichen. Der 2010 erfolgte Rückruf des Toyota Prius etwa diente zur Behebung eines Softwaredefekts, der eine ungleichmäßige Bremswirkung verursachte. 2011 rief Jaguar 18.000 Fahrzeuge zur Behebung eines Bugs zurück, der einigen Fahrern das Ausschalten des Tempomats verwehrte. Chrysler schließlich war 2013 zu einem Rückruf dreier Fahrzeugmodelle gezwungen, bei denen ein Softwaredefekt dafür sorgte, dass bei einem Unfall die falschen Airbags zündeten.

Nebenläufigkeits-Fehler

Es gibt kein Konzept, das in allen Situationen perfekt ist. Statische Tools umgehen die Notwendigkeit, die enorme Vielfalt möglicher Thread-Verzahnungen zu durchsuchen. Entwickler, die Software von hoher Qualität hervorbringen wollen, sind aus diesem Grund gut beraten, bei der Suche nach Nebenläufigkeits-Fehlern und bei deren Beseitigung sowohl auf statische als auch auf dynamische Tools zu setzen.

Wiederverwendung von Code

Eine Strategie, das Risiko von Softwaredefekten abzuwenden, ist die Wiederverwendung von vorhandenem Code, der sich bereits als verlässlich erwiesen hat. Grundlage hierfür ist die Argumentation, dass Software, die ihre Zuverlässigkeit bereits in einem Fahrzeugmodell unter Beweis gestellt hat, mit großer Wahrscheinlichkeit auch in einem neuen Modell ähnlich zuverlässig sein wird. Dies trifft in der Tat häufig zu, doch gibt es bestimmte Arten von Softwarefehlern, die lange Zeit unentdeckt bleiben können und erst dann zu Tage treten, wenn die Software auf einer anderen Softwareplattform ausgeführt wird. Das Wechseln der Verarbeitungsplattform aber ist gängige Praxis, weil der Wunsch nach mehr Funktionalität den Einsatz immer schnellerer und leistungsstärkerer Mikroprozessoren erfordert.

Umstieg auf Multicore-Prozessoren

Eine interessante und kosteneffektive Möglichkeit, die Performance zu verbessern, ist der Umstieg auf Multicore-Prozessoren, die gegenüber Single-Core-Prozessoren ein enormes Potenzial zur Steigerung der Verarbeitungsleitung haben. Um aber die Leistungsreserven dieser Prozessoren optimal ausschöpfen zu können, muss der Code so geschrieben sein, dass er die Nebenläufigkeit unterstützt. Das Umschreiben von vorhandenem Programmcode aber ist riskant, denn jede Softwareänderung birgt die Gefahr, dass neue Defekte entstehen.

Besonders viele Risiken bestehen beim Umschreiben von Single-Threaded-Code für die Multi-Threaded-Verarbeitung. Nebenläufigkeits-Defekte wie etwa Data-Races, Deadlocks und Starvation kommen sehr leicht vor und sind äußerst schwierig zu eliminieren. Eine weitere Komplikation ergibt sich daraus, dass viele Programmierer falsche Vorstellungen von einigen wichtigen Aspekten der Multi-Threaded-Programmierung haben.

Wenn der ursprüngliche Code auf einer Single-Thread-Struktur basierte, versteht es sich von selbst, dass bei der Migration größte Vorsicht angewendet werden muss. Weniger offensichtlich ist dagegen die Tatsache, dass auch Code, der bereits in mehrere Threads gegliedert ist und vielleicht schon jahrelang ohne Probleme auf einem Single-Core-Prozessor gelaufen ist, ein hohes Fehlerrisiko birgt, wenn er von einem Multi-Core-Prozessor verarbeitet wird.

Data-Races

Bild 1. Beispiel für ein Data-Race.

Bild 1. Beispiel für ein Data-Race.Grammatech

Um die Gründe hierfür zu erläutern, muss auf einen der subtilsten Bug-Typen eingegangen werden, der in nebenläufigen Systemen auftreten kann. Gemeint sind die sogenannten Data-Races. Es handelt sich dabei um Race-Conditions, die bei Zugriffen auf einen gemeinsam genutzten Speicher auftreten können. Zu einem Data-Race kommt es, wenn mehrere Verarbeitungs-Threads auf eine gemeinsam genutzte Speicherstelle zugreifen, wenn mindestens einer dieser Threads an diese Speicherstelle schreibt und wenn die Zugriffe nicht durch eine explizite Synchronisation (zum Beispiel durch Freigabe eines Mutex oder Setzen eines Semaphoren) separiert werden. Bild 1 zeigt ein Beispiel hierfür. Das Einkreisen und Diagnostizieren des Fehlers gestaltet sich recht schwierig, denn die Speicherinhalte werden durch Data-Races nicht unbedingt verfälscht. Kommt es zu einer Verfälschung des Speicherinhalts, muss dies außerdem nicht in jedem Fall einen von außen erkennbaren Bug auslösen.

Ob Data-Races einen Ausfall zur Folge haben oder nicht, hängt in hohem Maße vom Timing des jeweiligen Verarbeitungsdurchgangs ab. Ein bestimmter Testfall kann bei exakt gleichen Eingangsbedingungen hunderte Male einwandfrei ablaufen, um dann aus unerfindlichen Gründen fehlzuschlagen, weil eine Race-Condition ein unerwünschtes Verhalten ausgelöst hat. Tritt ein Funktionsfehler auf, ist er schwierig zu diagnostizieren, da sich die genaue Abfolge der Speicherzugriffe, die die Störung hervorgerufen hat, nur schwierig reproduzieren lässt.

Problematisch sind Data-Races, weil das Ineinandergreifen der Speicherzugriffe der verschiedenen Threads im Wesentlichen nicht-deterministisch erfolgt. Schon bei kleinen Programmen ist die Zahl der möglichen Verzahnungen enorm, und es ist unmöglich, alle denkbaren Kombinationen zu überprüfen.

Multi-Threaded-Programme verhalten sich auf Single-Core-Prozessoren tendenziell unproblematischer, weil die Nebenläufigkeit hier nur virtueller Natur ist. Die Threading-Bibliothek oder das Betriebssystem kümmern sich um das Scheduling der Threads, um die parallele Verarbeitung zu simulieren. Zu viele Kontextwechsel sind ineffizient, weil sie stets mehrere Befehle erfordern. Der Scheduler versucht aus diesem Grund, ihre Anzahl zu minimieren. Die CPU führt deshalb möglicherweise einige Tausend Befehle von Thread A aus, um anschließend ein paar Millionen Befehle von Thread B zu verarbeiten und anschließend wieder zu Thread A zu wechseln, und so weiter. Dies führt dazu, dass die Speicherzugriffs-Ereignisse der einzelnen Threads nur grob miteinander verzahnt werden.

Reale physische Nebenläufigkeit

Bild 2. Die Verzahnung der Speicherzugriffs-Ereignisse erfolgt auf einem Multi-Core-Prozessor wesentlich feinstufiger.

Bild 2. Die Verzahnung der Speicherzugriffs-Ereignisse erfolgt auf einem Multi-Core-Prozessor wesentlich feinstufiger.Grammatech

Auf einem Multi-Core-Prozessor kommt es dagegen zu einer realen physischen Nebenläufigkeit, und die Speicherzugriffs-Ereignisse werden wesentlich feiner miteinander verzahnt, wie es Bild 2 verdeutlicht.

Die grobe Verzahnung bei der Verarbeitung durch einen Single-Core-Prozessor macht es wesentlich unwahrscheinlicher, dass ein Data-Race einen Funktionsfehler auslöst. Wenn die problematischen Speicherzugriffe beispielsweise nur wenige Befehle nach dem Start des Threads vorkommen, ist die Wahrscheinlichkeit, dass der Scheduler so früh einen Kontextwechsel vornimmt, gering. Dementsprechend unwahrscheinlich ist es, dass die Zugriffe falsch verzahnt werden. Bei der feinstufigeren Verzahnung auf einem Multi-Core-Prozessor ist dagegen schon aufgrund der größeren Zahl der Verzahnungen das Risiko eines Fehlers höher.

Hieraus lässt sich der Schluss ziehen, dass Tests auf einem Single-Core-Prozessor denkbar schlecht geeignet sind, um Aussagen über die Betriebssicherheit nebenläufiger Programme zu machen. Latente, nicht zum Tragen gekommene Nebenläufigkeits-Schwachstellen, die bislang unentdeckt blieben, können ernste Fehler auslösen.

Warum alle Data-Races gefährlich sind

Eine weitere Gefahr im Zusammenhang mit der Multi-Threaded-Programmierung besteht darin, dass einige wichtige Aspekte des Problems von den Programmierern nicht immer richtig verstanden werden. Verschärft wird das Risiko zudem durch eine Reihe verbreiteter Fehleinschätzungen im Zusammenhang mit Nebenläufigkeits-Fehlern. Am weitesten verbreitet ist die irrige Auffassung, Data-Races seien vollkommen harmlos und es sei ungefährlich, Code mit diesem Fehler auszuliefern. Diese Ansicht ist grundlegend falsch! Alle Data-Races sind potenziell gefährlich und ihre Eliminierung ist die einzige Möglichkeit, eine sichere Verarbeitung zu gewährleisten. Da dies der Intuition vieler Programmierer widerspricht, soll diese Aussage nun begründet werden.

Die Programmiersprache C entstand lange bevor das Multi-Threading große Verbreitung erlangte. Die Unterstützung für die Nebenläufigkeit wurde deshalb erst in späteren Versionen der offiziellen Spezifikation eingeführt. Um die Kompatibilität zu vorhandenen Codebeständen und den bisherigen Spezifikationen zu wahren, ging man allerdings hinsichtlich des Multi-Threadings bestimmte Kompromisse ein. Diese erlauben den Compilern das Fällen von Optimierungs-Entscheidungen, die der Intuition der Programmierer zuwiderzulaufen scheinen. Die offizielle Spezifikation der Sprache besagt, dass Data-Races ein undefiniertes Verhalten darstellen und die Compiler bei ihrem Vorkommen frei entscheiden können, was zu tun ist.

Security

Warum es außerdem notwendig ist, neben Safety-Maßnahmen (beispielsweise gemäß ISO 26262) auch Security-Maßnahmen zur Datensicherheit zu implementieren, das erfahren Sie in diesem separaten Beitrag der Redaktion.

In der Praxis tun die Compiler das einzig Vernünftige, indem sie davon ausgehen, dass Data-Races nie vorkommen. Kommt ein sogenannter gutartiger Data-Race vor, kann der Compiler Optimierungen am Code vornehmen, die zwar völlig angemessen sind, das eigentlich harmlose Programm aber in fehlerbehafteten Code verwandeln. Eine der häufigsten Optimierungen ist beispielsweise das Umstellen des Codes. Die Befehle werden dabei in einer Reihenfolge ausgeführt, die von der textlichen Anordnung im Quellcode abweicht. Diese wichtige Optimierung gibt dem Compiler die Möglichkeit, von Cache-Speichern und anderen Hardware-Features zu profitieren sowie die Programmverarbeitung so zu beschleunigen. Es ist deshalb auch nicht sinnvoll, solche Optimierungen zu deaktivieren.

Es ist aus diesem Grund wichtig, dass die Entwickler gegenüber Data-Races eine Null-Toleranz-Politik anwenden und jedes verfügbare Hilfsmittel nutzen, um derartige Defekte zu beseitigen.

Tools zum Aufspüren von Nebenläufigkeits-Defekten

Bild 3. Screenshot eines statischen Analyse-Tools, das einen gravierenden Nebenläufigkeits-Fehler aufgedeckt hat.

Bild 3. Screenshot eines statischen Analyse-Tools, das einen gravierenden Nebenläufigkeits-Fehler aufgedeckt hat.Grammatech

Allerdings ist die Fähigkeit traditioneller Prüftechniken, Nebenläufigkeits-Defekte zu finden, unzureichend. Entwickler müssen deshalb neue Methoden und Werkzeuge einsetzen, um sichere Aussagen über die einwandfreie Verarbeitung von Multi-Core-Software zu bekommen. Inzwischen gibt es dynamische Tools, die entweder die Verarbeitung des Codes auf verdächtige Speicherzugriffs-Muster absuchen oder aber den Prüfern die Möglichkeit geben, das Scheduling genau zu kontrollieren, damit sich Fehler leichter reproduzieren und diagnostizieren lassen. Statische Methoden hingegen bieten auf andere Weise einen Nutzen, denn sie decken etwaige Defekte auf, indem sie den Quellcode direkt auf mögliche problematische Verarbeitungspfade absuchen. Da sie alle möglichen Thread-Verzahnungen untersuchen können, sind sie den dynamischen Tools damit überlegen. Stoßen sie auf einen Defekt wie etwa ein Data-Race, liefern sie eine umfassende Erläuterung mit, weshalb sie diese Meldung generiert haben. Die Entwickler erhalten auf diese Weise eine wichtige Hilfestellung für das Beseitigen des Fehlers. Bild 3 zeigt einen Screenshot eines statischen Analyse-Tools. Die Bildschirmdarstellung illustriert ein Data-Race.