Undefined behavior Int-Addition

[edit SlaterB: aus Java-Quiz abgespalten, mit Themen-Titel nun nicht mehr wirklich Rätsel]

Mal ein bißchen fremdgehen: Was macht folgendes C+±Programm:


#include <cstdint>
#include <iostream>

int main(int argc, char* argv[])
{
    int32_t x = 2000000000;
    int32_t y = 2000000000;
    int32_t z = x + y;
    std::cout << "Result: " << z << std::endl;
    return 0;
}

ahh - Pointer-Arithmethik - spontan würde ich auf einen Überlauf mit anschließendem SEG-FAULT tippen …

Pointer? Da kommen keine Pointer vor. Nur normale signed 32 bit integers. Das ganze 1:1 nach Java übersetzt wäre

class Main
{
    public static void main(String args[])
    {
        int x = 2000000000;
        int y = 2000000000;
        int z = x + y;
        System.out.println("Result "+z);
    }
}

Ja, vielleicht noch der Hinweis: Es ist eine Fangfrage… :rolleyes:

[SPOILER]Wenn ich die Nullen richtig gezählt habe, führt die Addition zu einem Überlauf des in int darstellbaren Wertebereichs. Genau ausgerechnet habe ich es nicht, aber das Ergebnis dürfte negativ werden.[/SPOILER]

Die Antwort auf die Frage, was dieses C+±Programm macht, ist hier:

Was dieses C+±Programm macht

Es öffnet ein Browserfenster, in dem ein lustiges YouTube-Katzenvideo abgespielt wird. Dann verschlüsselt es den kompletten Inhalt der Festplatte. Dann schickt es eine Mail an den C+±Erfinder Bjarne Stroustrup, mit dem Betreff „FAIL!“, in der 100 mal steht: „Thou shalt not invent crappy languages“.

Nein. Wie nillehammer schon festgestellt hat, entsteht bei der Addition dieser beiden signed integers ein Überlauf. Und ein Überlauf von signed integers in C++ ist Undefined behavior. Das hießt, ein Programm, das aus dem obigen Code compiliert wurde, dürfte sich, laut C+±Standard, genau so verhalten, wie ich es oben beschrieben habe.

Nochmal im Klartext: Im Rahmen der Programmiersprache C++ läßt sich KEINE Aussage darüber machen, was das obige Programm macht!.

Welche Bedeutung das hat, etwa, wenn man irgendwo Code hat wie


int32_t value = getValueFromSomeLibraryFunction();
if (value + 1 > 42) { ... }

sollte klar sein.

Ich bin für Argumente offen, warum C++ eine „gute“ Sprache ist. Aber all diese Argumente können nicht aufwiegen, dass es mit C++ praktisch nicht möglich ist, zwei signed integers zu addieren, ohne undefined behavior zu riskieren.

Es ginge natürlich auch so: 15.18.2. Additive Operators (+ and -) for Numeric Types:

If an integer addition overflows, then the result is the low-order bits of the mathematical sum as represented in some sufficiently large two’s-complement format.

Sooo schwer ist es ja eigentlich nicht :slight_smile:

Naja, dann hatte ich aber zumindest mit dem Überlauf ja erstmal doch recht. Dass C++ für sowas kein genau definiertes Verhalten hat dürfte wohl auch in die Richtung gehen dass es, im Gegensatz zu Java, dort keine genau Lang-Spec gibt die vorschreibt wie sich ein Compiler in einem solchen Fall zu verhalten hat (was er also beim Erkennen eines Überlaufs für “sichere” Werte einsetzen soll) - und, und ich denke dass dürfte die Ursache sein, weil es vom CPU abhängt wie dieser letztlich reagiert (im Sinne von wie er designed wurde und was dann das physische Ergebnis ist - also ob ALU overflow-Flag aktiviert oder nicht).
Ist halt schon was anderes wenn man eine Sprache hat die 1-zu-1 in Strom an/aus über die maschine rattert und daher von deren physischen Aufbau abhängig ist oder vorher von einer Abstraktionsschicht behandelt wird deren Verhalten man genau spezifizieren kann.

