Rust im Embedded Bereich: Wie man Programmierfehler mit Todesfolge verhindert

27.1.2025 - Martin Defuns

Embedded Rust

Wenn zwei Prozesse gleichzeitig auf denselben Speicher zugreifen, kann das fatale Folgen haben - wie der tragische Therac-25 Vorfall in den 1980er Jahren zeigte. Heute, fast 40 Jahre später, verspricht die Programmiersprache Rust solche "Race Conditions" durch ihr einzigartiges Ownership-Konzept unmöglich zu machen. Besonders im Embedded-Bereich, wo Sicherheit und Ressourceneffizienz Hand in Hand gehen müssen, wäre dies ein gewaltiger Fortschritt. In den letzten Wochen des Jahres 2024 haben wir Rust auf Embedded-Systemen getestet.

Der Therac-25 Vorfall

Zwischen 1985 und 1987 ereignete sich eine tragische Serie von Vorfällen, die das Vertrauen in softwaregesteuerte Medizingeräte erschütterte. Ein unscheinbarer Softwarefehler führte dazu, dass drei Menschen ihr Leben verloren und drei weitere schwer verletzt wurden. Die Ursache? Eine sogenannte "Race Condition".Der Therac-25, ein hochmodernes Strahlentherapiegerät der Atomic Energy of Canada Limited (AECL), wurde entwickelt, um Krebspatienten präzise und sicher zu behandeln. Doch hinter der fortschrittlichen Technologie lauerte eine tückische Gefahr. Die Software des Geräts war für mehrere Aufgaben gleichzeitig verantwortlich – sie erfasste Messwerte, steuerte die Maschine und ermöglichte die Interaktion mit dem Bediener. Doch genau dieses Multitasking wurde zum Verhängnis.Unter bestimmten Umständen griffen zwei Prozesse gleichzeitig auf denselben Speicherbereich zu – während der Bediener noch Daten änderte (Schreibzugriff), las das System diese bereits aus und begann mit der Kalibrierung. Dadurch wurden teils veraltete oder unvollständige Werte verwendet, was zu fehlerhaften Strahlendosen führte. Eine fehlende Synchronisation zwischen Lese- und Schreiboperationen, ein klassisches Beispiel für eine "Race Condition", wurde den Patienten zum Verhängnis [1].Dieser Vorfall zeigt eindrucksvoll, wie kritisch eine zuverlässige Softwareentwicklung in sicherheitsrelevanten Bereichen ist. Hier kommt Rust ins Spiel – eine moderne Programmiersprache, die genau für solche Herausforderungen entwickelt wurde.

Die Geschichte von Rust

Rust entstand zwischen 2006 und 2009 als persönliches Projekt des Mozilla Mitarbeiters Graydon Hoare. Der Name "Rust" kommt laut Hoare vom Rostpilz, der "übermässig gut für sein Überleben entwickelt ist". Im Jahr 2009 begann Hoare, über sein persönliches Projekt zu sprechen, angeregt durch Interesse seiner Arbeitskollegen. Sein Ziel war es, bewährte Konzepte aus älteren Programmiersprachen zu priorisieren, anstatt eine gänzlich neue Sprache zu entwickeln. Rust sollte hohe Geschwindigkeit mit Speicher- und Thread-Sicherheit kombinieren.Da eine solche Funktionalität eine interessante Möglichkeit für die Entwicklung eines sicheren Webbrowsers bot, sponserte Mozilla das Rust-Projekt noch im selben Jahr offiziell. Die erste offizielle Version erschien 2012. Kurz darauf, im Jahr 2013, trat Hoare zurück und die Entwicklung von Rust war nun in den Händen eines sechsköpfigen Teams. In den folgenden Jahren entwickelte sich Rust durch Open-Source-Beiträge sowie interne Weiterentwicklung stetig weiter, bis am 15. Mai 2015 die erste, offizielle stabile Version Rust 1.0 erschien.Seitdem hat Rust viele Veränderungen durchgemacht und ist heute eine beliebte Alternative zu C++ oder C.

Die Tücken von Low-Level-Programming

