Legacy Code in C mit Ada sicher aktualisieren

Autor / Redakteur: Benjamin M Brosgol * / Sebastian Gerstl |

Wachsende Anforderungen an Sicherheit und Zuverlässigkeit moderner eingebetteter Systeme werden zum Problem, wenn in C geschriebene Legacy-Software modifiziert werden soll: wie können die in der Regel höheren Sicherheitsanforderungen, die für die neue Software gelten, erreicht werden, ohne die Performance des C-Codes zu beeinträchtigen? Eine Lösung kann sein, Ada für neue Komponenten zu verwenden.

Anbieter zum Thema

Wie behält man etablierten C-Code in neuen Anwendungen bei, ohne neue Sicherheitsrisiken einzugehen? Bei Ada-Programmierung lassen sich bewährte C-Funktionen mittels Wrappers sicher importieren.
Wie behält man etablierten C-Code in neuen Anwendungen bei, ohne neue Sicherheitsrisiken einzugehen? Bei Ada-Programmierung lassen sich bewährte C-Funktionen mittels Wrappers sicher importieren.
(Bild: gemeinfrei / Pixabay )

Die Welt der eingebetteten Systeme spricht zum allergrößten Teil C. Diese Sprache eignet sich gut für hardwarenahe Programmierung, wie zum Beispiel Interruptbehandlung, memory mapped I/O, Gerätetreiber und ähnliches. Die Anfälligkeit von C-Code für Sicherheitsprobleme, wie der berüchtigte „Pufferüberlauf“, sowie die fehlenden Möglichkeiten der Strukturierung von großen Programmen, führen zu Risiken, wenn neuer Code entwickelt und verifiziert werden soll.

C++ ist eine mögliche Alternative, hat aber die gleichen Probleme wie C, und die Komplexität von C++ kann ein Hindernis sein für Entwickler, die zwar Fachmänner auf ihrem Gebiet sind, aber nicht zwingend Experten in Programmiersprachen.

Eine weitere alternative Lösung wäre die Verwendung von Ada für die neuen Komponenten. Ada verfügt über effiziente und einfach zu benutzende Schnittstellen mit C-Code, und hat einige Vorteile, wie die frühe Erkennung von Fehlern und reduzierte Kosten der Verifikation.

Gerade wenn hohe Zuverlässigkeit benötigt wird, kann Ada eine attraktive und leicht umsetzbare Lösung darstellen. Die Sprache hat eine lange Historie von erfolgreichen Einsätzen in kritischen eingebetteten Bereichen, und Ada-Code wurde nach den höchsten Sicherheitsniveaus von Standards wie DO-178B/C (kommerzielle Luftfahrt), ECSS-E-ST-40C und ECSS-Q-ST-80C (Raumfahrt) und EN 50128 (Eisenbahn) zertifiziert. Ein großer Vorteil von Ada ist die hohe Kompatibilität mit anderen Sprachen, insbesondere C. Dies wird durch eine Kombination von standardisierten Sprachfeatures und Compiler-spezifischen Techniken erreicht.

Unterstützung der Programmiersprache Ada

Um kompilierten Code in verschiedenen Sprachen in das gleiche ausführbare Programm zu kombinieren, ist es nötig, Daten gleich darzustellen. Auch Aufrufkonventionen und Laufzeitmodelle müssen identisch sein. Der Abschnitt des Sprachstandards von Ada, der sich mit der Schnittstelle von C befasst, enthält die folgenden Features, die zusammen all diese Bedingungen erfüllen:

  • Das vordefinierte Paket Interfaces.C definiert Typen, die genau den vordefinierten Typen in C (int, char, usw.) entsprechen, sowie die zugehörigen Operationen. Andere vordefinierte Pakete unterstützen die Verarbeitung von null-terminierten Zeichenketten, wie sie in C üblich ist, und andere Idiome,
  • Das Pragma Convention (C) stellt sicher, dass eine benutzerdefinierte Datenstruktur in Ada die gleiche Repräsentation hat wie die entsprechende C-Struktur (so entspricht etwa ein record in Ada einem struct in C),
  • Die Pragmas Import und Export ermöglichen es, eine C-Funktion von Ada aufzurufen und umgekehrt, und
  • Der Standard enthält auch Hinweise für Programmierer und Compiler-Entwickler zu Details über die Parameterübergabe in Ada und C.

