Forum: Compiler & IDEs C/C++: Frage zu (un)signed overflow


von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

Hallo Leute,

Ich habe da eine Frage zum Thema Overflow von signed/unsigned 
Datentypen.

Folgendes Beispiel:
1
#include <iostream>
2
#include <string>
3
#include <cstdint>
4
5
6
using namespace std;
7
8
9
int main()
10
{
11
    string str;
12
    cin >> str;
13
14
    size_t len = str.length();
15
    uint16_t i;
16
17
    for (i = 0; i < len; i++) {
18
        cout << "running loop... " << i << endl;
19
    }
20
21
    cout << "after loop... " << i << endl;
22
    cout << "len was   ... " << len << endl;
23
24
    return 0;
25
}
Compileraufruf (GCC 7.2.0):
1
g++ -Wall -Wextra -o main overflow_test.cpp
Programmaufruf:
1
./main < datei_mit_70k_zeichen.txt
Folgendes Verhalten:
Fuettere ich das Programm mit einem String, der laenger ist als der 
Schleifenzaehler i fassen kann (in diesem Beispiel mit einem 70.000 Byte 
langen String), so ergibt sich eine Endlosschleife.
Soweit so logisch und auch erwartet.

Aendere ich den Datentyp von i von uint16_t zu int16_t, also von 
unsigned zu signed, so bricht die Schleife ab, sobald i ueberlaeuft.
1
...
2
running loop... 32764
3
running loop... 32765
4
running loop... 32766
5
running loop... 32767
6
after loop... -32768
7
len was   ... 70000
Und eben dieses Verhalten verstehe ich nicht. Ich habe erwartet, dass 
beide Varianten (signed und unsigned) eine Endlosschleife produzieren. 
Das ein signed Overflow undefiniertes Verhalten ist, weiss ich. Aber 
liegt das daran?

Fuer mich ist i, egal ob signed oder unsigned, immer kleiner als 70.000.
Kann mir jemand erklaeren, was da genau passiert?

Gruesse

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Kaj G. schrieb:
> Fuettere ich das Programm mit einem String, der laenger ist als der
> Schleifenzaehler i fassen kann (in diesem Beispiel mit einem 70.000 Byte
> langen String), so ergibt sich eine Endlosschleife.
> Soweit so logisch und auch erwartet.
>
> Aendere ich den Datentyp von i von uint16_t zu int16_t, also von
> unsigned zu signed, so bricht die Schleife ab, sobald i ueberlaeuft.

Signed-Overflow ist Undefined Behaviour (UB) in C / C++, du kannst also 
nicht von einem bestimmten Verhalten wie wrap-around ausgehen — auch 
wenn das für diese Übersetzung so sein sollte.  Bereits mit aktivierter 
Optimierung darf sich und kann sich ein Compiler anders (bzw. genauso, 
nämlich immer noch UB) verhalten.

Wenn du für Signed-Overflow definiertes Verhalten willst, dann verwende 
-fwrapv:

https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#index-fwrapv

von Arno (Gast)


Lesenswert?

Kaj G. schrieb:
> Fuer mich ist i, egal ob signed oder unsigned, immer kleiner als 70.000.
> Kann mir jemand erklaeren, was da genau passiert?

Ist ja bald Spekulatiuszeit: Vielleicht wird (int16_t) i = -32768 in 
size_t gewandelt, bevor es mit len verglichen wird (oder beides auf 
uint32_t) und dabei größer als 70000.

MfG, Arno

von Mampf F. (mampf) Benutzerseite


Lesenswert?

Hmm, size_t ist normalerweise unsigned und int16_t ist signed ... Mein 
Compiler warnt mich immer vor signed mit unsigned Vergleichen (oder 
andersherum)