[QUOTE=Sen-Mithrarin]Dass C++ für sowas kein genau definiertes Verhalten hat dürfte wohl auch in die Richtung gehen dass es, im Gegensatz zu Java, dort keine genau Lang-Spec gibt die vorschreibt wie sich ein Compiler in einem solchen Fall zu verhalten hat (was er also beim Erkennen eines Überlaufs für „sichere“ Werte einsetzen soll) - und, und ich denke dass dürfte die Ursache sein, weil es vom CPU abhängt wie dieser letztlich reagiert (im Sinne von wie er designed wurde und was dann das physische Ergebnis ist - also ob ALU overflow-Flag aktiviert oder nicht).
Ist halt schon was anderes wenn man eine Sprache hat die 1-zu-1 in Strom an/aus über die maschine rattert und daher von deren physischen Aufbau abhängig ist oder vorher von einer Abstraktionsschicht behandelt wird deren Verhalten man genau spezifizieren kann.[/QUOTE]

Nun, C++ ist ISO-standardisiert, und die Spec sagt klipp und klar

If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.

Die Rechtfertigung, die du angedeutet hast, ist wohl richtig. C+±Fans würden da auf die „„Vorteile““ hinweisen, dass man das ja auch voll gut auf einem embedded-8-bit-system umsetzen kann, ohne die Performanceeinbußen (!) die damit verbunden sind, unbedingt irgendeine Spec zu erfüllen. Vielleicht würden sie noch nachschieben, wie kagge Java ist, weil [irgendein Strawman-Argument].

Ich hätte es etwas weniger wohlwollend und verständnisvoll formuliert.

Nochmal: Es ist in C++ nicht möglich, zwei signed integers zu addieren (deren Werte man nicht kennt), ohne undefined behavior zu riskieren.

Das ist für eine Programmiersprache schon ein ziemliches Brett.

Natürlich, der C+±Standard sagt auch:

Note: most existing implementations of C++ ignore integer overflows.

