C programmieren: 10 Codierungsregeln für sicherheitskritischen Code

| Autor / Redakteur: Prof. Dr. Christian Siemers * / Sebastian Gerstl

Ob Anfänger oder Fortgeschrittener in der C-Programmierung: Für saubere, sicherheitskritische Software empfiehlt es sich stets, 10 Codierungsregeln im Kopf zu behalten.
Ob Anfänger oder Fortgeschrittener in der C-Programmierung: Für saubere, sicherheitskritische Software empfiehlt es sich stets, 10 Codierungsregeln im Kopf zu behalten. (Bild: Fabian Grohs - Unsplash.com)

Wie geht man beim Programmieren im C am geschicktesten vor, um stabilen, leicht testbaren und vor allem sicheren Code zu erhalten? Hier sind 10 grundsätzliche Codierungsregeln, die sich Entwickler auf dem Weg zu sicherer Software verinnerlichen sollten.

In der Programmierung mit C haben sich einige grundlegende Regeln etabliert, die gerade für Softwareentwicklung in sicherheitskritischen Bereichen gelten und anerkannt sind. Über Codierungsregeln kann man sich natürlich sehr ausführlich auslassen – nahezu jede Entwicklungsgruppe, die etwas auf sich hält, hat mindestens ein Regelwerk, das auch sehr umfänglich sein kann.

Die hier vorgestellten Regeln hat unter anderem Gerad J. Holzmann unter dem Titel ”The Power of 10: Rules for Developing Safety-Critical Code“ popularisiert. Diese zehn Elemente stellen ein übersichtliches Regelwerk dar.

Codierungsregel 1: Nur einfache Kontrollstrukturen verwenden

Im gesamten Code sollen nur einfache Kontrollflusskonstrukte verwendet werden. Insbesondere sollen goto, direkte oder indirekte Rekursion vermieden werden.

Dies resultiert insbesondere in einer erhöhten Klarheit im Code, der leichter zu analysieren und zu beurteilen ist. Die Vermeidung von Rekursion resultiert in azyklische Codegraphen, die wesentlich einfacher bezüglich Stackgröße und Ausführungszeit analysiert werden können.

Die Regel kann noch dadurch verschärft werden, dass pro Funktion nur ein einziger Rücksprung erlaubt ist.

Codierungsregel 2: Obergrenzen für Schleifen festlegen

Alle Schleifen müssen eine Konstante als obere Grenze haben. Es muss für Code-Check-Tools einfach möglich sein, die Anzahl der durchlaufenen Schleifen anhand einer Obergrenze statisch bestimmen zu können.

Diese Regel dient dazu, unbegrenzte Schleifen zu verhindern. Hierbei müssen auch implizit unbegrenzte Schleifen wie das folgende Beispiel verhindert werden:

int k, m, array[1024];

for( k = 0, m = 0; k < 10; k++, m++ )
{
    if( 0 == array[m] )
        k = 0;
}

Die wichtige Regel ist also diejenige, dass der Code-Checker die Obergrenze erkennen können muss.

Es gibt allerdings eine Ausnahme von dieser Regel: Es gibt immer wieder explizit unendlich oft durchlaufene Schleifen (etwa: while(1)), die für bestimmte Aufgaben notwendig sind (Process Scheduler, Rahmen für endlos laufendes Programm etc.). Diese sind selbstverständlich erlaubt.

Eine Möglichkeit, diese Regel zu erfüllen und bei Überschreiten dieser oberen Grenze einen Fehler bzw. eine Fehlerbehebung einzuführen, sind so genannte assert()-Funktionen (siehe auch Hardwarebeschreibungssprachen wie VHDL). Bei Überschreiten wird eine solche Funktion aufgerufen, diese kann dann entsprechende Aktionen einleiten. Es ist zwar möglich, die Fehlerbehebung auch in den eigentlichen Sourcecode einzubauen, die explizite Herausführung dient aber der Übersicht.

Codierungsregel 3: Speicherallokation beachten

Nach einer Initialisierungsphase soll keine dynamische Speicherallokation mehr erfolgen.

Die Allokationsfunktionen wie malloc() und die Freigabe (free()) sowie die Garbage Collection zeigen oftmals unvorhersagbare Verhaltensweisen, daher sollte hiervon im eigentlichen Betrieb Abstand genommen werden. Zudem stellt die dynamische Speicherverwaltung im Programm eine hervorragende Fehlerquelle dar bezüglich Speichernutzung nach Rückgabe, Speicherbereichsüberschreitung etc.

Codierungsregel 4: Keine Funktion soll mehr als 60 Zeilen haben

Keine Funktion soll mehr als 60 Zeilen haben, d.h. bei einer Zeile pro Statement und pro Deklaration soll die Funktion auf einer Seite ausgedruckt werden können.

Diese Regel dient einfach der Lesbarkeit des Codes. Das klingt banal, hilft aber massiv dabei, die Übersicht über den geschriebenen Code zu behalten – und so möglicher Sicherheitslücken im Zweifelsfall schnell finden und stopfen zu können.

Codierungsregel 5: assert()-Funktionen und Assertionsdichte im Auge behalten

Die Dichte an Assertions soll im Durchschnitt mindestens 2 pro Funktion betragen. Hierdurch sollen alle besonderen Situationen, die im Betrieb nicht auftauchen dürfen, abgefangen werden. Die Assertions müssen seiteneffektfrei sein und sollen als Boolesche Tests definiert werden. Die assert()-Funktionen selbst, die bei fehlgeschlagenen Tests aufgerufen werden, müssen die Situation explizit bereinigen und z.B. einen Fehlercode produzieren bzw. zurückgeben.