Ansonsten ist -32768 der Korrekte Wert, wenn man 16Bit signed 32767 
inkrementiert (0x7fff + 1 = 0x8000. Umgerechnet in Dezimal über's 
Zweierkomplement 0x8000 => 0x3fff + 1 = 32768 aber Minus).

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Mampf F. schrieb:
> Ansonsten ist -32768 der Korrekte Wert, wenn man 16Bit signed 32767
> inkrementiert.

Nicht in C(++): Da dürfen signed in diesem Zusammenhang wie Ganze Zahlen 
behandelt werden:  Wenn du auf 0, egal wie oft, 1 addierst, wirst du nie 
-1 oder einen anderen negativen Wert erhalten.

von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

Mampf F. schrieb:
> Mein
> Compiler warnt mich immer vor signed mit unsigned Vergleichen (oder
> andersherum)
Ja, macht meiner auch. Darum geht es hier ja aber auch gar nicht.

Mampf F. schrieb:
> Ansonsten ist -32768 der Korrekte Wert, wenn man 16Bit signed 32767
> inkrementiert (0x7fff + 1 = 0x8000. Umgerechnet in Dezimal über's
> Zweierkomplement 0x8000 => 0x3fff + 1 = 32768 aber Minus).
Das habe ich ja auch nie bestritten. -32768 ist aber kleiner als 70000. 
Deswegen habe ich erwarten, dass es wie bei unsigned eine Endlosschleife 
gibt, was nicht der Fall ist.

Arno schrieb:
> Ist ja bald Spekulatiuszeit: Vielleicht wird (int16_t) i = -32768 in
> size_t gewandelt, bevor es mit len verglichen wird (oder beides auf
> uint32_t) und dabei größer als 70000.
Irgendwie sowas denke ich mir auch, aber kann man das irgendwie 
feststellen, ob genau das passiert? Muesste das nicht ueber den 
Assembleroutput feststellbar sein?

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Kaj G. schrieb:
> Deswegen habe ich erwarten

Was hast du denn an dem Wort „undefined“ im „undefined behavior“
nicht verstanden?

von Mampf F. (mampf) Benutzerseite


Lesenswert?

Johann L. schrieb:
> Nicht in C(++): Da dürfen signed in diesem Zusammenhang wie Ganze Zahlen
> behandelt werden:  Wenn du auf 0, egal wie oft, 1 addierst, wirst du nie
> -1 oder einen anderen negativen Wert erhalten.

Dann hält sich C(++) nicht an die eigene Spezifikation?

Oder der GCC ist verbuggt?

Schön, falls es anderes Definiert ist - das bringt aber nichts, wenn 
sich kein Compiler daran hält - dann bringt es auch nichts, jemanden 
darüber aufzuklären, wenn die Realität ganz anders aussieht ;-)

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Mampf F. schrieb:
> Dann hält sich C(++) nicht an die eigene Spezifikation?

Inwiefern?  Hast du ein Zitat?

von (prx) A. K. (prx)


Lesenswert?

Mampf F. schrieb:
>> Nicht in C(++): Da dürfen signed in diesem Zusammenhang wie Ganze Zahlen
>> behandelt werden:  Wenn du auf 0, egal wie oft, 1 addierst, wirst du nie
>> -1 oder einen anderen negativen Wert erhalten.
>
> Dann hält sich C(++) nicht an die eigene Spezifikation?

Obacht: Johann schrieb "dürfen", nicht "müssen"! Er darf das so halten, 
muss aber nicht. Die Spezifikation besagt "undefiniert", wenn mit 
Vorzeichen.

> sich kein Compiler daran hält

Er hält sich genau an die Spezifikation, die besagt, dass er bei einem 
Überlauf mit Vorzeichen tun darf was er will.

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Mampf F. schrieb:
> Oder der GCC ist verbuggt?

Das ist immer die erste Mutmaßung, wenn das eigene Verständnis der 
Sprache "verbuggt" ist.

