, wie zu überprüfen, dass das Verhalten in c nicht definiert ist?

7

Ich weiß, dass das Folgende nicht definiert ist, weil ich versuche, den Wert einer Variablen in demselben Ausdruck zu lesen und zu schreiben, nämlich

%Vor%

Aber wenn es so ist, warum ist das folgende Code-Snippet nicht undefiniert

%Vor%

wie auch hier versuche ich den Wert von a zu ändern und schreibe gleichzeitig dazu.

Erklären Sie auch, warum der Standard dies nicht heilt oder dieses undefinierte Verhalten beseitigt , obwohl sie wissen, dass es undefiniert ist?

    
YakRangi 24.03.2014, 07:53
quelle

5 Antworten

3

Kurz gesagt, Sie können jedes definierte Verhalten im Standard finden. Alles was dort nicht definiert ist - ist undefiniert.

Intuitive Erklärung zu Ihrem Beispiel:

%Vor%

Sie möchten die Variable a zweimal in einer einzigen Anweisung ändern.

%Vor%

Wenn Sie hier nachsehen:

%Vor%

Sie ändern die Variable a nur einmal:

%Vor%

Warum definiert der Standard nicht a=a++ behavior?

Einer der möglichen Gründe ist: Der Compiler kann Optimierungen durchführen. Je mehr Fälle Sie in einem Standard definieren, desto weniger Freiheits-Compiler muss Ihren Code optimieren. Da unterschiedliche Architekturen verschiedene Implementierungen mit zunehmenden Anweisungen aufweisen können, würde der Compiler nicht alle Prozessoranweisungen verwenden, falls sie das Standardverhalten brechen. Oder in einigen Fällen kann der Compiler die Auswertungsreihenfolge ändern, aber diese Einschränkung wird einen Compiler zwingen, solche Optimierungen zu deaktivieren, wenn Sie etwas zweimal modifizieren möchten.

    
JustAnotherCurious 24.03.2014, 08:05
quelle
6

Der Grund, warum es nicht definiert ist, ist nicht, dass Sie lesen und schreiben, sondern dass Sie zweimal schreiben.

a++ bedeutet, a zu lesen und nach dem Lesen zu erhöhen, aber wir wissen nicht, ob ++ vor der Zuweisung mit = passieren wird (in diesem Fall überschreibt = den alten Wert von a ) oder nach, in diesem Fall wird ein inkrementiert.

Verwenden Sie einfach a++; :)

a = a + 1 hat das Problem nicht, da a nur einmal geschrieben wird.

    
CMoi 24.03.2014 08:00
quelle
6
  

warum das folgende Code-Snippet nicht undefiniert ist

%Vor%

Der Standard besagt, dass

  

Zwischen dem vorherigen und dem nächsten Sequenzpunkt darf ein gespeicherter Wert eines Objekts höchstens einmal durch die Auswertung eines Ausdrucks geändert werden. Darüber hinaus soll auf den früheren Wert nur zugegriffen werden, um den Wert zu bestimmen, der gespeichert werden soll.

Im Fall von a = a + 1 ; a wird nur einmal geändert, und der vorherige Wert von a wird nur abgerufen, um den Wert zu ermitteln, der in a gespeichert werden soll.
Im Fall von a=a++; wird a mehr als einmal geändert - um ++ operator in Unterausdruck a++ und um = operator bei der Zuweisung des Ergebnisses nach links a . Jetzt ist nicht definiert, welche Änderung, entweder von ++ oder von = , zuerst stattfindet .

Fast alle modernen Compiler mit dem Flag -Wall würden beim Kompilieren des ersten Snippets eine Warnung ausgeben:

%Vor%

Weitere Informationen: Wie kann ich komplexe Ausdrücke wie die in diesem Abschnitt verstehen und vermeiden, nicht definierte Ausdrücke zu schreiben?

    
haccks 24.03.2014 07:56
quelle
3

Der ++ Operator fügt eins zu a hinzu, was bedeutet, dass die Variable a zu einer + 1 wird. In der Tat sind die folgenden beiden Aussagen gleich:

%Vor%

Die letzte Anweisung, a + 1, erhöht nicht a - es erzeugt ein Ergebnis mit dem Wert a + 1. Wenn Sie wollen, dass a zu + 1 wird, müssen Sie das Ergebnis von a + 1 zu a zuordnen mit

%Vor%

Der Grund, warum die erste Anweisung nicht funktioniert, ist, dass Sie etwas wie

schreiben %Vor%     
EagleV_Attnam 24.03.2014 08:02
quelle
2

Andere haben bereits über die Details Ihres speziellen Beispiels gesprochen. Daher füge ich einige allgemeine Informationen und Tools hinzu, die helfen, undefiniertes Verhalten zu erfassen.

