Es folgen einige Beispiele für Kodierungsregeln, mit deren Befolgung sich diverse Arten von Firmware-Bugs reduzieren oder eliminieren lassen.
Regel 1: Geschweifte Klammern
Alle Code-Blöcke – auch als zusammengesetzte Anweisungen bezeichnet –, die auf die Schlüsselwörter if, else, switch, while, do und for folgen, sind in geschweifte Klammen {} zu setzen. Das Gleiche gilt für Einzelanweisungen und leere Anweisungen, die auf diese Schlüsselwörter folgen.
// Also nicht so:
if (timer.done)
// Eine einzelne Anweisung benötigt geschweifte Klammern!
timer.control = TIMER_RESTART;
// Sondern so:
while (!timer.done)
{
// Auch eine leere Anweisung sollte in geschweifte Klammern gesetzt werden.
}
Leere und einzelne Anweisungen, die nicht durch geschweifte Klammern eingeschlossen werden, sind mit erheblichen Risiken behaftet. Code-Konstrukte dieser Art sind oft mit Fehlern in Verbindung zu bringen, wenn benachbarter Code modifiziert oder auskommentiert wird. Durch konsequente Verwendung geschweifter Klammern lassen sich Fehler dieser Art eliminieren.
Regel 2: Das Schlüsselwort „const“
Wann immer möglich, sollte das Schlüsselwort const zum Einsatz kommen, so zum Beispiel
Der Vorteil der möglichst häufigen Verwendung von const besteht in dem vom Compiler durchgesetzten Schutz vor unbeabsichtigten Schreibzugriffen auf Daten, die nur gelesen werden sollten.
Regel 3: Das Schlüsselwort „static“
Das Schlüsselwort static sollte zur Deklaration aller Funktionen und Variablen verwendet werden, die außerhalb des Moduls, in dem sie deklariert wurden, nicht sichtbar sein müssen. Begründung: static hat in der C-Sprache mehrere Bedeutungen. Auf der Modul-Ebene werden damit deklarierte globale Variablen und Funktionen vor dem unbeabsichtigten Zugriff aus anderen Modulen geschützt. Die strenge Verwendung von static reduziert somit die Kopplung und kommt der Kapselung zugute.
Regel 4: Das Schlüsselwort „volatile“
Das Schlüsselwort volatile ist anzuwenden, wo immer es sinnvoll ist, so zum Beispiel
Die sinnvolle Verwendung von volatile eliminiert eine ganze Klasse schwierig aufzudeckender Bugs. Dabei wird der Compiler von Optimierungen abgehalten, die angeforderte Schreib- oder Lesezugriffe auf Variablen oder Register eliminieren würden, die durch eine parallel laufende Instanz jederzeit geändert werden könnten.
Regel 5: Kommentare
Kommentare sind weder zu verschachteln noch zum – auch nur zeitweiligen – Deaktivieren eines Code-Abschnitts zu verwenden. Um einen Codeabschnitt kurzzeitig inaktiv zu machen, sollte das bedingte Kompilierungs-Feature des Präprozessors benutzt werden, zum Beispiel #if 0 ... #endif.
// Also nicht so:
/*
a = a + 1;
/* comment */
b = b + 1;
*/
// Sondern so:
#if 0
a = a + 1;
/* comment */
b = b + 1;
#endif
Verschachtelte Kommentare und auskommentierter Code bergen beide das Risiko, dass Code-Passagen unerwarteterweise in die finale ausführbare Datei kompiliert werden.
Regel 6: Datentypen mit festgelegter Breite
Wann immer es in einem Programm auf die in Bit oder Byte angegebene Breite eines Integer-Wertes ankommt, sollte anstelle von char, short, int, long oder long long ein Datentyp mit festgelegter Breite benutzt werden. Die vorzeichenbehafteten und vorzeichenlosen Datentypen mit festgelegter Breite sind Tabelle 1 zu entnehmen.
Warum? Der ISO-C-Standard lässt für die Datentypen char, short, int, long und long long implementierungsdefinierte Breiten zu, was zu Portierbarkeits-Problemen führt. Der Standard aus dem Jahr 1999 änderte zwar nichts an diesem grundlegenden Aspekt, führte aber die in der Tabelle genannten einheitlichen Typbezeichnungen ein, die in dem neuen Header File <stdint.h> definiert sind. Diese Bezeichnungen sind auch dann zu nutzen, wenn die typedefs manuell erstellt werden müssen.
Regel 7: Bitweise Operatoren
Keiner der bitweisen Operatoren &, |, ~, ^, << und >> darf zum Manipulieren vorzeichenbehafteter Integer-Daten verwendet werden.
// Also nicht so:
int8_t signed_data = -4;
signed_data >>= 1; // nicht unbedingt -2
Der C-Standard spezifiziert nicht das zugrundeliegende Format vorzeichenbehafteter Daten, z.B. Zweierkomplement, und überlässt dem Compiler-Autor die Definition, welche Effekte einige bitweise Operatoren haben.
Regel 8: Vorzeichenbehaftete und vorzeichenlose Integer-Werte
Vorzeichenbehaftete Integer-Werte dürfen in Vergleichen oder Ausdrücken nicht mit vorzeichenlosen Integer-Werten kombiniert werden. Zu diesem Zweck sollten Dezimalkonstanten, die vorzeichenlos sein sollen, mit einem „u“ am Ende deklariert werden.
// Also nicht so:
uint8_t a = 6u;
int8_t b = -9;
if (a + b < 4)
{
// Dieser korrekte Weg sollte ausgeführt werden
// wenn erwartungsgemäß -9 + 6 das Ergebnis -3 < 4 ergäbe.
}
else
{
// In Wirklichkeit wird dieser unkorrekte Weg
// ausgeführt, weil -9 + 6 zu
// (0x100 - 9) + 6 = 253 wird.
}
Mehrere Details der Manipulation binärer Daten in vorzeichenbehafteten Integer-Containern sind Implementierungs-definierte Verhaltensweisen des C-Standards. Darüber hinaus können die Ergebnisse des Vermischens vorzeichenbehafteter und vorzeichenloser Daten zur datenabhängigen Bugs führen.
Regel 9: Parametrisierte Makros oder Inline-Funktionen
Auf parametrisierte Makros ist zu verzichten, wenn die betreffende Aufgabe durch das Schreiben einer Inline-Funktion erledigt werden kann.
// Also nicht so:
#define MAX(A, B) ((A) > (B) ? (A) : (B))
// ... wenn es auch so geht:
inline int max(int a, int b)
Die Verwendung der #defines von Präprozessoren ist mit vielen Risiken behaftet, von denen viele mit dem Erstellen parametrisierter Makros zusammenhängen. Die umfangreiche Verwendung von Klammern wie im Beispiel dargestellt ist zwar wichtig, beseitigt aber nicht die Möglichkeit der unbeabsichtigten doppelten Inkrementierung eines Aufrufs wie MAX(i++, j++). Weitere Risiken der fehlerhaften Verwendung von Makros sind Vergleiche von vorzeichenbehafteten und vorzeichenlosen Daten oder jegliche Tests von Gleitkomma-Daten.
Regel 10: Der Komma-Operator
In Variablen-Deklarationen ist auf die Verwendung des Komma-Operators (,) zu verzichten.
// Also nicht so:
char * x, y; // Sollte y ein Zeiger sein oder nicht?
Jede Deklaration in eine eigene Zeile zu schreiben, verursacht nur wenig Aufwand. Ansonsten ist die Gefahr groß, dass der Compiler oder die für die Programmpflege zuständige Person Ihre Intention falsch versteht.
Konstante Wachsamkeit und Beachtung von Details sind notwendig, um den C-Code fehlerfrei zu halten. Die konsequente Befolgung dieser zehn Regeln kann bei der Entwicklung einer Mentalität helfen, die hierzu beiträgt. Wenn diese Regeln zum Kodierungsstandard hinzugefügt und eingehalten werden, lassen sich potenzielle Bugs aus dem C-Code vermeiden.
Der Autor
Michael Barr |
---|
besitzt Bachelor- und Master-Diplome in Elektrotechnik und lehrte außerdem am Department of Electrical and Computer Engineering der University of Maryland, an der er darüber hinaus ein MBA-Diplom erwarb. Er ist Mitbegründer und Chief Technical Officer der Barr Group. Barr ist international als Experte im Bereich der Embedded-Software-Prozesse und -Architekturen anerkannt und wurde als Sachverständiger zu Gerichtsverfahren in den USA und Kanada hinzugezogen, wenn es um Reverse Engineering, das Abfangen verschlüsselter Signale, Patentverletzungen, den Diebstahl von urheberrechtlich geschütztem Quellcode sowie Produkthaftung ging. |