Aus dem C-Standard:
1
§3.4.3 undefined behavior
2
3
behavior, upon use of a nonportable or erroneous program construct
4
or of erroneous data, for which this International Standard imposes
5
no requirements.
6
7
NOTE
8
Possible undefined behavior ranges from ignoring the situation
9
completely with unpredictable results, to behaving during translation
10
or program execution in a documented manner characteristic of the
11
environment (with or without the issuance of a diagnostic message),
12
to terminating a translation or execution (with the issuance of a
13
diagnostic message).
14
15
EXAMPLE
16
An example of undefined behavior is the behavior on integer overflow.

von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

Jörg W. schrieb:
> Kaj G. schrieb:
>> Deswegen habe ich erwarten
>
> Was hast du denn an dem Wort „undefined“ im „undefined behavior“
> nicht verstanden?
Das "undefined" bezieht sich nach meinem Verstaendnis auf den Overflow, 
heisst:
Nach der Operation 32767 + 1 kann i irgendeinen Wert aus dem Intervall 
[-32768, 32767] haben, es muss aber nicht -32768 sein. Das verstehe ich 
an dieser stelle unter "undefined".
Der Vergleich der beiden Variablen i und len sollte aber, nach meinem 
Verstaednis, weiterhin funktionieren (was er auch tut).

Entsprechend diesem Stackoverflow-Beitrag:
https://stackoverflow.com/a/5416498
scheint aber die integer-promotion zuzuschlagen, was den Abbruch der 
Schleife erklaert. Und das ist eigentlich alles, was ich wissen wollte 
(worauf mich Arno aber erst richtig gebracht hat).

Warum bricht die Schleife ab?
Durch die Integer-Promotion von int16_t i zum Typ size_t (weil len von 
Typ size_t ist) kann die Schleife abbrechen, muss es aber nicht, da 
nicht definiert ist, das i nach dem Overflow einen negativen Wert haben 
muss (UB).
Hat i, wie in diesem Beispiel, den Wert -32768, und wird dies nach 
size_t konvertiert, so ergibt sich, dass 18446744073709518848 > 70000 
ist.

von Detlev T. (detlevt)


Lesenswert?

Kaj G. schrieb:
> Das habe ich ja auch nie bestritten. -32768 ist aber kleiner als 70000.

Nicht unbedingt. Vermutlich wird der Wert zuvor auf 32 Bit erweitert um 
dann mit einem 32 Bit unsigned Wert verglichen zu werden. Aus 0x8000 
wird so 0xFFFF8000. Und der ist - dann als unsigned interpretiert - eben 
größer als 70000 dezimal.

von (prx) A. K. (prx)


Lesenswert?

Kaj G. schrieb:
> Nach der Operation 32767 + 1 kann i irgendeinen Wert aus dem Intervall
> [-32768, 32767] haben, es muss aber nicht -32768 sein. Das verstehe ich
> an dieser stelle unter "undefined".

Der C Standard sieht das anders. Ebenso könnte das Programm mit einem 
Overflow-Trap abbrechen (... terminating a translation or execution). 
Nicht der Wert nach der Addition ist undefiniert, sondern das Verhalten 
des Programms ist undefiniert.

Du hingegen gehst immer noch von einem definierten Verhalten aus, indem 
du selbst definierst, dass das Ergebnis in -32768..+32767 liegen sollte. 
Aber auch das ist nicht definiert.

: Bearbeitet durch User
von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

A. K. schrieb:
> dass das Ergebnis in -32768..+32767 liegen sollte.
> Aber auch das ist nicht definiert.
Da haette ich jetzt aber gerne 'ne Erklaerung, wie das Ergebnis nicht 
in diesem Bereich liegen kann:
Wenn die Operation ausgefuehrt wird (das Programm also nicht 
abstuertzt/beendet wird/die Anweisung nicht uebersprungen wird, etc.) 
muss es doch auch ein (undefiniertes) Ergebnis geben. Irgendein Wert, 
und sei er noch so undefiniert, muss da im Speicher stehen.
Und der wird bei einem 16 Bit Wert zwischen 0x0000 und 0xFFFF liegen. 
Einen anderen Wertebereich fuer 16 Bit gibt es nicht. 'Nichts' kann auch 
nicht im Speicher stehen.