Es gibt kein ultimatives Werkzeug oder eine Methode, um undefiniertes Verhalten zu erfassen. Selbst wenn Sie alle diese Tools verwenden, gibt es keine Garantie dafür, dass sich in Ihrem Code nicht etwas befindet, das nicht definiert ist. Aber IME diese werden eine Menge der gemeinsamen Probleme fangen. Ich führe nicht die Standard-Good-Practices der Softwareentwicklung auf, wie Unit-Testing, die Sie ohnehin verwenden sollten.

  • clang (-analyze) verfügt über mehrere Optionen, die beim Erfassen undefinierten Verhaltens sowohl zur Kompilierungszeit als auch zur Laufzeit hilfreich sein können. Es hat -ftrapv, es hat neu Unterstützung für kanarische Werte, seine Adresse Sanitizer, --fcatch-undefined-Verhalten, etc. erhalten.

  • gcc hat auch mehrere Optionen, um undefiniertes Verhalten zu erfassen, wie zum Beispiel Schmutzfänger, seine Adressen-Desinfektion, den Stack-Protector.

  • valgrind ist ein fantastisches Werkzeug, um zur Laufzeit speicherbedingtes undefiniertes Verhalten zu finden.

  • frama-c ist ein statisches Analysewerkzeug, das undefiniertes Verhalten finden und visualisieren kann. Die Fähigkeit, toten Code zu finden (undefiniertes Verhalten kann oft dazu führen, dass andere Teile des Codes unbrauchbar werden) ist ein ziemlich nützliches Werkzeug, um mögliche Sicherheitsbedenken aufzuspüren. frama-c hat viele erweiterte Funktionen, kann aber wohl schwieriger zu benutzen sein als ...

  • Andere kommerzielle statische Analysewerkzeuge, die undefiniertes Verhalten auffangen können, gibt es zB in PVS-studio, klocwork und so weiter. Diese kosten jedoch normalerweise viel.

  • Kompilieren Sie mit verschiedenen Compilern und für seltsame Architekturen. Wenn Sie können, warum kompilieren und führen Sie Ihren Code auf einem 8-Bit-AVR-Chip aus? Ein Himbeer-Pi (32-Bit-ARM)? Kompilieren Sie es mit Javascript mit emscripten und führen Sie es in V8? Dies tendiert dazu, ein praktisches Mittel zu sein, um undefiniertes Verhalten einzufangen, das zu Abstürzen führen würde (aber wenig / nichts, um lauernde UB zu fangen, die beispielsweise Sicherheitsprobleme verursachen könnten).

Nun zu den ontologischen Gründen, warum undefiniertes Verhalten existiert ... Es ist im Grunde für die Leistung und Einfachheit der Implementierung Gründe. Viele Dinge, die UB in C sind, erlauben es dem Compiler bestimmte Dinge zu optimieren, die andere Sprachen nicht optimieren können. Wenn Sie z.B. Vergleichen Sie, wie Java, Python und C den Überlauf von vorzeichenbehafteten Integer-Typen handhaben, Sie können sehen, dass Python es an einem extremen Ende vollständig in einer für den Programmierer bequemen Weise gut definiert - Ints können tatsächlich unendlich groß werden. C am anderen Ende des Spektrums lässt es undefiniert - es liegt in Ihrer Verantwortung, Ihre signierten Ganzzahlen niemals zu überlaufen. Java ist etwas dazwischen.

Aber andererseits bedeutet das, dass in Python nicht bekannt ist, welche Arbeit die "int + int" -Operation tatsächlich ausführen wird, wenn sie ausgeführt wird. Es kann viele hundert Anweisungen ausführen, eine Rundreise durch das Betriebssystem machen, um etwas Speicher zuzuweisen, und so weiter. Das ist ziemlich schlecht, wenn Sie viel Wert auf Leistung oder genauer auf konsistente Leistung legen. C am anderen Ende des Spektrums erlaubt es dem Compiler, "+" dem nativen Befehl der CPU zuzuordnen, der ganze Zahlen hinzufügt (falls einer existiert). Sicher, verschiedene CPUs können Überläufe anders handhaben, aber da C das undefiniert verlässt, ist das in Ordnung - Sie als Programmierer müssen dafür sorgen, dass Ihre Ints nicht überlaufen. Das bedeutet, dass C dem Compiler die Option gibt, Ihre "int + int" -Operationen auf einer einzigen Maschinenanweisung auf so ziemlich allen CPUs zu kompilieren - etwas, das Compiler nutzen können und tun.

Beachten Sie, dass C keine Garantie dafür gibt, dass + tatsächlich direkt auf einen nativen CPU-Befehl abgebildet wird, es lässt dem Compiler nur die Möglichkeit, dies auf diese Weise offen zu legen - und offensichtlich wäre dies ein beliebiger Compiler-Schreiber Vorteil von. Javas Methode zur Definition von vorzeichenbehafteten Integer-Überläufen ist weniger vorhersehbar (in Bezug auf die Leistung) als Pythons, kann aber nicht dazu führen, dass + bei vielen CPU-Typen in einen einzelnen CPU-Befehl umgewandelt wird, wo C dies zulässt.

Im Wesentlichen versucht C also undefiniertes Verhalten einzubeziehen und entscheidet sich für (konsistente) Geschwindigkeit und einfache Implementierung, wenn andere Sprachen sich für sicheres oder vorhersagbares Verhalten entscheiden (aus Sicht der Programmierer). Das ist keine gute Entscheidung mit z in Bezug auf die Sicherheit, aber das ist, wo C steht. Es läuft darauf hinaus, "das geeignete Werkzeug für die anstehende Aufgabe zu kennen", und es gibt definitiv viele Fälle, in denen die Vorhersagbarkeit der Leistung C für Sie absolut notwendig ist.

    
Amadiro 24.03.2014 08:37
quelle