Embedded Programming wird heute häufig mit C und C++ umgesetzt. Diese Sprachen ermöglichen "Low-Level-Programming", was bedeutet, dass man sehr nah an der Hardware programmieren kann. Der Vorteil dabei ist, dass die kompilierten Programme wenig Speicher benötigen und sehr schnell sind. Ausserdem erlauben diese Sprachen, Programme unmittelbar für die Zielarchitektur zu kompilieren. Dadurch können sie ohne Betriebssystem auf einem Mikrocontroller ausgeführt werden, indem sie direkt mit der Hardware des Controllers interagieren. Dies ist essenziell im Embedded Programming, da man je nach Chip mit stark limitierten Ressourcen arbeiten muss.Um mit einem Mikrocontroller zu interagieren, erfolgt der Zugriff auf die einzelnen Register des Controllers durch gezieltes Lesen und Schreiben. Über diese Register können verschiedene Funktionen des Geräts gesteuert oder Sensordaten erfasst werden.

Diese Freiheit bringt jedoch einige Nachteile mit sich: Der Grösste davon ist, dass Speicherzugriffe nicht überprüft werden. Ein Programmierfehler kann Teile des Programms oder der Daten einfach überschreiben. Dies führt zu Sicherheitslücken und Fehlern, die schwer zu finden sind. Werden Programme dazu noch parallel ausgeführt, wie im Fall von Therac-25, kann dies häufig zu schwer nachvollziehbaren Problemen führen, die nur selten auftreten und dadurch aufwändig zu lokalisieren sind. Der Grund dafür liegt in der nicht-deterministischen Natur paralleler Programme – die Reihenfolge, in der einzelne Anweisungen ausgeführt werden, kann sich bei jedem Lauf unterscheiden. Dadurch verhalten sich Programme nicht immer gleich, was die Reproduzierbarkeit von Fehlern erschwert und deren Behebung zu einer grossen Herausforderung macht.

Rust zur Rettung

Rust bietet eine spannende Alternative. Es kombiniert die Performance und den direkten Hardwarezugriff von Low-Level-Sprachen mit modernen Sicherheitsmechanismen, die viele typische Fehler verhindern können. Dies wird erreicht, indem der Compiler viel strenger ist als bei Sprachen wie C und C++. Der Compiler hilft, typische Fehler durch verschiedene Prinzipien zu vermeiden – der wichtigste davon ist Ownership.

Ownership

Ownership ist die wohl einzigartigste Eigenschaft von Rust und hat weitreichende Auswirkungen auf die gesamte Sprache. Sie ermöglicht es Rust, Speicher-Sicherheitsgarantien zu gewährleisten und vor allem hilft es, typische Fehler bei paralleler Programmierung zu vermeiden. Dies alles mithilfe von drei einfachen Regeln:

  • Jeder Wert in Rust hat einen Besitzer (Owner)

  • Es kann immer nur einen Besitzer gleichzeitig geben

  • Sobald der Besitzer nicht mehr verwendet wird, wird der Wert automatisch freigegeben

Wenn man unter anderem eine Liste myList in Rust initialisiert, so wird die Variable, welche mit dieser Liste verbunden ist zum Besitzer (Owner) dieser Liste. Diese Liste kann entweder an andere Funktionen oder Codeblöcke ausgeliehen (borrow) oder ihr Ownership kann übertragen (move) werden. Der entscheidende Punkt dabei ist, dass nach einem Transfer der Ownership die ursprüngliche Variable nicht mehr verwendet werden darf.

Diese Regeln werden während der Kompilierung durch den "Borrow Checker" überprüft. Rust erkennt genau, wer eine Variable ausgeliehen hat oder wer ihr aktueller Besitzer ist. Dadurch können viele Fehler, die in anderen Sprachen erst zur Laufzeit auftreten würden, bereits während der Kompilierung erkannt und verhindert werden.

Furchtlose Parallelität

Wie bereits am Beispiel des Therac-25 gezeigt, stellt Parallelisierung in vielen Programmiersprachen eine grosse Herausforderung dar, oft mit unvorhersehbaren und gefährlichen Konsequenzen. Rust bietet mit seinem Ownership-System eine elegante Lösung für dieses Problem. Dank der strikten Regeln von Ownership und dem Borrow Checker stellt Rust sicher, dass:

  • Mehrere Threads niemals gleichzeitig schreibend auf denselben Speicher zugreifen können.

  • Daten entweder exklusiv einem Thread gehören oder sicher zwischen Threads geteilt werden.