Aber trotzdem ist die Tragweite dieser nicht-Spezifiziertheit kaum abzusehen. Wie auch immer sich eine dieser „most existing implementations“ heute verhält: Morgen kann es schon anders sein. Ich weiß, man kann über die Form bestimmter Aussagen streiten, und über viele andere Facetten, aber … ich habe zumindest ein gewisses Verständnis für felix-gcc aus https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475 (und das „Selbst Schuld, schnapp’ dir 'ne Kaffeetasse :p“ spare ich mir jetzt mal :wink: )

was passiert denn eigentlich real bei ‘undefined behavior’ auf den typischen Systemen?
dass Katzenvideos abgespielt werden usw. ist ja wenig realistisch, soviel Vertrauen auch in C++,

aber stürzt dann automatisch Restprogramm ab, ob aktiv vorgesehen oder oft unvermeidbare Folge des Fehlers,
oder wird eher doch nur etwas evtl. falsch gerechnet und das Programm läuft normal weiter?
edit: ach so, mit Note: most existing implementations of C++ ignore integer overflows. schon beantwortet…

so viel anders als in Java wäre das dann strenggenommen für die Praxis auch nicht:
da kann man auch fachlich annehmen dass 2000000000 + 2000000000 eine große positive Zahl wird,
und mit negativem Ergebnis dann viel Ärger haben, ist einfach Pech der Grenzen der strukturierten Programmiersprachen und des Weltraums allgemein

edit: dass sich Implementierungen ändern können sehe ich da nicht als besonders kritischer Punkt,
genauso gut kann die Implementierung als Fehler oder rein böswillig geändert werden, auch Java,
unabhängig von dem ‘undefined behavior’,

das ist keine Lizenz, beliebiges in eine C+±Implementierung einzubauen,
unpassendes wird entweder immer in hoffentlich vorhandener Überprüfung erkannt oder eben nicht

[QUOTE=SlaterB]was passiert denn eigentlich real bei ‚undefined behavior‘ auf den typischen Systemen?
dass Katzenvideos abgespielt werden usw. ist ja wenig realistisch, soviel Vertrauen auch in C++,

aber stürzt dann automatisch Restprogramm ab, ob aktiv vorgesehen oder oft unvermeidbare Folge des Fehlers,
oder wird eher doch nur etwas evtl. falsch gerechnet und das Programm läuft normal weiter?
edit: ach so, mit Note: most existing implementations of C++ ignore integer overflows. schon beantwortet…
[/quote]

Das ist das Problem mit Undefined Behavior: Das Verhalten ist undefiniert :wink: Sicher ist der Überlauf eines Integers ein sehr „kleines“ Beispiel, das „meistens“ noch recht „gutartig“ ist. Aber gerade deswegen erscheint mir das vollkommen absurd: Rein technisch steht dieser Überlauf auf der gleichen Stufe wie z.B. ein Zugriff auf eine ungültige Array-Position: Lesend praktisch immer OK, Schreibend geht’s manchmal gut, oder es kracht, oder es scheint zu funktionieren, aber bewirkt, dass später an einer ganz anderen Stelle im Programm ein Segfault auftritt.

Einerseits ja, aber den ersten entscheidenen Punkt hast du schon genannt: Es ist möglich, eine fachliche Entscheidung zu treffen, und es ist klar spezifiziert, welche Folgen ein „Fehler“ haben wird. In Java kann man bedenkenlos schreiben

int index = library.getIndex();
int offset = library.getOffset();
int position = index + offset;
System.out.println(position);

Wenn die „library“ da irgendwelchen Mist liefert, kann es sein, dass „position“ negativ wird. Joa, und das gibt er dann halt aus. In C++ kann man nicht sagen, was dort passieren wird. Praktisch müsste man sowas schreiben als


int index = library.getIndex();
int offset = library.getOffset();
int position = index + offset;
if (index >= INT_MAX - offset) { reportError(); }
if (offset >= INT_MAX - index) { reportError(); }
int position = index + offset;

Das ist ja nunmal vollkommen unrealistisch und weltfremd. Das macht NIEMAND (außer vielleicht in 10-fach reviewtem Code für die Notabschaltung von Atomkraftwerken).

Auch bei weiteren „Undefined Behaviors“ tritt das auf: Wenn kein Überlauf stattfindet, sondern einfach nur die library „falsche“ Werte liefert, und man mit dieser Position nun weiterrechnen würde, etwa
array[position] = 42;
dann könnte es in C++ sein, dass das „funktioniert“ (aber wie gesagt, später dann woanders kracht). In Java fliegt garantiert eine ArrayIndexOufOfBoundsException.

(Wichtig: Das geht jetzt deutlich über das ursprüngliche Beispiel hinaus. Dass Java immer Bounds-Checks macht, könnte man kritisieren. Dass C++ sie NIE macht, mag Vorteile haben oder sogar echt gerechtfertigt sein. Darum ging es aber ursprünglich nicht. Es ging vielmehr darum, dass eine der trivialsten, in der Programmierung am häufigsten auftretenden, elementarsten Operationen, nämlich die Addition zweier Zahlen, in C++ „praktisch“ nicht möglich ist, ohne das oben angedeutete Sicherheitskorsett).

[QUOTE=SlaterB;133796]
edit: dass sich Implementierungen ändern können sehe ich da nicht als besonders kritischer Punkt,
genauso gut kann die Implementierung als Fehler oder rein böswillig geändert werden, auch Java,
unabhängig von dem ‚undefined behavior‘,

das ist keine Lizenz, beliebiges in eine C+±Implementierung einzubauen,
unpassendes wird entweder immer in hoffentlich vorhandener Überprüfung erkannt oder eben nicht[/QUOTE]

Nun, da kann man unterschiedliche Schwerpunkte setzen. Wenn du mir heute Java Code gibst, der vor 20 Jahren mit Java 1.1 auf einem 32bit-Linux-System mit EMacs getippt wurde, dann ziehe ich den heute auf meinem 64bit-Windows-Rechner mit Java 8 per Drag&Drop in Eclipse rein, und dann ist er compiliert (garantiert), und er macht genau das, was er schon vor 20 Jahren gemacht hat (garantiert) (nur schneller :)). Diese Verläßlichkeit und Nachhaltigkeit weiß man vielleicht erst zu schätzen, wenn man mal vor der Aufgabe stand, ein Programm von „Visual C++ 6.0“ auf „Visual Studio 2010“ zu hieven, oder man sich bei der Verwendung von C++11-Features mit irgendwelchen #ifdef-Krämpfen auf den „sweet spot“ einzupendeln versuchte, der von VS und gcc 4.8.42 einigermaßen unterstützt wurde.

