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
usingnamespacestd;
7
8
9
intmain()
10
{
11
stringstr;
12
cin>>str;
13
14
size_tlen=str.length();
15
uint16_ti;
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
return0;
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
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
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
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).
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.
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?
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 ;-)
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.
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.
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.
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.
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.
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.
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.
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. ;-)
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.
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.".
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
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
intfunc(intx)
2
{
3
if(x<0)
4
x=-x;
5
6
if(x==-32768)
7
x=32767;
8
9
returnx;
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.
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 …
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.
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. ;-)
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?
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.
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. ;-)
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?
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_toverflow()
2
{
3
int16_tx=32767;
4
returnx+1;
5
}
6
7
intmain(intargc,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.
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 :)
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_ti=0;i<len;i++){
3
// ....
4
}
5
}else{
6
// ...
7
}
dass es dann kein UB mehr sein duerfte. Sehe ich das richtig?
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
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.
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/PR56899https://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.
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.