Das man sich auf das Ergebnis nicht verlassen kann ist klar, aber das 
der Wert irgendwie ausserhalb des Wertebereiches liegen kann (und das 
deutest du, lieber A.K., an), also nicht im Intervall [min, max], 
bezweifel ich dann doch (sachen wie integer-promotion/casts mal 
aussenvor, weil das ergebnis ja nicht in der Variablen steht).

Es sei denn natuerlich, du meinst mit "nicht definiert" sowas wie "In 
der Mathematik ist durch 0 teilen nicht erlaubt, weil es nicht definiert 
ist", dann soll mir das als Erklaerung reichen.

Aber ich lass mich gerne eines besseren belehren, deswegen bin ich ja 
hier.

von Markus F. (mfro)


Lesenswert?

Undefined behaviour bedeutet nicht, dass das Ergebnis einer Operation 
undefiniert ist, sondern die Operation selbst.

Der Compiler ist frei, zu tun oder zu lassen, was ihm gerade so einfällt 
oder ihm sinnvoll erscheint.

Ein (wahrscheinlich falsches) Ergebnis zurückliefern, abort() aufrufen 
(dann hast Du tatsächlich kein Ergebnis) oder sich im nächsten KKW 
einhacken und eine Kernschmelze erzeugen.

von (prx) A. K. (prx)


Lesenswert?

Kaj G. schrieb:
> Es sei denn natuerlich, du meinst mit "nicht definiert" sowas wie "In
> der Mathematik ist durch 0 teilen nicht erlaubt, weil es nicht definiert
> ist", dann soll mir das als Erklaerung reichen.

Na also, geht doch. ;-)

von Nop (Gast)


Lesenswert?

Johann L. schrieb:

> Nicht in C(++): Da dürfen signed in diesem Zusammenhang wie Ganze Zahlen
> behandelt werden:  Wenn du auf 0, egal wie oft, 1 addierst, wirst du nie
> -1 oder einen anderen negativen Wert erhalten.

Was daher kommt, daß die Darstellung nicht vom Standard vorgegeben wird, 
damit das auf jeder Architektur effizient ist. Heute nimmt jede CPU das 
Zweierkomplement, aber es wären auch Einerkomplement oder Betrag und 
Vorzeichen denkbar. Bei letzterem dürfte das Vorzeichen sogar im LSB 
sitzen, und wenn man dann zum Betrag in den Bits 1-15 immer 1 addiert, 
dann kommt nach 32767 der Wrap-Around auf 0.

Wobei das wieder so eine C-Stelle ist, die man besser als 
implementation-defined statt als undefined gemacht hätte. IMO ist man 
damals mit UB zu größzügig umgegangen, auch weil man nicht erwartet hat, 
daß eines Tages Compiler-Hersteller anfangen würden, UB aktiv gegen den 
Programmierer zu verwenden. Also unter dem Vorwand der Optimierung, die 
da nur deswegen zustandekommt, weil man die ganze Operation weglassen 
kann und somit ein schnelleres, aber garantiert funktional verkehrtes 
Programm hat.

von Rolf M. (rmagnus)


Lesenswert?

Kaj G. schrieb:
> Jörg W. schrieb:
>> Kaj G. schrieb:
>>> Deswegen habe ich erwarten
>>
>> Was hast du denn an dem Wort „undefined“ im „undefined behavior“
>> nicht verstanden?
> Das "undefined" bezieht sich nach meinem Verstaendnis auf den Overflow,
> heisst:
> Nach der Operation 32767 + 1 kann i irgendeinen Wert aus dem Intervall
> [-32768, 32767] haben,

