Undefined behavior Int-Addition

Auch wenn ich mich wiederhole und im Kreis drehe: Der Vergleich mit Java ist nicht das, worauf ich raus wollte. In Java ist das schlimmste, was passieren kann: Es fliegt eine Exception. Ja, big deal. In C/C++ kann

ALLES

passieren, und das ist schon recht viel. Eigentlich hatte ich vermutet, dass der verlinkte Bug-Report die Brenzligkeit bestimmter Verhalten verdeutlicht, und auch klar macht, dass das nur die Spitze des Eisbergs ist. Aber ich sehe schon, ich bin der einzige, der es komisch findet, dass die Addition zweier Zahlen bewirken darf, dass die Festplatte formatiert wird :rolleyes:

Natürlich gibt es Tools, Websuchen liefern was wie UndefinedBehaviorSanitizer — Clang 3.9 documentation , was wohl provokativ formuliert eine Art “FindBugs für Leute, die zu viel Zeit haben” ist. Und zugegeben, ich bin von technischer Seite natürlich kein “Experte” für dieses Thema (sowas wie A Guide to Undefined Behavior in C and C++, Part 1 – Embedded in Academia zu lesen liegt bestenfalls auf meiner “Auf-Der-Arbeit”-TODO-List).

(BTW: Ich bin mir der Ironie bewußt, dass die JVM in C++ geschrieben ist ;-)).

Ja, das Performance-Argument ist böse. Dadurch das natürlich C++ eine größere Performance besitzt, besitzt man als Programmierer eine größere Verantwortung für den Code. Anderseits ist die Verantwortung der Compiler-Bauer größer. Es ist nämlich katastrophal, dass Performance-Optimierungen, bestehenden Code kaputt machen, weil Code auf diesem undefinierten Verhalten beruht. Eben auch beim Beispiel vom Ausschalten der Nullchecks wäre es die Aufgabe der Entwickler gewesen zu überprüfen, ob bestehender kaputt geht.

Ich komm noch mal zum Code zurück: In wie fern sollte ein Overflow ein Problem darstellen? Letztlich kommt es auf die Hardware an ob die ALU entsprechende Flags wie overflow oder negativ ausspuckt oder nicht. Und da bis auf Exoten die meisten gebräuchlichen Hardware-Platformen fest definiert sind kann man als Compiler-Bauer auch schon vorher wissen was die Hardware bei einem solchen Konstrukt macht und zurück gibt. Sollte sich ein Compiler dazu entschließen statt eines ungültigen Wertes in den vorhanden Speicher zu packen über diesen hinaus in einen ungültigen Speicherbereich schreiben ist das IMO Fehler des Compilers da dieser einen für die Zielplatformen falschen Binär-Code erzeugt hat.
Ob also die ReSA mit nem segfault aussteigt oder die Stäbe statt einzufahren lieber herausziehen will ist kein Problem der Sprache denn Grund/Ursache liegt an anderer Stelle.
Grundsätzlich würde ich aber schon in Richtung Exception gehen: Es wäre eine vom Programmierer zu verhinderne Ausnahme vom grplanten Code-Ablauf.

[QUOTE=Sen-Mithrarin]Ich komm noch mal zum Code zurück: In wie fern sollte ein Overflow ein Problem darstellen?

Sollte sich ein Compiler dazu entschließen statt eines ungültigen Wertes in den vorhanden Speicher zu packen über diesen hinaus in einen ungültigen Speicherbereich schreiben ist das IMO Fehler des Compilers da dieser einen für die Zielplatformen falschen Binär-Code erzeugt hat.[/QUOTE]

Und ich komme nochmal zu dem zurück, was ich so haarsträubend finde, und die ganze Zeit zu vermitteln versuche: Das ist eben gerade nicht so. Heute macht der GCC 6.1.0 daraus “normalerweise” einen “normalen” Überlauf (wie in Java). Und wenn er in Version 6.1.1 in diesen Fällen die Festplatte formatiert, dann ist das in Ordnung. Kein Fehler. Millionen von Festplatten werden formatiert, aber der Sprachstandard erlaubt das. Die Schuld liegt dann bei denjenigen, die irgendwo
int z = x + y;
geschrieben haben, statt