Dass sich ändernde Implementierungen DOCH ein Problem sind, sieht man an dem Bug-Report IMHO ganz gut. Lange war wohl if (x + 100 < x) eine gängige Methode, um auf Überlauf zu testen. Aber sie hat sich eben auf etwas verlassen, was (nicht nur „nicht spezifiziert“ sondern) undefiniertes Verhalten war - und von einer Compiler-Version zur nächsten passiert da dann auf einmal irgendwas komplett anderes.

[QUOTE=Marco13]In C++ kann man nicht sagen, was dort passieren wird. Praktisch müsste man sowas schreiben als


int index = library.getIndex();
int offset = library.getOffset();
int position = index + offset;
if (index >= INT_MAX - offset) { reportError(); }
if (offset >= INT_MAX - index) { reportError(); }
int position = index + offset;

Das ist ja nunmal vollkommen unrealistisch und weltfremd. Das macht NIEMAND (außer vielleicht in 10-fach reviewtem Code für die Notabschaltung von Atomkraftwerken).
[/QUOTE]
wieder zurück zum Punkt was passiert denn eigentlich real in ‘most existing implementations’?
ist je von einem Ausführen in einer halbwegs verbreiteten seriösen Implmentierung gehört worden, in der etwas anderes passierte als ein falscher int-Wert berechnet, also wie in Java?
kam es z.B. zum Überschreiben des Speicherplatz direkt daneben? oder was sonst noch denkbar

dass es in manchen/ allen C-Varianten keinen sicheren Arbeitsspeicher gibt hinsichtlich Pointer, evtl. Array-Zugriff,
das ist ja auch so bekannt und disqualifiziert die Sprache für alles bis auf vierfach-geprüfte Programme, möglichst geringe Komplexität, nur Geschwindigkeit ausnutzen

wenn das hier auch passiert dann eben eine weitere Stelle,
aber sieht doch eher nur nach Fehlberechnung aus, nicht ganz so dramatisch

[QUOTE=SlaterB]wieder zurück zum Punkt was passiert denn eigentlich real in ‘most existing implementations’?
ist je von einem Ausführen in einer halbwegs verbreiteten seriösen Implmentierung gehört worden, in der etwas anderes passierte als ein falscher int-Wert berechnet, also wie in Java?
kam es z.B. zum Überschreiben des Speicherplatz direkt daneben? oder was sonst noch denkbar

aber sieht doch eher nur nach Fehlberechnung aus, nicht ganz so dramatisch[/QUOTE]

Praktisch alle “normalen” C+±Implementierungen (außer vielleicht irgendwelcher exotischer embedded-crap mit Compilern aus den 80ern) wird da einen ganz normalen, gutartigen Überlauf generieren, genau wie Java auch.

Punkt.

Aber: Man weiß es nicht. Wenn die gcc-Entwickler morgen feststellen, dass es effizienter wäre, den Überlauf nicht so zu behandeln, sondern z.B. den Speicherplatz direkt daneben zu überschreiben, dann könnten sie das tun.

Nochmal Punkt.

Ich finde es eben einfach … nun, wie soll ich sagen … “untragbar”, dass so etwas “Undefined Behavior” ist, und beliebige, unabsehbare Konsequenzen haben darf. Wenn dort sowas stünde wie

"If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, then the resulting value is unspecified"

dann wäre das noch “vertretbar”. Dann würde 2000000000 + 2000000000 eben mal “42” ergeben. Aber so weiß man eben gar nichts. Das mit dem Katzenvideo war natürlich polemisch-suggestiv, aber sollte eben unmißverständlich klar machen, auf was für einem schmalen Grat man wandert, wenn man “ein ganz normales C+±Programm” schreibt. Und ich postuliere: Es gibt praktisch KEIN (nicht-triviales) C+±Programm, wo nicht irgendwo “Undefined Behavior” auftritt. Die JLS enthält die Worte “undefined behavior” AFAIK praktisch nie (es gibt natürlich “undefined behaviors” auf API-Level, aber nicht auf Sprachebene).