Nein. Nach der Operation kann IRGENDWAS passieren. Das Programm darf 
auch abstürzen oder rückwärts weiterlaufen. Früher in comp.std.c haben 
wir immer  gesagt, dass in so einem Fall auch Dämonen aus deiner Nase 
geflogen kommen können und das auch konform wäre. ( 
http://catb.org/jargon/html/N/nasal-demons.html )
Das ist es, was oben im Zitat gemeint ist mit "... for which this 
International Standard imposes no requirements.".

von mh (Gast)


Lesenswert?

Rolf M. schrieb:
> Früher in comp.std.c haben
> wir immer  gesagt, dass in so einem Fall auch Dämonen aus deiner Nase
> geflogen kommen können und das auch konform wäre.

Wo wir bei den Dämonen sind hier ist ein passender und unterhaltsamer 
Beitrag auf der cppcon2016 "Garbage In, Garbage Out: Arguing about 
Undefined Behavior With Nasal Demons" https://youtu.be/yG1OZ69H_-o

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Kaj G. schrieb:
> Da haette ich jetzt aber gerne 'ne Erklaerung, wie das Ergebnis *nicht*
> in diesem Bereich liegen kann:
> Wenn die Operation ausgefuehrt wird (das Programm also nicht
> abstuertzt/beendet wird/die Anweisung nicht uebersprungen wird, etc.)
> muss es doch auch ein (undefiniertes) Ergebnis geben. Irgendein Wert,
> und sei er noch so undefiniert, muss da im Speicher stehen.


Die "Logik" erinnert mich an eine Kommunikation, die ich kürzlich hatte. 
Es ging um Code ähnlich dem folgenden, hier mit int=16:
 
1
int func (int x)
2
{
3
    if (x < 0)
4
        x = -x;
5
6
    if (x == -32768)
7
        x = 32767;
8
9
    return x;
10
}
 
Für avr setzt gcc das zum Beispiel so um:
 
1
func:
2
  sbrs r25,7
3
  rjmp .L2
4
  neg r25
5
  neg r24
6
  sbc r25,__zero_reg__
7
.L2:
8
  ret

Die Beschwerde betraf das Fehlen des 2. if-Statements samt Block.  Als 
"Argument" wurde vorgebracht, dass im Falle von (binär) 0x8000 als Input 
von func nach dem Abarbeiten des 1. if ja irgendein Wert in den 
entsprechenden Registern stehen müsse, und dieser Wert könne im Falle 
von 0x8000 und aufgrund des Codes des ersten if nur 0x8000 sein.  Der 2. 
Vergleich dürfe also nicht entfallen, weil nachweisbar 0x8000 in den 
Registern stehe, just der Wert, der in if (x == -32768) abgefragt wird.

Der Code ist jedoch korrekt, und da fehlt nix: Falls x < 0, wird x durch 
-x ersetzt und folglich wird hernach x > 0 sein: Overflow braucht nicht 
berücksichtigt zu werden, da UB.  Insgesamt ist also x >= 0 nach dem 1. 
if, also kann x == -32768 für kein x erfüllt sein, also darf das 2. if 
entsorgt werden.

Und auch wenn physikalisch nachprüfbar und im Simulator / Debugger klar 
sichtbar -32768 in r25:r24 steht, darf die Implementation (vulgo: 
Compiler) davon ausgehen, dass fortan nicht -32768 oder irgendein 
anderer, als negativ anzusehender signed-Wert, im Doppelregister steht. 
Und genause dürfte sie annehmen, dass bei Input von -32768 ein Wert 
von -32768 oder -1 oder 42 im Register stünde, wenn sich das als 
vorteilhaft erwiese und ausgeschlachtet werden könnte.

Der Denkfehler ist, dass ein Registerinhalt Aussage über den Zustand der 
abstrakten Maschiene zulasse, hier der Schluss vom Wert 0x8000 auf den 
Wert von x.  Dem ist nicht so.

: Bearbeitet durch User
von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Rolf M. schrieb:
> Früher in comp.std.c haben wir immer  gesagt, dass in so einem Fall auch
> Dämonen aus deiner Nase geflogen kommen können und das auch konform
> wäre.

Ein früher GCC hat in diesem Falle wohl mal das (einstmals populäre)
Spiel "nethack" angeworfen …

von mh (Gast)


Lesenswert?

Jörg W. schrieb:
> Ein früher GCC hat in diesem Falle wohl mal das (einstmals populäre)
> Spiel "nethack" angeworfen …

Ein im binary integrietes nethack oder ein anderweitig installiertes 
nethack? Ersteres wäre sehr praktisch.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

mh schrieb:
> ein anderweitig installiertes nethack?

Das des Hosts.

Tante Gugel meint, es war GCC 1.17, und das passierte wohl dann,
wenn ein unbekanntes #pragma auftauchte.  Offenbar wollte da einer
der GCC-Entwickler auf die Unsinnigkeit solcher Pragmas hinweisen.

(Kann sogar sein, dass es "implementation defined" war.  Man muss
dann halt nur dokumentieren, dass beim Auftreten unbekannter Pragmas
nethack gestartet wird. ;-)

von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

A. K. schrieb:
> Na also, geht doch. ;-)
Sag das doch einfach gleich :D