#include <climits>

if (x >= INT_MAX - y) return errorReport();
if (y >= INT_MAX - x) return errorReport();
int z = x + y;

und die Compilerbauer waschen sich die Hände in der Unschuld, dass das ja “Undefined Behavior” ist, und sie ja nichts dafür können, dass Leute so dummen UB-Code schreiben (auch wenn der nur aus einem “x+y” besteht).

Und genau dieses unspezifizierte Verhalten ermöglicht es einem Entwickler, mittels Compilerswitch das erstellte Programm bei einem Überlauf sich selbst beenden zu lassen (-ftrapv) oder mit einem anderen Switch dafür zu sorgen, dass ein Überlauf sich wie bei Java verhält (-fwrapv) (bezogen auf gcc - siehe hier).
@Marco13 : der Code zum prüfen, ob ein Überlauf stattfinden wird, müsste doch eigentlich mittels > testen. Und was mir dabei gleich noch auffällt: dieser Code ist doch gar kein Schutz vor einem Überlauf, denn wenn x oder y negativ ist, findet in jedem Fall ein Überlauf statt. Wenn dadurch die Festplatte formatiert wird, hilft auch dieser „Schutz“ nicht weiter.

*** Edit ***

Der Code müsste also so lauten:

#include <climits>

if (y > 0 && x > INT_MAX - y) return errorReport();
if (x > 0 && y > INT_MAX - x) return errorReport();
if (y < 0 && x < INT_MIN - y) return errorReport();
if (x < 0 && y < INT_MIN - x) return errorReport();
int z = x + y;

[QUOTE=cmrudolph]Der Code müsste also so lauten:

#include <climits>

if (y > 0 && x > INT_MAX - y) return errorReport();
if (x > 0 && y > INT_MAX - x) return errorReport();
if (y < 0 && x < INT_MIN - y) return errorReport();
if (x < 0 && y < INT_MIN - x) return errorReport();
int z = x + y;

[/QUOTE]

Ich finde man sollte ruhig von Builtins Gebrauch machen, die sind nämlich geschenkt, kürzer und man kann nicht viel falsch machen:


int z = 0;
if (__builtin_add_overflow(x, y, &z)) return errorReport();

Ich will ja nicht mehr sagen, dass dass es praktisch niemand so macht, wie man es machen „müßte“ (es werden millionenfach ints ganz normal addiert (und vermutlich tausendfach wird dabei das „wrap“-Überlauf-Verhalten ausgenutzt)), und man sich damit immer in der Grauzone befindet, in der man nicht sagen kann, was das Programm macht. Dass die Einstellung dazu von einem verständnislosen „WTF kann man sowas in einer Programmiersprache unterbringen?“-Kopfschütteln bis hin zu gelangweiltem „Joa, is halt so“-Schulternzucken reicht, ist inzwischen klar :wink:

Es werden auch millionenfach ints ganz normal inkrementiert. Ich glaube es würden Leute verrückt werden, würde man alles machen, wie man es machen müsste (Stichwort: Schulbuch OOP). Danach ginge es dann mehr oder weniger auf Defensive Programming heraus. Ich glaube einfach, dass man letztlich gesehen diese “Sprachfeature” akzeptieren muss, wenn man mit dieser Programmiersprache arbeitet. Im Nachhinein gesehen war das keine gute Idee, dem undefinierten Verhalten zuzuordnen.