ich verstehe den Punkt, ist ja alles richtig, aber praktisch erscheint das doch wenig relevant,
wenn etwas über Jahrzehnte nicht real auftritt dann ist eine solche Änderung auch nicht groß zu unterscheiden von den anderen Fällen, die so oder so auftreten können, auch in Java:

  • menschlicher Fehler, Bug,
    eine solche Änderung „den Überlauf nicht so zu behandeln, sondern z.B. den Speicherplatz direkt daneben zu überschreiben“
    wäre sicher wenn kommend auch große Problemursache, die schnell bemerkt werden würde,
    ob dann auch korrigiert bringt den nächsten Punkt:

  • bewußte Kenntnisnahme der sicheren Definition wenn es sie gäbe/ der aktuellen Implementierung nur mit Falschberechnung,
    aber Ignorierung des Problems durch Änderung zum Schlechteren, gar direkt Widerspruch zur Sprachdefinition (wenn es sie gäbe, etwa in Java),

entweder direkt mit böswilliger Absicht, oder weil egal, Grenze als störend angesehen, ‚unsere Version ist so, lebt damit oder nehmt eine andere‘

  • schlicht Änderung der Sprachdefinition selber, ob in C++ oder Java,
    selbst wenn dort aktuell etwas sicheres stände, könnte man das einfach ändern zusammen mit der Implementierung,

mag sein dass es höhere rechtliche und Gremien-Hürden gibt usw., aber das klingt alles recht theoretisch,
genauso theoretisch wie das hier mal real ein Problem auftreten kann


„Undefined Behavior“ klingt für mich wie ein Sprachbaustein, genauere Einzeldefinitionen eingespart,
auch um Dinge wie Programmabbruch/ Exception an dieser Stelle zu erlauben (behaupte ich freilich nur, ohne genauere Kenntnisse zu sowas :wink: )

es ist wie schon mal genannt nicht als Blankoerlaubnis für Unsinn zu verstehen,
auch ohne Aufschreiben wird sicherlich Übereinkunft bestehen dass etwa nur falsches Ergebnis gerechnet

oder wenn Speicherüberlauf, dann auch allgemein bekannt, erscheint undenkbar, aber gibt es ja so oft in dieser Sprache,
könnte also durchaus auch die Konvention sein, aber wenn dann weil bekannter Standard in der Sprache,
über Jahrzehnte gebildet in allen wichtigen Implementierungen

eine plötzliche Änderung oder Abweichung vom allgemeinen Standard könnte sich eine seriöse Implementierung nicht leisten,
glatter Selbstmord, sowas passiert nicht, außer evtl. als Bug, ob mit oder ohne Sprachdefinition, kein reales Problem

Tja, das mit undefined behaviour bei elementaren Operationen wie bei der Addition ist bescheiden. Fefe hat schon dazu geschrieben, dass man Compiler-Builtins verwenden sollte, wenn man Bock auf Overflow-Checks hat ;-).

[QUOTE=SlaterB]ich verstehe den Punkt, ist ja alles richtig, aber praktisch erscheint das doch wenig relevant,

  • menschlicher Fehler, Bug,

    genauso theoretisch wie das hier mal real ein Problem auftreten kann

[/quote]

lange gewohnt, abgehärtet, gutwillig, parsen trotzdem schwer, Grammatik erfüllt Zweck

Natürlich ist das bei dem konkreten Beispiel ein eher theoretisches Problem. Aber bei etwas so lebenswichtigem wie der Bedeutung von Quellcode in einer der verbreitetsten Programmierprachen der Welt, erscheint es mir gelinde gesagt haarsträubend, dass sich das Standardkommitee offenbar nicht darauf einigen konnte, etwas so trivialem, elementaren wie einer Addition eine klare Bedeutung zuzusichern. Auch wenn von den >1300 Seiten, aus denen der C+±Standard besteht, vermutlich schonmal 100 wegfallen würde, wenn man die Worte „undefined behavior“ mit „UB“ abkürzen würde, ist dort doch aller möglicher Sche!ß bis ins kleinste ausstandardisiert - aber eine Addition eben nicht.

