Forum: PC-Programmierung Overflow bei add unsigned char


You were forwarded to this site from EmbDev.net. Back to EmbDev.net
von Alexander S. (alesi)


Lesenswert?

Hallo,

ich habe gerade wieder etwas gelernt. Im Nachhinein habe ich eine 
Vermutung warum das Ergebnis bei a + b anders ist als bei c.
1
/* add_char_overflow.c  */
2
3
#include <limits.h>
4
#include <stdio.h>
5
6
int
7
main(void)
8
{
9
    unsigned char a, b, c;
10
11
    for (a = 60; a < UCHAR_MAX - 40; a += 20)
12
        for (b = 30; b < UCHAR_MAX - 60; b += 30) {
13
            c = a + b;
14
            printf("%3d %3d %3d %3d\n", a, b, a+b, c);
15
        }
16
17
    return 0;
18
}
19
20
gcc -Wall -pedantic -ansi -o add_char_overflow add_char_overflow.c
1
 60  30  90  90
2
 60  60 120 120
3
 60  90 150 150
4
 60 120 180 180
5
 60 150 210 210
6
 60 180 240 240
7
 80  30 110 110
8
 80  60 140 140
9
 80  90 170 170
10
 80 120 200 200
11
 80 150 230 230
12
 80 180 260   4
13
100  30 130 130
14
100  60 160 160
15
100  90 190 190
16
100 120 220 220
17
100 150 250 250
18
100 180 280  24
19
120  30 150 150
20
120  60 180 180
21
120  90 210 210
22
120 120 240 240
23
120 150 270  14
24
120 180 300  44
25
140  30 170 170
26
140  60 200 200
27
140  90 230 230
28
140 120 260   4
29
140 150 290  34
30
140 180 320  64
31
160  30 190 190
32
160  60 220 220
33
160  90 250 250
34
160 120 280  24
35
160 150 310  54
36
160 180 340  84
37
180  30 210 210
38
180  60 240 240
39
180  90 270  14
40
180 120 300  44
41
180 150 330  74
42
180 180 360 104
43
200  30 230 230
44
200  60 260   4
45
200  90 290  34
46
200 120 320  64
47
200 150 350  94
48
200 180 380 124

von (prx) A. K. (prx)


Lesenswert?

Sollte in jedem C Buch stehen.

von Alexander S. (alesi)


Lesenswert?

Klar ist die vierte Spalte (c). Erst nach kurzem Nachdenken ist mir auch 
die dritte Spalte (a + b) klar.
1
/* add_char_overflow.c  */
2
3
#include <limits.h>
4
#include <stdio.h>
5
6
int
7
main(void)
8
{
9
    unsigned char a, b, c;
10
11
    for (a = 60; a < UCHAR_MAX - 40; a += 20)
12
        for (b = 30; b < UCHAR_MAX - 60; b += 30) {
13
            c = a + b;
14
            printf("%3d %3d %3d %3d %s\n", a, b, a+b, c, (b < UCHAR_MAX - a) ? "Ok" : "Overflow");
15
        }
16
17
    return 0;
18
}
1
 60  30  90  90 Ok
2
 60  60 120 120 Ok
3
 60  90 150 150 Ok
4
 60 120 180 180 Ok
5
 60 150 210 210 Ok
6
 60 180 240 240 Ok
7
 80  30 110 110 Ok
8
 80  60 140 140 Ok
9
 80  90 170 170 Ok
10
 80 120 200 200 Ok
11
 80 150 230 230 Ok
12
 80 180 260   4 Overflow