Ich verstehe immer noch nicht warum du dich so drauf festnagelst das bei int z=int x + int y = dd if=/dev/zero of=/dev/sda rauskommen soll.
“Undefiniertes Verhalten” bedeutet in diesem Zusammenhang keines Falls dass bei Anweisung X im Source der Compiler plötzlich Anweisung Y draus machen darf. Der Compiler muss schon genau das übersetzen was der Programmierer geschrieben hat - nämlich mal ganz krass runtergebrochen ein ADD EAX, EDX, also den Wert der in EAX liegt mit dem Wert der in EDX liegt zu addieren. “Undefiniert” ist hier lediglich ob als return ein simpler overflow OHNE oder MIT overflow-Flag der ALU kommt - und ob ggf. NEGATIVE mit angesteuert wird weil die ALU mit zwei unsigned statt zwei signed ints rechnet. Wobei, wie bereits erwähnt, für viele der heute standardmäßigen Plattformen die vom i386 abstammen ein solches Verhalten was die physische Hardware in einem solchen Fall macht im InstructSet fest definiert ist. Wenn ich also einen Compiler für die AMD64-Platform (praktisch alle heute modernen eingesetzten CPUs) schreibe ist es meine Aufgabe als Compiler-Entwickler mich mit dieser Platform vertraut zu machen und meinen Compiler entsprechend so zu bauen dass er genau den Binärcode erzeugt den die CPU nach Ihrer Platform-spec auch ausführen soll.

Der Haken an der Sache ist nun lediglich dass halt die Sprache C nicht selbst vorher festschreiben kann wie sich eine bestimmte Platform nachher zu verhalten hat. Wenn also mein selbstgebauter MIPS mit 74xx-TTL-Chips lieber OVERFLOW ohne NEGATIVE und ein AMD64 lieber OVERFLOW mit NEGATIVE macht - dann ist das so - aber auch darum weil der Designer der Platform das vorher so entschieden hat. Und dieses Platform-abhängige Verhalten was die Sprache C selbst nicht vorgeben kann - genau das ist hier mit “undefiniertem Verhalten” gemeint - nichts anderes.

Ergo: Ich kann sehr wohl vorher bei deinem Code sagen: “Es wird hier definitiv zu einem overflow kommen.”. Was ich aber nicht vorher sagen kann ist für welche bestimmte Hardware welche Status-Flags von der ALU angesteuert werden da dies vom Compiler und der Hardware abhängt auf der nachher die Bits mit dem Bus eine CPU-Rundfahrt machen. Das Ergebnis - ob nachher ein positiver oder ein negativer Wert in der Speicher-Adresse landet - ist nicht definiert. Das aber einfach so in einen nicht zulässigen Speicher-Bereich geschrieben werden darf - ich glaube das dürfte sehr wohl irgendwo als “nicht zulässig” definiert sein.

So, und nun entschuldigt mich bitte - ich muss dann mal noch ein paar catch-Blöcke im nächsten AKW korrigieren …

Jaja, ich sehe schon, es wird nicht ernst genommen und ist wohl ausdiskutiert.
Signed integer overflow ist undefined behavior.
Undefined behavior heißt: Der Compiler darf da machen, was er will.
Und wenn jemand das nicht wahr haben will, sind die Gründe für diese Verdrängung wohl im Grunde ähnlich, wie die, die mich ursprünglich dazu veranlasst hatten, diese Quizfrage zu stellen, und hängen mit dem Gedanken zusammen: “Das kann doch eigentlich gar nicht wahr sein!?”
Doch. Ist es.

nein - die CPU verhält sich evt. anders als Du erwartest. Jeder Compilerbauer der anschließen die Pladde formatiert gehört geköpft. Der Mythos ist völlig übertrieben und (wie wir gerade sehen) schießt am Ziel vorbei.

Doch @Sen-Mithrarin hat es viel besser erklärt als ich mit meinen 0,02€. Auf embedded Kisten wird fast ausschließlich C/C++ verwendet, weil dort Speichermangel herrscht. Also habe ich als Compilerbauer keine andere Wahl als mich darauf zu verlassen was die CPU bei einer Addition macht. Komme ich auf den Gedanken „der Überlauf macht sich hier praktisch“ ist das meine freie Entscheidung als Programmierer. Ändert allerdings das Management irgend wann die CPU (weil sie billiger ist) und sie verhält sich bei der Addition anders, dann fällt es mir auf die Füße, weil der Compilerbauer mich an der Stelle (mit einem undefined behavior) davor gewarnt hat. Sprich: der Compilerbauer macht hier keine Verenkungen damit das Programm sich auf allen CPU gleich verhält.