Untersuchungen zeigen, dass Code mit derartigen Assertions, die z.B. Vor- und Nachbedingungen von Funktionen, Werten, Rückgabewerten usw. testen, sehr defensiv arbeitet und einer raschen Fehlerfindung im Test dient. Die Freiheit von Seiteneffekten lässt es dabei zu, dass der Code bei Performance-kritischen Abschnitten später auskommentiert werden kann.

Codierungsregel 6: Datenobjekte im kleinstmöglichen Gültigkeitsbereich deklarieren

Alle Datenobjekte müssen im kleinstmöglichen Gültigkeitsbereich deklariert werden.

Dies ist das Prinzip des Versteckens der Daten, um keine Änderung aus anderen Bereichen zu ermöglichen. Es dient sowohl zur Laufzeit als auch zur Testzeit dazu, den Code möglichst einfach und verständlich zu halten.

Codierungsregel 7: Funktionen prüfen und gegenprüfen

Jede aufrufende Funktion muss den Rückgabewert einer aufgerufenen Funktion checken (falls dieser vorhanden ist), und jede aufgerufene Funktion muss alle Aufrufparameter auf ihren Gültigkeitsbereich testen.

Diese Regel gehört wahrscheinlich zu den am meisten verletzten Regeln. Aber der Test z.B. darauf, ob die aufgerufene Funktion erfolgreich war oder nicht, ist mit Sicherheit sinnvoll. Sollte es dennoch sinnvoll erscheinen, den Rückgabewert als irrelevant zu betrachten, dann muss dies kommentiert werden.

Codierungsregel 8: Nutzung des Präprozessors einschränken

Die Nutzung des Präprozessors muss auf die Inkludierung der Headerfiles sowie einfache Makrodefinitionen beschränkt werden. Komplexe Definitionen wie variable Argumentlisten, rekursive Makrodefinitionen usw. sind verboten. Bedingte Compilierung soll auf ein Minimum beschränkt sein.

Der Präprozessor kann – leider – so genutzt werden, dass er sehr zur Verwirrung von Softwareentwicklung und Code-Checker beitragen kann. Daher ist eine Begrenzung sinnvoll. Die Anzahl der Versionen, die man mittels bedingter Compilierung und entsprechend vielen Compilerswitches erzeugen kann, wächst exponentiell: Bei 10 Compiler-Switches erhält man bereits 210 = 1024 verschiedene Versionen, die alle getestet werden müssen.

Codierungsregel 9: Pointer-Nutzung begrenzen, aber nicht verschleiern

Die Nutzung von Pointern muss auf ein Minimum begrenzt sein. Grundsätzlich ist nur ein Level von Dereferenzierung zulässig. Pointer dürfen nicht durch Makros oder typedef verschleiert werden. Pointer zu Funktionen sind verboten.

Die Einschränkung bei Zeigern dürfte allgemein verständlich sein. Iinsbesondere aber soll die Arbeit von Code-Checkern nicht behindert werden.

Codierungsregel 10: Code-Checking und Warnstufen sind essentiell

Der gesamte Code muss vom ersten Tag an so compiliert werden, dass die höchste Warnstufe mit allen Warnungen zugelassen eingeschaltet ist. Der Code muss ohne Warnungen compilieren. Der Code muss täglich gecheckt werden, möglichst mit mehr als einem Codeanalysator, und dies mit 0 Warnungen.

Diese Regel sollte peinlichst beachtet werden, denn Warnungen bedeuten immer etwas. Sollte die Warnung als verkehrt identifiziert werden, muss der Code umgeschrieben werden, denn dies kann auch bedeuten, dass der Code-Checker den Teil nicht versteht.

Als ein gut geeigneter Code-Checker empfehlen sich übrigens: Lint bzw. der Ableger splint (Secure Programming Lint).

Dieser Beitrag findet sich im Handbuch „Embedded Systems Engineering“ im Kapitel „Einführung in die Sprache C“. Das Handbuch ist als kostenlose PDF-Version in voller Länge auf ELEKTRONIKPRAXIS.de verfügbar.

* Prof. Dr. Christian Siemers lehrt an der Technische Universität Clausthal und arbeitet dort am Institut für Elektrische Informationstechnik (IEI).

Kommentare werden geladen....

Kommentar zu diesem Artikel

Der Kommentar wird durch einen Redakteur geprüft und in Kürze freigeschaltet.

Anonym mitdiskutieren oder einloggen Anmelden

Avatar
Zur Wahrung unserer Interessen speichern wir zusätzlich zu den o.g. Informationen die IP-Adresse. Dies dient ausschließlich dem Zweck, dass Sie als Urheber des Kommentars identifiziert werden können. Rechtliche Grundlage ist die Wahrung berechtigter Interessen gem. Art 6 Abs 1 lit. f) DSGVO.
  1. Avatar
    Avatar
    Bearbeitet von am
    Bearbeitet von am
    1. Avatar
      Avatar
      Bearbeitet von am
      Bearbeitet von am

Kommentare werden geladen....

Kommentar melden

Melden Sie diesen Kommentar, wenn dieser nicht den Richtlinien entspricht.

Kommentar Freigeben

Der untenstehende Text wird an den Kommentator gesendet, falls dieser eine Email-hinterlegt hat.

Freigabe entfernen

Der untenstehende Text wird an den Kommentator gesendet, falls dieser eine Email-hinterlegt hat.

copyright

Dieser Beitrag ist urheberrechtlich geschützt. Sie wollen ihn für Ihre Zwecke verwenden? Infos finden Sie unter www.mycontentfactory.de (ID: 45524305 / IDEs & Programmiersprachen)