13
100  30 130 130 Ok
14
100  60 160 160 Ok
15
100  90 190 190 Ok
16
100 120 220 220 Ok
17
100 150 250 250 Ok
18
100 180 280  24 Overflow
19
120  30 150 150 Ok
20
120  60 180 180 Ok
21
120  90 210 210 Ok
22
120 120 240 240 Ok
23
120 150 270  14 Overflow
24
120 180 300  44 Overflow
25
140  30 170 170 Ok
26
140  60 200 200 Ok
27
140  90 230 230 Ok
28
140 120 260   4 Overflow
29
140 150 290  34 Overflow
30
140 180 320  64 Overflow
31
160  30 190 190 Ok
32
160  60 220 220 Ok
33
160  90 250 250 Ok
34
160 120 280  24 Overflow
35
160 150 310  54 Overflow
36
160 180 340  84 Overflow
37
180  30 210 210 Ok
38
180  60 240 240 Ok
39
180  90 270  14 Overflow
40
180 120 300  44 Overflow
41
180 150 330  74 Overflow
42
180 180 360 104 Overflow
43
200  30 230 230 Ok
44
200  60 260   4 Overflow
45
200  90 290  34 Overflow
46
200 120 320  64 Overflow
47
200 150 350  94 Overflow
48
200 180 380 124 Overflow

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Alexander S. schrieb:
> gcc -Wall -pedantic -ansi -o add_char_overflow add_char_overflow.c

Auch wenn's jetzt in dem Fall keine Rolle spielt, solltest du mal deine 
Optionen updaten. -ansi führt dazu, dass der Compiler auf das 35 Jahre 
alte ANSI-C zurückschaltet.

von 900ss (900ss)


Lesenswert?

Rolf M. schrieb:
> -ansi führt dazu, dass der Compiler auf das 35 Jahre alte ANSI-C
> zurückschaltet.

Wo ist da der Vorteil? Die Erklärung hast du offen gelassen.

von Bauform B. (bauformb)


Lesenswert?

900ss schrieb:
> Wo ist da der Vorteil?

Der Compiler würde auch meine Programme mögen ;) mit -ansi geht 
garnichts:
- error: C++ style comments are not allowed in ISO C90
- error: 'for' loop initial declarations are only allowed in C99 or C11 
mode
- warning: ISO C does not support '__FUNCTION__' predefined identifier
- warning: anonymous variadic macros were introduced in C99
- warning: comma at end of enumerator list
usw. Demnächst gibt's noch Binary Constants und falltgrough und noreturn 
und und

C11: https://open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

C23: https://open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

900ss schrieb:
> Rolf M. schrieb:
>> -ansi führt dazu, dass der Compiler auf das 35 Jahre alte ANSI-C
>> zurückschaltet.
>
> Wo ist da der Vorteil? Die Erklärung hast du offen gelassen.

Na dass eben alle Features, die der Sprache in den letzten 35 Jahren 
hinzugefügt wurden, genutzt werden können. "Bauform" hat ja schon ein 
paar davon gelistet. Wobei gcc manche auch mit -ansi fälschlicherweise 
durchlässt, wie z.B. bool oder stdint.h, die es in C89 eigentlich nicht 
gibt.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Alexander S. schrieb:
> Im Nachhinein habe ich eine
> Vermutung warum das Ergebnis bei a + b anders ist als bei c.

Da braucht man nichts zu vermuten.
Alle Berechnungen erfolgen immer so, als wären sie mindestens int. Und 
auch printf(%d) erwartet int, d.h. bei Bedarf erfolgt eine Erweiterung 
auf int.
Allein die Zuweisung auf c schneidet das höherwertige Byte wieder ab.

von 900ss (900ss)


Lesenswert?

Rolf M. schrieb:
> 900ss schrieb:
>> Rolf M. schrieb:
>>> -ansi führt dazu, dass der Compiler auf das 35 Jahre alte ANSI-C
>>> zurückschaltet.
>>
>> Wo ist da der Vorteil? Die Erklärung hast du offen gelassen.
>
> Na dass eben alle Features, die der Sprache in den letzten 35 Jahren
> hinzugefügt wurden, genutzt werden können. "Bauform" hat ja schon ein
> paar davon gelistet. Wobei gcc manche auch mit -ansi fälschlicherweise
> durchlässt, wie z.B. bool oder stdint.h, die es in C89 eigentlich nicht
> gibt.

