Vorzeichen oder nicht - das ist die C-Frage
Beim Erstellen eines Programms, das Daten aus einem Puffer kratzt, stolperte ich über einen sehr seltsamen Fehler in C. Es kostet mich ernstlich Mühe, das Problem auch nur zu verstehen.
Das Programm
Zuerst habe ich das Problem eingedampft auf ein minimales Beispiel:
#include <stdio.h> int main(void) { int len, step; char data[5]; for(len = 27; sizeof(data) < len; len -= step) { step = sizeof(data) + 2; printf("len = %3d, taking %d away.\n", len, step); } }
Was tut das Programm?
- Es gibt einen Puffer mit einer initialen Länge von 27 Byte.
- Passt die Struktur nicht mehr in den Restpuffer, brich ab.
- Solange die gesuchte Struktur aber noch rein passt, verarbeite sie (fehlt im Beispiel).
- Nach der Verarbeitung verkleinere die Rest-Datenmenge um die verarbeitete Struktur plus ein passendes Alignment (hier fest 2).
Der benutzte Puffer, das Verschieben des Arbeitszeigers und die eigentliche Datenverarbeitung sind in dem Beispiel entfallen.
Überraschung
Und hier die Ausgabe:
len = 27, taking 7 away. len = 20, taking 7 away. len = 13, taking 7 away. len = 6, taking 7 away. len = -1, taking 7 away. len = -8, taking 7 away. len = -15, taking 7 away. len = -22, taking 7 away. len = -29, taking 7 away. len = -36, taking 7 away.
Wow! Warum beendet sich das Programm denn nicht?
Die Größe der Struktur ist "5". Das ist ganz bestimmt nicht kleiner als "-1", oder etwa doch?
Ganz offensichtlich ist der Vergleich "5" ist kleiner als "-1" wahr, aber warum?
Evtl. hab' ich ja irgendwelche Warnungen des Compilers übersehen?
$ cc -O2 -Wall -o t t.c [ ... nichts ... ] $ cc --version FreeBSD clang version 3.9.1 (tags/RELEASE_391/final 289601) (based on LLVM 3.9.1) Target: x86_64-unknown-freebsd11.0 Thread model: posix InstalledDir: /usr/bin
Nein, auch nicht.
Erklärung
Wenn der C-Compiler den Wert des "signed int" mit dem Namen "len" in einen "unsigned int" verwandeln würde, dann wäre das Ergebnis erklärt. Der Vergleichwert wäre dann also bei mehr als 4 Mrd., also deutlich größer als die Länge der Struktur.
Aber warum sollt der Compiler eine solche Umwandlung durchführen?
Es bleibt nur ein Blick in die Norm:
- In 6.5.8 "Relational operators" heißt es: If both of the operands have arithmetic type, the usual arithmetic conversions are performed.
- In 6.3.1.8 "Usual arithmetic conversions" heißt es: If the operand that has unsigned integer type has rank greater or equal to the rank of the type of the other operand, then the operand with signed integer type is converted to the type of the operand with unsigned integer type.
- In 6.3.1.1 "Boolean, characters, and integers" heißt es: The rank of any unsigned integer type shall equal the rank of the corresponding signed integer type.
Für den Vergleich zwischen zwei normalen Integer-Typen – der eine mit, der andere ohne Vorzeichen – gilt also, dass beide Operatoren nach unsigned konvertiert werden: In diesem Fall also von -1 auf 4 Mrd.
Voraussetzung für diese Interpretation ist allerdings, dass der sizeof-Operator einen vorzeichenlosen Integer-Typ liefert. Also wieder die Norm lesen:
- In 6.5.3.4 "The sizeof operator" heißt es: The result is an integer constant. Huch? Keine Aussage zu Vorzeichen?
- Etwas später liest man: The value of the result is implementation-defined, and its type (an unsigned integer type) is size_t.
Man müsste also das Spiel gewinnen, wenn man die Variable "len" einen größeren Rank verpasst. Leider klappt das weder mit "long" noch mit "long long".
Erst eine beherzter Cast des sizeof-Operators nach "int" löst das Problem:
#include <stdio.h> int main(void) { int len, step; char data[5]; for(len = 27; (int)sizeof(data) < len; len -= step) { step = sizeof(data) + 2; printf("len = %3d, taking %d away.\n", len, step); } }
Und das ergibt:
len = 27, taking 7 away. len = 20, taking 7 away. len = 13, taking 7 away. len = 6, taking 7 away.
Ende der Geschichte.
Wer mag, kann mal herausbekommen, ob der Umgang des Compilers im Falle von "long" und "long long" standardkonform ist. Ich habe da meine Zweifel.
ersten Beispiel probiert:
#include <stdio.h>
int main(void) {
int len, step, size;
char data[5];
size = sizeof(data); // Hier!!
for(len = 27; size < len; len -= step) {
step = size + 2;
printf("len = %3d, taking %d away.\n", len, step);
}
}
und siehe da:
len = 27, taking 7 away.
len = 20, taking 7 away.
len = 13, taking 7 away.
len = 6, taking 7 away.
Gruesse, JPL
laut gcc manual (https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Warning-Options.html#Warning-Options):
-W = -Wextra
und -Wextra ist eine andere Warnungsgruppe als -Wall.
Das heißt wenn man tatsächlich auf der sicheren Seite sein will, müsste man immer beides angeben.
t.c:7:31: warning: comparison of integers of different signs: 'unsigned long' and 'int' [-Wsign-compare]
for(len = 27; sizeof(data) < len; len -= step) {
~~~~~~~~~~~~ ^ ~~~
1 warning generated.
Merke: -W != -Wall
4 Kommentare