Bild 1: 
Ausführen von C-Code aus Ada heraus.
Bild 1: 
Ausführen von C-Code aus Ada heraus.
(Bild: Adacore)

Der in Bild 1 gezeigte Code führt beispielhaft auf, wie diese Features dafür genutzt werden können, um eine C-Funktion von Ada aufzurufen. Dieser Code ist portabel, funktioniert also mit allen Ada-Compilern.

Generierung von Schnittstellen-Code

Zusätzlich zu der Unterstützung im Ada-Standard bieten Compiler auch Generatoren von Schnittstellen-Code an. Diese können in der einen Richtung Ada-Schnittstellen von C-Header-Dateien erstellen und sind in der anderen Richtung in der Lage, Ada-Paketspezifikationen auf C-Header abzubilden.

Insbesondere die erste Richtung ist für das Upgrade von Legacy-C-Code nützlich, da das Gerüst der Schnittstelle zwischen Ada und C weitestgehend automatisch generiert werden kann.

Der Ada Wrapper: C-Funktionen importieren

Bild 2: 
Vertragsbasierte Programmierung einer Schnittstelle.
Bild 2: 
Vertragsbasierte Programmierung einer Schnittstelle.
(Bild: Adacore)

Wenn Funktionalität zu einer C-Anwendung hinzugefügt wird, ist es schwierig, sicherzustellen, dass bereits vorhandene Funktionen von dem neuen Code korrekt verwendet werden. Die vertragsbasierte Programmierung von Ada 2012 bietet hier eine Lösung an. Die Ada Funktionsdeklaration, die einer C-Funktion entspricht, kann mit einem Vertrag erweitert werden, der die Vorbedingung der Funktion darstellt. Diese kann durch Dokumentation oder Inspektion des C-Codes ermittelt werden. Bild 2 zeigt ein Beispiel, dass der Prozedur aus Bild 1 auch eine Vorbedingung gibt.

Verträge sind ein Standard-Sprachfeature von Ada. Die Syntax „with Pre“, auch als Aspekt bezeichnet, spezifiziert einen Booleschen Ausdruck, den die Funktion als wahr annimmt. Im in Bild 2 dargestellten Beispiel ist dieser Ausdruck ein Aufruf der Funktion Is_Nul_Terminated, die in Interfaces.C definiert ist und einen Booleschen Wert zurückgibt. Wenn die Laufzeit-Überprüfung von Assertionen eingeschaltet wird, und die Vorbedingung zu false evaluiert wird, dann wird eine Laufzeit-Exception ausgelöst. Mit entsprechender Unterstützung von Tools (zum Beispiel SPARK, der formal analysierbare Teil von Ada) können solche Verträge auch statisch überprüft werden, eine zusätzliche Überprüfung zur Laufzeit ist dann nicht mehr nötig.

Compilerspezifische Unterstützung

Die Laufzeitbibliotheken, die Teil jedes Ada-Compilers sind, nutzen in der Regel existierende C-Funktionen, zum Beispiel für Speichermanagement und Thread-Unterstützung. Daher teilen die beiden Sprachen ein gemeinsames Laufzeitmodell, das die Zusammenarbeit einfacher macht.

Zusätzlich benutzen einige Compiler den gleichen Code-Erzeuger für Ada und C – das GCC- oder LLVM-Backend im Fall von GNAT Pro von AdaCore – was ebenfalls der Zusammenarbeit hilft. Mit dem gleichen Code-Erzeuger (und den gleichen Optimierungen im Backend) haben die beiden Sprachen die gleiche Performance für Code, der die gleiche Funktionalität hat.

Ein Legacy-System zu erneuern ist keine leichte Aufgabe, aber die richtige Sprache und Werkzeuge können sie überschaubar machen. Ada hat viele Vorteile für die Entwicklung von hochsicheren eingebetteten Systemen, und es kann benutzt werden, um Schritt für Schritt neue Funktionalität in eine existierende C-Codebasis einzuführen, ohne den Lebenszyklus der Software in der Organisation zu stören. „C mit einer Prise Ada“ klingt wie ein Trick für die Küche, ist aber ein Erfolgsrezept.

Dieser Beitrag ist erschienen im Sonderheft Embedded Systems Development und Internet of Things I der ELEKTRONIKPRAXIS (Download PDF)

* Benjamin M Brosgol ist Senior Software Engineer bei AdaCore in Boston.

(ID:46301618)