Ich danke euch Allen fuer eure Muehe. Ihr seid spitze. :)

Jetzt habe ich nur noch eine letzte Frage:
Ab wann genau ist das Verhalten des Programms UB?
Ist es die ganze Zeit UB (also auch schon beim Start, also i = 0), weil 
die gefahr des int-overflows besteht, oder ist es erst ab dem Moment des 
overflows (32767 + 1) UB?

von Rolf M. (rmagnus)


Lesenswert?

Kaj G. schrieb:
> Jetzt habe ich nur noch eine letzte Frage:
> Ab wann genau ist das Verhalten des Programms UB?
> Ist es die ganze Zeit UB (also auch schon beim Start, also i = 0), weil
> die gefahr des int-overflows besteht, oder ist es erst ab dem Moment des
> overflows (32767 + 1) UB?

Die Frage haben wir damals in comp.std.c auch schon diskutiert. Soweit 
ich mich erinnere, war man sich einig, dass das UB dann die ganze Zeit 
besteht. Es ist darüber hinaus nicht nur auf die Programmlaufzeit 
beschränkt. So wäre es z.B. auch erlaubt, wenn der Compiler mit Fehler 
abbricht und gar nicht erst ein Programm erzeugt.

Beitrag #5214019 wurde vom Autor gelöscht.
Beitrag #5214022 wurde vom Autor gelöscht.
von (prx) A. K. (prx)


Lesenswert?

Kaj G. schrieb:
> Ab wann genau ist das Verhalten des Programms UB?

Jetzt wirds etwas philosophisch.

So wie oben präsentiert ist alles ok, so lange "len" innerhalb des 
Wertebereichs bleibt, der einen Überlauf verhindert. Ausserhalb davon 
wird es komplizierter, weil es dann davon abhängen kann, ob der Compiler 
den Wert kennt oder nicht.

Denn wenn der Compiler den Wert kennt, dann wird er möglicherweise die 
Schleife zu
  for (i = 0; true; i++) {
    ...
  }
umbauen. Im Grunde könnte also der Compiler dir schon bei der 
Übersetzung eines solchen Programms die Dämonen aus der Nase ziehen. 
Dann wärs zumindest offensichtlich.

Das fiese an der Definition von UB ist, dass auch das Verhalten des 
Compilers bei Erkennung von UB undefiniert ist. ;-)

: Bearbeitet durch User
von Markus F. (mfro)


Lesenswert?

Rolf M. schrieb:
> Die Frage haben wir damals in comp.std.c auch schon diskutiert. Soweit
> ich mich erinnere, war man sich einig, dass das UB dann die ganze Zeit
> besteht. Es ist darüber hinaus nicht nur auf die Programmlaufzeit
> beschränkt. So wäre es z.B. auch erlaubt, wenn der Compiler mit Fehler
> abbricht und gar nicht erst ein Programm erzeugt.