Dass die Beispiele mit “Katzenvideos” und “Platte löschen” suggestiv waren, um das Problem zu unterstreichen, sollte klar sein. Nichtsdestotrotz machen die Compiler Dinge, die man nicht erwarten würde.

Das ändert alles nichts am allgemeinen Problem: “Undefined Behavior” erlaubt dem Compiler ALLES - z.B. auch “Optimierungen”, die zu echten (Sicherheits!)problemen führen können. Der erste Teil des schon verlinkten Artikels A Guide to Undefined Behavior in C and C++, Part 1 – Embedded in Academia macht das recht gut deutlich. Grob gesagt: Der Compiler stellt irgendwo “Undefined behavior” fest, und nimmt sich darum die Freiheit, z.B. einen Check wegzuoptimieren, der eigentlich verhindern sollte, dass das System kompromittiert und schädlicher Code ausgeführt wird. Der generierte Code ist in Spezialfällen dann 0.8% schneller und der Compilerbauer hat seinen Benchmark verbessert (und im System klafft eine Sicherheitslücke, aber das ist ja die Schuld der dummen UB-Programmierer).

Wer den Artikel noch nicht gelesen hat, kann ja als “sportliche Herausforderung” mal versuchen, zwei Zahlen zu dividieren, ohne UB zu risikieren:
int32_t safe_div_int32_t (int32_t a, int32_t b) { ... }
(Ich behaupte: Aus dem Stand schafft das praktisch niemand, der nicht schon stark sensibilisiert ist und die dunklen Ecken kennt)

Und diejenigen, die mit (sorry: recht gestelzt geekisch wirkendem) technischem Halbwissen darauf rumreiten, dass “auf x86 das Verhelten beim Überlauf ja klar und bekannt ist”, sollten sich zusätzlich vielleicht noch c++ - Why does integer overflow on x86 with GCC cause an infinite loop? - Stack Overflow ansehen. Sooo klar ist das Verhalten eben nicht. Das spezielle Problem kann man auf diesem speziellen Compiler mit -fwrapv verhindern (rethorische Preisfrage: Was macht Visual Studio?) aber dafür muss man erstmal einen Anlass sehen - und wenn man denkt: “Joa, überlauf, Zweierkomplement, alles nicht so schlimm”, dann sieht man den eben offenbar nicht.

Beim gcc Compilerflag -ftrapv wird deutlich, dass der Compiler sehr wohl “machen kann, was er will”. Denn vereinfacht gesagt beendet sich das Programm dann, falls ein Überlauf auftreten sollte.

Was mich noch stört: Wie ist eigentlich “undefined behaviour” definiert? Irgendwo wird sicher stehen in welchem Rahmen sich dies bewegen darf. Auch interessant wäre: Ist mit “undefined behaviour” gemeint das unklar ist was ein Compiler draus macht oder aber eher wie sich eine bestimmte Platform verhalten wird? Oder vielleicht eine Kombination daraus?
Wie sieht es hier mit der korrekten Deutung und Übersetzung aus? Ist gemeint dass das Ergenis innerhalb eines gewissen Rahmen unbestimmt ist? Oder wie hier proklamiert wird: “Der Compiler darf machen was er will!”?
Sicher schönes Thema um dass man sich als Nutzer dieser Sprache geben muss. Nur: Muss ich um halbwegs gut C zu können mir über solche Einzelheiten den Kopf zerbrechen oder nutze ich das Lieblingsspielzeug das auf “Delegation” hört? Sicher würde ich den einfachen Weg gehen und fertigen Code nutzen den Andere bereits ausgiebig getestet haben.

Auch wenn ich mir nicht den Status eines Experten/LanguageLawyers anmaßen will, versuche ich, die Fragen nach bestem Wissen zu beantworten, auf Basis von allem, was ich bisher dazu gelesen habe:

Nach allem, was ich bisher gelesen habe, darf beim Auftreten von UB
alles
passieren. Einschließlich der Katzenvideos.

[QUOTE=Sen-Mithrarin;133911]
Auch interessant wäre: Ist mit „undefined behaviour“ gemeint das unklar ist was ein Compiler draus macht oder aber eher wie sich eine bestimmte Platform verhalten wird? Oder vielleicht eine Kombination daraus?
Wie sieht es hier mit der korrekten Deutung und Übersetzung aus? Ist gemeint dass das Ergenis innerhalb eines gewissen Rahmen unbestimmt ist? Oder wie hier proklamiert wird: „Der Compiler darf machen was er will!“?[/QUOTE]

Der Artikel pflückt das ganz gut auseinander. (Das ist das tolle am Internet: Jeder Idiot findet eine Webseite, mit der er seine wertlose Meinung untermauern kann - und wenn nicht, erstellt er einfach eine :D). Der Grundgedanke ist GROB, dass der Standard eine Art „idealisierte Maschine“ beschreibt (ähnlich, wenn auch nicht so konkret und plastisch, wie eine JVM). Und solange alles gut geht, macht die Maschine genau das, was im Standard steht. Aber sobald irgendwo UB auftritt, kann diese Maschine alles machen. Und darauf setzen Compilerbauer eben auf. Wie cmrudolph schon sagte: Das flag „-fwrapv“ erzwingt das gutmütige Verhalten (mit dem nebulösen Nebensatz „…** enables some optimizations and disables other.**“). Es GIBT also per default Optimierungen, die dieses UB ausnutzen, und eben bewirken, dass da NICHT der normale Überlauf stattfindet - siehe auch der stackoverflow-link. Demgegenüber bewirkt „-ftrapv“, dass das Program sich verabschiedet. Eine andere Option. Laut Standard auch OK. Man könnte auch ein flag „-fcatvideo“ einführen, das … naja, sollte klar sein.

EDIT: Welches „Delegation-Spielzeug“ da gemeint ist, weiß ich gerade nicht. Und über das, was notwendig ist, um sich gerechtfertigt als „guter C-Programmierer“ bezeichnen zu dürfen, werde ich mir sicher kein Urteil anmaßen. Ich weiß nur, dass es viele gibt, die sich als „gute C(++)-Programmierer“ sehen, es aber definitiv nicht sind, und ich bin mir sicher, dass sich der überwiegende Teil der C+±Programmierer über viele Details, und ganz speziell den UB-Fall, um den es hier ursprünglich ging, nicht im Klaren sind - deswegen auch die Quizfrage :wink:

[QUOTE=Marco13]Nach allem, was ich bisher gelesen habe, darf beim Auftreten von UB
alles
passieren. Einschließlich der Katzenvideos.[/QUOTE]

Ach, sieh es doch mal pragmatisch: Soo viel wäre jetzt auch nicht gewonnen wenn in den Specs klipp und klar definiert wäre das bei einem Überlauf die Festplatte gelöscht und ein Katzenvideo abgespielt wird… :witless:

PS: Ich habe den Artikel zwar noch nicht gelesen, aber er steht immerhin schon auf der TODO-Liste

Mir leuchtet ja (bis zu einem gewissen Grad) ein, dass man sich nicht festlegen will. C/C++ soll eben das Schweizer Taschenmesser unter den eierlegenen Wollmilchsäuen sein. Und wenn man einen Rechner mit 12-bit-Einerkomplement-Integer-Darstellung hat, soll einem die Spec nicht das Leben unnötig schwer machen. Aber wie der Artikel auch so schön sagt:

One suspects that the C standard body simply got used to throwing behaviors into the “undefined” bucket and got a little carried away.

Ungültigen Speicher zu beschreiben kann ja meinetwegen UB sein. Aber zwei Zahlen addieren?! Hallo!? Ich könnte mir vorstellen, eine Formulierung wie „…the resulting value is unspecified“ würde noch genügend Freiheitsgrade offen lassen (aber zumindest diese verd… Katzenvideos ausschließen :D)