Somit werden Parallelisierungfehler bereits zur Kompilierzeit erkannt und Entwickler können parallelen Code schreiben, ohne sich Sorgen über typische Fehler machen zu müssen. Rust erzwingt Thread-Sicherheit auf Sprachebene! Im Falle des Therac-25 hätte Rust den gleichzeitigen Lese- und Schreibzugriff auf denselben Speicherbereich verhindert und so eine der Hauptursachen des Fehlers ausgeschlossen.

Datenrennen: Ein typischer Bug in Low-Level-Sprachen und wie Rust dieses Problem verschwinden lässt

Ein Datenrennen (Data Race) tritt auf, wenn mehrere Threads gleichzeitig auf denselben Speicherbereich zugreifen, einer schreibt, während ein anderer liest, ohne angemessene Synchronisation. Solche Fehler sind besonders tückisch, da sie oft sporadisch auftreten und schwer reproduzierbar sind. Im folgenden sehen wir ein einfaches Beispiel in der Programmiersprache C, wo solch ein Fehler passieren kann:

Wenn die Methode my_method() den Speicher, auf den pointer zeigt, freigeben würde, wäre dieser Speicher anschliessend ungültig. Dennoch greift das Hauptprogramm danach erneut auf *pointer zu, was zu undefiniertem Verhalten führen kann. Das Programm könnte abstürzen, falsche Werte anzeigen oder sogar Sicherheitslücken öffnen. Das eigentliche Problem liegt darin, dass wir nicht kontrollieren können, welche Anweisung zuerst ausgeführt wird. Wird my_method() auf dem separaten Thread zuerst ausgeführt, wird der Speicher freigegeben, bevor das Hauptprogramm darauf zugreifen kann – was zu einem Fehler führt. Läuft das Hauptprogramm jedoch zuerst weiter, verhält es sich scheinbar korrekt. C würde diesen Code jedoch problemlos kompilieren, obwohl er eine potenzielle Gefahr darstellt.

Beispiel DataraceBeispiel eines typischen Datenrennens. Die Zeit verläuft von oben nach unten. In Situation 1 wird die printf-Funktion ausgeführt, bevor der Thread den Speicher freigibt. In Situation 2 wird der Speicher jedoch zuerst freigegeben, was zu einem Fehler führt.

Das gleiche Programm in Rust

In diesem Rust-Code würde der Compiler einen Fehler melden, da die Ownership der Variable value an den neu gestarteten Thread übergeben wurde. Dadurch ist value im Hauptthread nicht mehr verfügbar. Rusts Ownership-System stellt sicher, dass nach der Übergabe keine unzulässigen Speicherzugriffe mehr möglich sind.

Der entscheidende Unterschied zu C: Während C diesen fehleranfälligen Code problemlos kompilieren würde, erkennt Rust das Problem bereits zur Compile-Zeit, lange bevor das Programm überhaupt ausgeführt wird. Dies verhindert potenzielle Datenrennen und sorgt für höhere Sicherheit und Zuverlässigkeit in parallelen Programmen.

Hinweis: Die beiden Programme sind stark vereinfacht dargestellt, und einige technische Details wurden ausgelassen, um die Beispiele verständlicher zu halten.

Embedded Programming mit Rust: Vorteile und Herausforderungen

Wie man erkennen kann, sind die Garantien, die uns Rust gibt, besonders hilfreich im Embedded Programming, da viele Fehler vermieden werden können. Da Rust jedoch eine relativ neue Sprache ist, verglichen mit C und C++, wollten wir testen, wie gut sich Rust für Embedded Programming eignet.

Unsere Experimente wurden mit Espressif-Chips durchgeführt. Für Embedded Rust gibt es bereits eine grosse Menge an Ressourcen. Espressif hat sogar eine eigene Rust-Bibliothek entwickelt, die einen Grossteil der ESP-Chips unterstützt. Eine Liste mit Ressourcen für die Entwicklung mit Rust auf Espressif-Geräten findet sich hier.