Ach shit, ich hatte das genau anders verstanden. Ich hatte es erst so 
verstanden dass er -ansi nutzen soll und kratzte mich am Kopf und fragte 
mich... äh? Dann geht ja "nichts" mehr :)
Aber eben habe ich gesehen, dass du eigentlich wolltest, dass er -ansi 
aus seinen angegebenen Optionen streicht. Ich hatte die Option irgendwie 
übersehen.

Bauform hat ja schöne Beispiele gebracht.

Ich nutze mal eine IDE im Job, die -ansi default setze wenn man ein 
Projekt erzeugte. Das war das erste was ich gestrichen habe.

: Bearbeitet durch User
von Alexander S. (alesi)


Lesenswert?

Hallo,

nur der Vollständigkeit halber hier die Version mit 'unsigned int' wo 
für a+b und c immer das gleiche herauskommt.
1
/* overflow_uint.c */
2
3
#include <stdio.h>
4
#include <limits.h>
5
6
int
7
main(void)
8
{
9
    unsigned int a, b, c;
10
11
    printf("UINT_MAX = %u\n", UINT_MAX);
12
13
    for (a = UINT_MAX / 8; a < UINT_MAX - UINT_MAX / 4; a += UINT_MAX / 8)
14
        for (b = UINT_MAX / 8; b < UINT_MAX - UINT_MAX / 4; b += UINT_MAX / 8) {
15
            c = a + b;
16
            printf("%u %u %u %u %s\n", a, b, a + b, c, (b <= UINT_MAX - a) ? "Ok" : "Overflow");
17
        }
18
19
    a = UINT_MAX / 2;
20
    if (UINT_MAX % 2)
21
        b = a + 1;
22
    else
23
        b = a;
24
25
    c = a + b;
26
    printf("%u %u %u %u %s\n", a, b, a + b, c, (b <= UINT_MAX - a) ? "Ok" : "Overflow");
27
28
    return 0;
29
}
1
$ gcc -Wall -pedantic -ansi -o overflow_uint overflow_uint.c
2
3
UINT_MAX = 4294967295
4
536870911 536870911 1073741822 1073741822 Ok
5
536870911 1073741822 1610612733 1610612733 Ok
6
536870911 1610612733 2147483644 2147483644 Ok
7
536870911 2147483644 2684354555 2684354555 Ok
8
536870911 2684354555 3221225466 3221225466 Ok
9
536870911 3221225466 3758096377 3758096377 Ok
10
1073741822 536870911 1610612733 1610612733 Ok
11
1073741822 1073741822 2147483644 2147483644 Ok
12
1073741822 1610612733 2684354555 2684354555 Ok
13
1073741822 2147483644 3221225466 3221225466 Ok
14
1073741822 2684354555 3758096377 3758096377 Ok
15
1073741822 3221225466 4294967288 4294967288 Ok
16
1610612733 536870911 2147483644 2147483644 Ok
17
1610612733 1073741822 2684354555 2684354555 Ok
18
1610612733 1610612733 3221225466 3221225466 Ok
19
1610612733 2147483644 3758096377 3758096377 Ok
20
1610612733 2684354555 4294967288 4294967288 Ok
21
1610612733 3221225466 536870903 536870903 Overflow
22
2147483644 536870911 2684354555 2684354555 Ok
23
2147483644 1073741822 3221225466 3221225466 Ok
24
2147483644 1610612733 3758096377 3758096377 Ok
25
2147483644 2147483644 4294967288 4294967288 Ok
26
2147483644 2684354555 536870903 536870903 Overflow
27
2147483644 3221225466 1073741814 1073741814 Overflow
28
2684354555 536870911 3221225466 3221225466 Ok
29
2684354555 1073741822 3758096377 3758096377 Ok
30
2684354555 1610612733 4294967288 4294967288 Ok
31
2684354555 2147483644 536870903 536870903 Overflow
32
2684354555 2684354555 1073741814 1073741814 Overflow
33
2684354555 3221225466 1610612725 1610612725 Overflow
34
3221225466 536870911 3758096377 3758096377 Ok
35
3221225466 1073741822 4294967288 4294967288 Ok
36
3221225466 1610612733 536870903 536870903 Overflow
37
3221225466 2147483644 1073741814 1073741814 Overflow
38
3221225466 2684354555 1610612725 1610612725 Overflow
39
3221225466 3221225466 2147483636 2147483636 Overflow
40
2147483647 2147483648 4294967295 4294967295 Ok