Hmmm. Kann das sein? Das würde doch jedes Programm, das

scanf("%hd"...);

verwendet, zu UD erklären?

von tictactoe (Gast)


Lesenswert?

Kaj G. schrieb:
> Ab wann genau ist das Verhalten des Programms UB?

UB ist abhängig vom Input. Also "immer" im Sinne von "wirklich immer und 
für alle Zeit" stimmt so nicht. Aber sobald für gegebenen Input UB 
auftritt, gilt UB auch, bevor die fragliche Code erreicht wird. Das kann 
man mit folgendem Programm illustrieren (aus dem Kopf geschrieben, ich 
lasse #includes und namespace weg):
1
int16_t overflow()
2
{
3
  int16_t x = 32767;
4
  return x+1;
5
}
6
7
int main(int argc, char** argv)
8
{
9
  if (argc == 2) {
10
    cout << "overflow: ";
11
    cout << overflow() << endl;
12
  } else {
13
    cout << "ok" << endl;
14
  }
15
}
Der Compiler darf den Zweig für argc == 2 ganz weglassen. Wenn man das 
Programm also mit einem Argument aufruft (wodurch dieser Zweig betreten 
würde), dann muss nicht "overflow:" auf dem Terminal erscheinen, 
obwohl es "vor" dem UB aufscheint.

Ruft man das Programm aber ohne Argumente auf, dann muss "ok" auf dem 
Terminal erscheinen.

von Mampf F. (mampf) Benutzerseite


Lesenswert?

A. K. schrieb:
> Im Grunde könnte also der Compiler dir schon bei der
> Übersetzung eines solchen Programms die Dämonen aus der Nase ziehen.
> Dann wärs zumindest offensichtlich.

Das macht er ja ... Er warnt davor :)

von (prx) A. K. (prx)


Lesenswert?

Mampf F. schrieb:
> Das macht er ja ... Er warnt davor :)

Ja, aber das ist Kulanz. Er darf, aber muss nicht. ;-)

von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

A. K. schrieb:
> Jetzt wirds etwas philosophisch.
Dachte ich mir :)

tictactoe schrieb:
> UB ist abhängig vom Input. Also "immer" im Sinne von "wirklich immer und
> für alle Zeit" stimmt so nicht. Aber sobald für gegebenen Input UB
> auftritt, gilt UB auch, bevor die fragliche Code erreicht wird.
Das fuehrt mich zu der Ueberlegung, dass wenn ich vorher eine Abfrage 
mache (pseudo-code):
1
if (len < int16_max) {
2
    for (int16_t i = 0; i < len; i++) {
3
        // ....
4
    }
5
} else {
6
    // ...
7
}
dass es dann kein UB mehr sein duerfte. Sehe ich das richtig?

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

A. K. schrieb:
> Im Grunde könnte also der Compiler dir schon bei der Übersetzung eines
> solchen Programms die Dämonen aus der Nase ziehen.

Bei meinem "if (x < 0) x = -x;" geschieht das auch:
1
avr-gcc ... -Os -Wstrict-overflow=2 ...
2
In function 'func':
3
6:8: warning: assuming signed overflow does not occur when simplifying conditional to constant [-Wstrict-overflow]
4
     if (x == -32768)
5
        ^

Die Erkenntnis ist allerdings von Analyse-Passes abhängig, die bei -O0 
erst garnicht laufen.  Entsprechend fehlt die Warnung mit -O0, und das 
Compilat enthält Code für den gezeigten Vergleich (mit Verhalten, das 
gemäß Standard immer noch UB ist).

In diesem Zusammenhang muss auch darauf hingewiesen werden, dass es 
einen GCC-Bug gab, der unsigned analog und daher inkorrekt behandelte:

https://gcc.gnu.org/PR75964

von tictactoe (Gast)


Lesenswert?

