1 Theorie | |
→ | 1.1 Die Programmiersprache C |
Vorzeichenlose ganzzahlige Datentypen werden im Dualsystem gespeichert.
Für eine Zahl mit n Bits ergibt sich ein Wertebereich
0 ... 2n-1
(z.B. für 32-Bit-Zahlen 0 ... 4294967295).
Die C-Standards legen nicht fest, wie negative Zahlen zu
speichern sind.
Alle mir derzeit bekannten C-Compiler verwenden das
Zweierkomplement. Um eine negative Zahl zu bilden, wird von der
Bitrepräsentation der positiven Zahl ausgegangen. Alle Bits werden
einzeln invertiert. Anschließend wird 1 addiert.
Vereinfachte Methode zur Bildung des Zweierkomplements: Das
am weitesten rechts stehende Bit mit dem Wert 1 wird gesucht.
Dieses Bit und alle evtl. vorhandenen rechts davon stehenden Bits
mit dem Wert 0 behalten ihren Wert. Alle links davon stehenden Bits
werden invertiert.
Beispiel: Es soll -6 als 32-Bit-Zahl dargestellt werden.
Für eine Zahl mit n Bits ergibt sich ein Wertebereich
-2n-1 ... 2n-1-1
(z.B. für 32-Bit-Zahlen -2147483648 ... 2147483647).
In der Header-Datei limits.h werden Minimal- und Maximalwerte für Ganzzahltypen definiert:
Datentyp | Minimalwert | Maximalwert |
---|---|---|
signed char | SCHAR_MIN | SCHAR_MAX |
unsigned char | UCHAR_MAX | |
short | SHRT_MIN | SHRT_MAX |
unsigned short | USHRT_MAX | |
int | INT_MIN | INT_MAX |
unsigned int | UINT_MAX | |
long | LONG_MIN | LONG_MAX |
unsigned long | ULONG_MAX |
Aus verschiedenen Gründen ergab sich die Notwendigkeit, auch größere ganze Zahlen als 32-Bit-Zahlen darzustellen, insbesondere für Dateigrößen und Zeitstempel.
Zunächst wurden hierfür weitere Datentypen eingeführt, wie
"long long" und "unsigned long long", diese sind auf
den mir bekannten System 64 Bit breit.
In der Funktion printf() müssen als Platzhalter "%lld" und
"%llu" eingesetzt werden.
Die Grenzen der Wertebereiche werden durch LLONG_MIN und LLONG_MAX
(long long) bzw. ULLONG_MAX (unsigned long long) angegeben.
Um für weitere Entwicklungen gerüstet zu sein, definieren POSIX-System verschiedene ganzzahlige Datentypen, z.B.:
Diese Datentypen sind immer passend für das jeweilige System konfiguriert. Hat ein System beispielsweise nur einen 32-Bit-Prozessor, das Dateisystem unterstützt aber Dateigrößen oberhalb von 4294967295 Bytes, so ist off_t ein 64-Bit-Datentyp.
Um beispielsweise Informationen über eine Datei zu erlangen, kann man die Funktion stat() aufrufen, diese füllt eine Struktur vom Typ struct stat, welche die Daten in den o.g. Datentypen enthält.
Unter Windows entschied man sich für eine andere Vorgehensweise.
Hier gibt es Datenstrukturen struct _stat, __stat32,
__stat64, __stati64, __stat32i64,
__stat64i32 und zugehörige Funktionen _stat(),
_stat32(), _stat64(), _stati64(),
_stat32i64(), _stat64i32().
Diese Strukturen verwenden nicht generische Datentypen wie off_t
und time_t, die bei Bedarf angepasst werden können, sondern legen
explizit 32- und 64-Bit-Typen fest.
Dabei sind für Zeitstempel und Dateigröße jeweils einmal 64 Bit und
32 Bit vorgesehen.
In struct _stat, welcher mit der Funktion
_stat() befüllt wird, haben zwar Zeitangaben eine Größe von
64 Bit, die Dateigröße aber nur 32 Bit. Daher empfehle ich die
Verwendung von struct __stat64 mit der Funktion
_stat64().
Für bestimmte Aufgaben muss die Zuordnung von uid_t ...
size_t bzw. Zeitstempeln oder Dateigrößen zu Datentypen
fester Größe bekannt sein bzw. es muss eine Konvertierung in einen
Datentyp fester Größe vorgenommen werden.
Ein Beispiel hierfür ist die Ausgabe mit printf(), da hier
der zu verwendende Platzhalter vom Datentyp abhängt.
Die Datentypen intmax_t und uintmax_t werden in der
Header-Datei stdint.h definiert und verfügen über eine solche
Bitbreite, dass alle vorzeichenbehafteten Ganzzahlen verlustfrei
nach intmax_t konvertiert werden können und alle
vorzeichenlosen Ganzzahlen verlustfrei nach uintmax_t
konvertiert werden können.
Bei der Verwendung von printf() auf POSIX-Systemen wird der
Platzhalter "%jd" bzw. "%ji" für intmax_t verwendet. Für uintmax_t
wird "%ju" benutzt.
Unter Windows muss der Platzhalter "%I64d" bzw. "%I64u" benutzt
werden. Laut CERT C Coding Standard soll dies zwar nur bis
einschließlich Visual Studio 2012 notwendig sein, die Online-Hilfe
zu printf-Format-Strings für Visual Studio 2013 listet aber auch
(noch) kein j-Flag.
Die Grenzen des Wertebereiches werden durch INTMAX_MIN und
INTMAX_MAX (intmax_t) bzw. UINTMAX_MAX (uintmax_t) angegeben.
Für den Umgang mit intmax_t und uintmax_t und den zugehörigen Minimal- bzw. Maximalwerten sollten mit
#include <limits.h> #include <stdint.h>
die benötigten Header-Dateien limits.h und stdint.h eingebunden werden.
Entstehen bei mathematischen Operationen mit n-Bit-Zahlen
Ergebnisse, die nicht mehr mit n Bits darstellbar sind,
werden nur die letzten n Bits verwendet, die vorderen Bits
werden abgeschnitten. Dies verfälscht natürlich das Ergebnis.
Dieser Vorgang wird als Überlauf bezeichnet.
Sie sollten sicherstellen, einen Überlauf zu erkennen, da in diesem
Fall das Berechnungsergebnis falsch ist.
Obwohl die Ganzzahl-Datentypen in C 16, 32 oder 64 Bits breit
sind, wird hier zur Demonstration mit vorzeichenlosen 4-Bit-Zahlen
gearbeitet.
Bei der Addition von "1111" + "1111" (dezimal 15+15) erhalten wir
"11110" (dezimal 30). Da wir aber mit 4-Bit-Zahlen arbeiten, werden
nur die letzten 4 Bits "1110" (dezimal 14) im Prozessorregister
gespeichert bzw. für weitere Berechnungen verwendet. Bei der
Addition können Ergebnisse entstehen, die ein Bit größer sind als
die Summanden.
Die Multiplikation von "1111" * "1111" (dezimal 15*15) ergibt
ergibt sich "11100001" (dezimal 225). Da wir mit 4-Bit-Zahlen
arbeiten, werden nur die letzten 4 Bits "0001" (dezimal 1) im
Prozessorregister gespeichert bzw. für weitere Berechnungen
verwendet.
Bei der Überlauf-Erkennung müssen die Tests so ausgeführt
werden, dass dabei nicht seinerseits wieder ein Überlauf auftreten
kann.
Um einen Überlauf bei einer Addition festzustellen, kann man nicht
einfach
if (a + b > MAX) { /* Ueberlauf */ }
(mit MAX als Maximalwert für die jeweilige Bitbreite) verwenden,
da hier im Fall eines Überlaufs das Ergebnis von "a + b"
auf die Bitbreite des Datentypes beschnitten wird, womit der
Maximalwert nie überschritten wird und der Überlauf nicht
festgestellt wird.
Für den Überlauf-Test müssen die Ungleichungen so umgestellt
werden, dass in den Berechnungen des Überlauf-Tests kein Überlauf
auftreten kann, z.B.:
if (a > MAX - b) { /* Ueberlauf */ }
Ganzzahlige mathematische Operationen werden direkt in der ALU
des Prozessors ausgeführt.
Eine Division durch 0 ist ein so grober Fehler, dass er im
Normalfall den sofortigen Programmabbruch nach sich zieht.
Dies ist für den Nutzer der Software äußerst unerfreulich.
Daher müssen Sie vor jeder ganzzahligen Division und
Divisions-Rest-Berechnung sicherstellen, dass der Nenner ungleich 0
ist.
Wir verwenden hier MAX für den Maximalwert 2n-1.
Die Summe kann größer werden als der Maximalwert.
Wenn MAX - a < b, tritt ein Überlauf auf.
Wenn a < b, tritt ein Überlauf auf.
Falls a und b ungleich 0 sind, muss getestet werden, ob das
Produkt größer ist als der Maximalwert.
Wenn a > MAX / b, tritt ein Überlauf auf.
Hier kann kein Überlauf auftreten. Wenn b gleich 0 ist, darf die Operation nicht ausgeführt werden.
Wir verwenden hier MAX für den Maximalwert 2n-1-1 und
MIN für den Minimalwert -2n-1.
Die Werte sind asymmetrisch, d.h. MIN ist betragsmäßig um 1 größer
als MAX.
Wenn sowohl a als auch b ungleich 0 sind, muss auf Überlauf getestet werden.
Wenn b gleich 0 ist, darf die Operation nicht ausgeführt werden. Andernfalls muss die nachfolgende Überlauf-Prüfung vorgenommen werden:
Wird eine vorzeichenbehaftete Zahl s mit einer vorzeichenlosen Zahl u verglichen, kann nicht einfach eine Typkonvertierung einer der beiden Zahlen mit anschließendem Vergleich vorgenommen werden.
Vielmehr ist folgendermaßen vorzugehen:
Die Funktionen in den Dateien he-long.h und he-long.c bzw. he-ulong.h und
he-ulong.c
demonstrieren mathematische Operationen mit long- bzw.
unsigned-long-Werten. Dabei werden Überläufe erkannt und Divisionen
durch 0 vermieden.
Für Datentypen anderer Größen (short, int, long long, intmax_t)
können die Verfahren analog angewandt werden.
Anstelle von
y = 3L * x + 2L;
kann man nun
int ec = HE_ERROR_NONE; y = hsm_et_long_add( hsm_et_long_mul(3L, x, &ec), 2L, &ec); if (HE_ERROR_NONE == ec) { /* Ergebnis benutzbar. */ } else { /* Fehler aufgetreten. Bei Bedarf kann Art des Fehlers ermittelt werden. */ switch (ec) { case HE_ERROR_MATH_OVERFLOW: { } break; case HE_ERROR_MATH_DIVZERO: { } break; } }
verwenden.
Die gezeigten Funktionen erwarten als drittes Argument einen
Zeiger auf eine Fehlercode-Variable. Diese muss vor Beginn der
Berechnung auf "kein Fehler aufgetreten" gesetzt werden.
Tritt in den Berechnungsfunktionen ein Fehler auf, wird der erste
Fehler in der Fehlercode-Variable vermerkt. Tritt kein Fehler auf,
wird die Fehlercode-Variable unverändert belassen.
Enthält die Fehlercode-Variable nach Abschluss der Berechnung noch
immer den Kennwert für "kein Fehler aufgetreten", kann das
Berechnungsergebnis benutzt werden.
Quelle: CERT C Coding Standard 〈1〉
const unsigned long alle_bits_gesetzt = 0xFFFFFFFFUL;Richtig:
const unsigned long alle_bits_gesetzt = -1;Die erste (falsche) Variante funktioniert nur, wenn unsigned long 32 Bits benutzt. Die zweite Variante funktioniert auch für andere Bitbreiten.
const unsigned long alle_bits_gesetzt = ULONG_MAX;
1 | http://www.securecoding.cert.org |