von Sebastian W. (wangnick)


Lesenswert?

Alexander S. schrieb:
> nur der Vollständigkeit halber

Da fehlt dazwischen aber noch die Version mit uint16_t.

LG, Sebastian

von Alexander S. (alesi)


Lesenswert?

Die Version mit uint16_t und 'unsigned long' darf jeder der möchte als 
Hausaufgabe machen. :-)

von Alexander S. (alesi)


Lesenswert?

Hallo,

einen habe ich noch. Nach einigen Antworten zuvor, ist mir aber klar, 
dass das Ergebnis allen klar ist.
1
#include <stdio.h>
2
#include <limits.h>
3
4
int
5
main(int argc, char *argv[])
6
{
7
    signed char c;
8
9
    c = SCHAR_MIN;
10
    printf("%d %d\n", c, c - 1);
11
    c = SCHAR_MIN;
12
    printf("%d %d\n", c, c - (signed char) 1);
13
    c = SCHAR_MIN;
14
    printf("%d ", c);
15
    --c;
16
    printf("%d\n", c);
17
18
19
    c = SCHAR_MAX;
20
    printf("%d %d\n", c, c + 1);
21
    c = SCHAR_MAX;
22
    printf("%d %d\n", c, c + (signed char) 1);
23
    c = SCHAR_MAX;
24
    printf("%d ", c);
25
    ++c;
26
    printf("%d\n", c);
27
28
    return 0;
29
}
1
$ gcc -Wall -pedantic -ansi -o overflow_signed_char overflow_signed_char.c
2
$ ./overflow_signed_char 
3
-128 -129
4
-128 -129
5
-128 127
6
127 128
7
127 128
8
127 -128

Mir ist das Verhalten in so weit klar, als das bei C das Verhalten bei 
underflow und overflow von signed char oder signed int, im Gegensatz zu 
unsigned char oder unsigned int, nicht definiert ist.

Beitrag #7600790 wurde vom Autor gelöscht.
von Rolf M. (rmagnus)


Lesenswert?

Alexander S. schrieb:
> Mir ist das Verhalten in so weit klar, als das bei C das Verhalten bei
> underflow und overflow von signed char oder signed int, im Gegensatz zu
> unsigned char oder unsigned int, nicht definiert ist.

Bis auf das
> --c;
und das
> ++c;

ist alles wohldefiniert.

von Alexander S. (alesi)


Lesenswert?

Rolf M. schrieb:
> ist alles wohldefiniert.

Nicht für signed int.

https://en.wikipedia.org/wiki/Integer_overflow
Tabelle "Integer overflow handling in various programming languages"
C/C++: Signed integer, undefined behavior

http://cs.yale.edu/homes/aspnes/classes/223/notes.html#overflow-and-the-c-standards

P.S. Im Standard selber habe ich nicht nachgeschaut.

von Rolf M. (rmagnus)


Lesenswert?

Alexander S. schrieb:
> Rolf M. schrieb:
>> ist alles wohldefiniert.
>
> Nicht für signed int.

Richtig, aber der Wertebereich von signed int ist auf jeden Fall groß 
genug, um den von dir verwendeten Zahlenbereich abzudecken, daher gibt 
es hier außer bei ++ und -- kein Problem.
Denn wie oben schon erwähnt wurde:

Peter D. schrieb:
> Alle Berechnungen erfolgen immer so, als wären sie mindestens int.

Das nennt sich "integer promotion". Das heißt, dass hier:

Alexander S. schrieb:
> c - 1

c erst mal auf int erweitert wird und dann die Berechnung mit dem Typ 
durchgeführt wird. Das Ergebnis ist natürlich dann auch vom Typ int. 
Somit kann da nix überlaufen, weil der Wertebereich von int groß genug 
ist, um das Ergebnis aufzunehmen.