Kaj G. schrieb:
> Das fuehrt mich zu der Ueberlegung, dass wenn ich vorher eine Abfrage
> mache (pseudo-code):
1
> if (len < int16_max) {
2
>     for (int16_t i = 0; i < len; i++) {
3
>         // ....
4
>     }
5
> } else {
6
>     // ...
7
> }
> dass es dann kein UB mehr sein duerfte. Sehe ich das richtig?

Das ist richtig. Das vermeidet UB!

von (prx) A. K. (prx)


Lesenswert?

Johann L. schrieb:
> In diesem Zusammenhang muss auch darauf hingewiesen werden, dass es
> einen GCC-Bug gab, der unsigned analog und daher inkorrekt behandelte:

Wobei dieser C-Code eigentlich nicht unsigned ist, sondern aufgrund der 
integer promotion rules signed ist. Dann aber beim Test nichts mehr mit 
dem Vorzeichen zu tun hat. Erst durch die Wegoptimierung der promotion 
wird es wieder unsigned.

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

A. K. schrieb:
> Johann L. schrieb:
>> In diesem Zusammenhang muss auch darauf hingewiesen werden, dass es
>> einen GCC-Bug gab, der unsigned analog und daher inkorrekt behandelte:
>
> Wobei dieser C-Code eigentlich nicht unsigned ist, sondern aufgrund der
> integer promotion rules signed ist.

PR75964 funktioniert auch auf ilp32, allerdings fand ich kein 
offizielles Target, wo es passierte.  Bei avr hat's dann auch gekracht, 
und der Fix behob auch das Problem auf der ilp32.  Die Idee hinter der 
"Optimierung" war ganz klar Undefined Overflow implementiert auf RTL, 
das wie im Commit-Comment ausgeführt kein Vorzeichen oder Overflow 
kennt.

Und das war bestimmt nicht das letzte mal, dass Signed Overflow den 
Entwicklern auf die Füße gefallen ist.  Bei internen Transformationen 
wie Distributivgesetzt ist zu unterscheiden, ob sie dem zu übersetzenden 
Programm entstammen oder einer GCC-internen Transformation auf 
tree-Ebene.  M.E. eine schlechte Entscheidung, die Bugs produziert wie

https://gcc.gnu.org/PR56899

https://gcc.gnu.org/PR68067

um 2 weitere zu nennen.

Robuster und besser wartbar wäre hier gewesen, das UB von C/C++ nicht 
auf tree-Ebene zu ziehen, zumal dies u.U. noch die Unterscheidung 
zwischen Zeiger- und Nichtzeiger-Arithmetik erfordert.  Besser wäre 
gewesen, Java-Semantik auf tree anzuwenden, was man bei -fwrapv eh 
unterstützen muss (wobei -fwrapv erst für Java eingeführt wurde, also 
nach tree und dessen Semantik, die deutlich älter sind als der 
inzwischen wieder entfernte Java-Support).  Das darauf zurückgehende 
Weniger an Optimierungen dürfte wirklich verschmerzlich sein und kaum 
ins Gewicht fallen.

: Bearbeitet durch User
von S. R. (svenska)


Lesenswert?

Kaj G. schrieb:
> Nach der Operation 32767 + 1 kann i irgendeinen Wert aus dem Intervall
> [-32768, 32767] haben, es muss aber nicht -32768 sein.

Die Operation darf (theoretisch) aber auch das benachbarte Byte 
zerstören oder einen Absturz provozieren. Du kannst dich also weder auf 
einen bestimmten Wert verlassen, noch darauf, dass dein Programm 
überhaupt läuft.

Kaj G. schrieb:
> Ab wann genau ist das Verhalten des Programms UB?

Laut Standard ist ein Programm, welches an irgendeiner Stelle UB 
hervorruft, schon vor dessen Ausführung undefiniert. Theoretisch darf 
sich also auch das Betriebssystem weigern, ein solches Binary überhaupt 
auszuführen.

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.