Während unserer Arbeit mit Rust und Espressif haben wir festgestellt, dass Treiber für Peripheriegeräte wie Displays oder LEDs oft nicht korrekt funktionieren oder gar nicht existieren. In mehreren Fällen mussten wir die Treiber selbst schreiben. Dies ist machbar, wenn man einfache LEDs ansteuern möchte. Der Zeitaufwand für die Entwicklung eines LCD-Display-Treibers ist jedoch erheblich und kann ein Projekt unnötig verzögern.

Das Positive

  • Espressif bietet offiziellen Support für Rust.

  • Es gibt viele Beispiele, die zeigen, wie Espressif-Chips für verschiedene Anwendungen genutzt werden können.

  • Die Embedded Rust Community ist sehr aktiv und hilfsbereit.

  • Die Bibliothek zur Steuerung der ESP-Chips funktioniert zuverlässig.

Das Negative

  • Wenn etwas nicht funktioniert, muss man oft selbst oder mithilfe der Community herausfinden, wo der Fehler liegt.

  • Viele Bibliotheken zur Ansteuerung spezifischer Peripheriegeräte wie LCD-Displays oder LEDs werden von der Community entwickelt und funktionieren nicht immer einwandfrei.

Fazit

Rust ist im Embedded Bereich bereits gut etabliert, aber man merkt, dass die Sprache noch nicht so lange existiert wie C oder C++. Diese älteren Sprachen, die seit Jahrzehnten Standards im Embedded Programming sind, bieten weitreichenden Support für viele Chips und Geräte.

Das bedeutet jedoch nicht, dass Rust für Embedded Programming ungeeignet ist. Man muss sich jedoch bewusst sein, dass die Nutzung von Rust mit einem Kompromiss verbunden ist: Mehr Sicherheit auf Kosten eines höheren Arbeitsaufwands bei der Entwicklung. Die strengen Regeln von Rust erfordern mehr Zeit und Sorgfalt beim Schreiben des Codes. Auf lange Sicht zahlt sich dieser Mehraufwand jedoch aus – durch die höhere Sicherheit und die frühzeitige Fehlererkennung wird weniger Zeit für die Fehlersuche benötigt. Was anfangs mehr Aufwand bedeutet, führt letztendlich zu robusterem und wartungsärmerem Code.

Wenn Sicherheit höchste Priorität hat und Zeit eine untergeordnete Rolle spielt, ist Rust eine äusserst attraktive Alternative zu herkömmlichen Low-Level-Sprachen. Hat man jedoch weniger Zeit oder Ressourcen und benötigt sofortigen Zugriff auf eine breite Palette von Treibern und Tools, empfiehlt sich der Einsatz von C oder C++, da der Arbeitsaufwand in der Regel geringer ist.

Trotzdem ist Rust eine innovative und zukunftsweisende Sprache, die zunehmend in der Industrie eingesetzt wird. Mit der wachsenden Verbreitung von Rust wird auch der Support im Embedded Bereich zunehmen, was Rust zu einer immer attraktiveren Alternative machen dürfte.

[1]: Leveson, Nancy G., and Clark S. Turner. "An investigation of the Therac-25 accidents." Computer 26.7 (1993): 18-41.

Hauptbild und Grafiken: Dall-E, Martin Defuns

Kontakt

Smoca AG
Technoparkstrasse 2
Gebäude A, 3. Stock
8406 Winterthur

Jobs

  • Momentan sind keine Stellenangebote verfügbar

Letzter Blogeintrag

Rust im Embedded Bereich: Wie man Programmierfehler mit Todesfolge verhindertMartin Defuns - 27.1.2025

Wenn zwei Prozesse gleichzeitig auf denselben Speicher zugreifen, kann das fatale Folgen haben - wie der tragische Therac-25 Vorfall in den 1980er Jahren zeigte. Heute, fast 40 Jahre später, verspricht die Programmiersprache Rust solche "Race Conditions" durch ihr einzigartiges Ownership-Konzept mehr ...

  • Smoca Facebook
  • Smoca Twitter
  • Smoca LinkedIn
  • Smoca RSS Feed