Alexander S. schrieb:
> c - (signed char) 1

Hier ist es das gleiche. Dein Cast ändert daran nichts. Es wird durch 
ihn die 1 lediglich erst nach signed char konvertiert, bevor sie dann 
sofort wieder (wie die Variable c) nach int konvertiert wird. Die 
Berechnung erfolgt dann auch wieder mit Typ int.

Lediglich bei --c und ++c gibt  es ein Problem, weil da das Ergebnis ja 
wieder nach c zurückgeschrieben wird, das aber nicht groß genug dafür 
ist.

: Bearbeitet durch User
von Alexander S. (alesi)


Lesenswert?

Dann kannst Du sicher auch erklären, warum
1
signed char c;
2
3
c = c + 1;

und
1
signed char c;
2
3
++c;

unterschiedliche Ergebnisse liefern.

von Rolf M. (rmagnus)


Lesenswert?

Weil c in beiden Fällen uninitialisiert ist und dein Code dadurch 
undefiniertes Verhalten auslöst. Aber ich vermute, du wolltest hier c 
mit SCHAR_MAX initialisieren. Dann hast du aber trotzdem undefiniertes 
Verhalten, weil eben der Wertebereich von c nicht mehr ausreicht.

Laut C-Standard ist ++c äquivalent zu c+=1 ("The expression ++E is 
equivalent to (E+=1), where the value 1 is of the appropriate type.")
und das wiederum ist (mit der Ausnahme dass c nur einmal evaluiert wird) 
äquivalent zu c = c + 1. ("A compound assignment of the form E1 op= E2 
is equivalent to the simple assignment expression E1 = E1 op (E2), 
except that the lvalue E1 is evaluated only once, and with respect to an 
indeterminately sequenced function call, the operation of a compound 
assignment is a single evaluation")

Da das Verhalten in diesem Fall undefiniert ist, muss das Ergebnis aber 
nicht gleich sein.

von Alexander S. (alesi)


Lesenswert?

Hallo Rolf,

der entscheidende Punkt ist wohl, dass man zwischen
1
signed char c;
2
c = SCHAR_MAX;
3
printf("%d", c + 1);

und
1
signed char c;
2
c = SCHAR_MAX;
3
c = c + 1;
4
printf("%d", c);

unterscheiden muss. Im ersten Fall ist der Wertebereich int und im 
zweiten Fall signed char.
1
#include <stdio.h>
2
#include <limits.h>
3
4
int
5
main(int argc, char *argv[])
6
{
7
    signed char c;
8
9
    c = SCHAR_MIN;
10
    printf("%d ", c);
11
    c = c - (signed char) 1;
12
    printf("%d\n", c);
13
    c = SCHAR_MIN;
14
    printf("%d ", c);
15
    --c;
16
    printf("%d\n", c);
17
18
19
    c = SCHAR_MAX;
20
    printf("%d ", c);
21
    c = c + (signed char) 1;
22
    printf("%d\n", c);
23
    c = SCHAR_MAX;
24
    printf("%d ", c);
25
    ++c;
26
    printf("%d\n", c);
27
28
    return 0;
29
}
1
$ ./overflow_signed_char 
2
-128 127
3
-128 127
4
127 -128
5
127 -128

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Alexander S. schrieb:
> der entscheidende Punkt ist wohl, dass man zwischensigned char c;
> c = SCHAR_MAX;
> printf("%d", c + 1);
>
> undsigned char c;
> c = SCHAR_MAX;
> c = c + 1;
> printf("%d", c);
>
> unterscheiden muss. Im ersten Fall ist der Wertebereich int und im
> zweiten Fall signed char.

Ja, weil du im zweiten Fall das Ergebnis nach c zurückschreibst, das vom 
Typ signed char ist, in den es aber nicht reinpasst. Im ersten Fall 
dagegen verarbeitest du das Ergebnis der Addition direkt als int weiter. 
Somit läuft da nichts über. Würdest du schreiben:
1
int d = c + 1:
dann wäre das Ergebnis das gleiche wie im ersten Fall.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.