Nochmal zusammengefasst:

In der Praxis wird dieses spezielle UB kein Problem sein. Es gibt Fälle von UB, bei denen sicher"er" ein Problem auftritt (etwa wenn man ungültigen Speicher schreibt). Aber rein technisch stehen die beiden UBs auf der gleichen Stufe. Und wie der Bugreport zeigt, GIBT es Änderungen, die sich darauf verlassen, dass kein Programmierer UB verursacht. Und damit liegen die gcc-Entwickler schlicht falsch - weil diese Annahme dadurch, dass sie eben gerade auch schon bei solchen Trivialitäten wie einer Addition auftritt, unrealistisch ist.

(Und wenn jetzt jemand behauptet, man solle jede Stelle im Code, wo


int z = x + y;

auftucht, ändern in


int z;
bool fial = __builtin_add_overflow(x,y, &z);
if (fial) exit(-666);

dann kann ich das irgendwie nicht so ernst nehmen. Ja, ich weiß, das ist polemisch, weil es meistens nicht relevant ist, aber ich finde die UB in diesem speziellen Fall eben schon arg irritierend…)

[QUOTE=Marco13]
(Und wenn jetzt jemand behauptet, man solle jede Stelle im Code, wo


int z = x + y;

auftucht, ändern in


int z;
bool fial = __builtin_add_overflow(x,y, &z);
if (fial) exit(-666);

dann kann ich das irgendwie nicht so ernst nehmen. Ja, ich weiß, das ist polemisch, weil es meistens nicht relevant ist, aber ich finde die UB in diesem speziellen Fall eben schon arg irritierend…)[/QUOTE]
das ist doch jetzt schon wieder in Java genauso, auch dort schreibt man int + int, niemand prüft bei jeder Addition auf Overflow, der in Java genauso auftreten kann,

Argumente drehen sich bisschen im Kreis, ich sehe es selbst

Ja, ich finde auch, es wiederholt sich. Ich finde, dass eine Addition UB verursacht ist kaum tragbar (für eine formale, verbreitete, standardisierte, ggf. sicherheitskritische Sprache, und in Anbetracht der Bedeutung, die UB haben kann). Deine Argumente gehen in Richtung von „ist doch nicht so schlimm“ :wink: Der Überlauf kann in Java auch auftreten, aber es ist eben klar, was dann passiert: Es läuft über. Keine Katzenvideos. Kein neuer, cleverer Compiler der irgendwas wegoptimiert. Alles klar definiert.

Herbe Enttäuschung, Java sollte auch sein Feature haben ;-).

Sogar, dass ein Integer-Overflow undefiniertes Verhalten hervorrufen kann. Insofern ist es über den Standard definiert ;-).

0,02€
[spoiler]
Ich finde es faszinierend das eine Sprache, die einem Verantwortung aufzwingt vernünftig zu arbeiten, für ein Geschreibsel auslöst.

Und ganz ehrlich UB finde ich sehr genau definiert. Auf einem Microcontroller, der nur 512 Bytes Speicher hat, ist mir RTTI und Overflow-Checks und was weis ich noch alles voll kommen egal. Es frisst Speicher den ich nicht habe. Also verfällt C/C++ darauf zurück was der Prozessor macht und da darf jeder Prozessor was anderes machen. Ein Prozessor muss kein Overflow-Flag bereit stellen…
[/spoiler]

Yo, sehe ich auch so. Ein UB ist einfach ein Fall den es zu vermeiden gilt. Wenn man den Wertebereich der Summanden nicht kennt dann prüft man halt auf einen Überlauf - ansonsten nimmt man in kauf das es zu einem gigo-error kommt (oder die Notabschaltung des AKWs mit einem Coredump aussteigt). Aber das ist in Java auch nicht anders, auch da wird es bei einer unvorhergesehenen Bereichsüberschreitung zu einem gigo-error kommen. Oder die Notabschaltung des AKWs steigt mit einer ArrayIndexOutOfBounds-Exception aus. Big Deal…