Wie kann ich verhindern, dass der gcc-Optimierer falsche Bitoperationen erzeugt?

Lesezeit: 7 Minuten

Benutzer-Avatar
Merline2011

Betrachten Sie das folgende Programm.

#include <stdio.h>

int negative(int A) {
    return (A & 0x80000000) != 0;
}
int divide(int A, int B) {
    printf("A = %d\n", A);
    printf("negative(A) = %d\n", negative(A));
    if (negative(A)) {
        A = ~A + 1;
        printf("A = %d\n", A);
        printf("negative(A) = %d\n", negative(A));
    }
    if (A < B) return 0;
    return 1;
}
int main(){
    divide(-2147483648, -1);
}

Wenn es ohne Compileroptimierungen kompiliert wird, liefert es die erwarteten Ergebnisse.

gcc  -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1

Wenn es mit Compileroptimierungen kompiliert wird, erzeugt es die folgende falsche Ausgabe.

gcc -O3 -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative 
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0

ich renne gcc version 5.4.0.

Gibt es eine Änderung, die ich im Quellcode vornehmen kann, um zu verhindern, dass der Compiler dieses Verhalten unter erzeugt -O3?

  • A = ~A + 1; ist UB, wenn A == INT_MINdas +1 macht einen vorzeichenbehafteten Integer-Überlauf.

    – mch

    13. Februar 2018 um 7:54 Uhr

  • Ist nicht 0x7FFFFFFF + 1 undefiniertes Verhalten sowieso für 32-Bit int Typ?

    – Wetterfahne

    13. Februar 2018 um 7:54 Uhr

  • @mch, ich denke du hast den Nagel auf den Kopf getroffen. Es ist mir nie in den Sinn gekommen, dass sich der Überlauf von vorzeichenbehafteten Ganzzahlen nicht wie ein Überlauf von vorzeichenlosen Ganzzahlen verhält. Ich habe gerade diese Frage gesehen, also kenne ich jetzt den Unterschied.

    – merlin2011

    13. Februar 2018 um 7:56 Uhr


  • @Someprogrammerdude Nein, absolut ist nicht bekannt dafür, fehlerhaften Code zu generieren. Sicher, es gibt Compiler-Bugs in GCC (alles hat Bugs), aber -O3 ist ein sicherer Standard, und es gibt in dieser Einstellung nicht wesentlich mehr Fehler, die die Korrektheit beeinträchtigen, als in GCC im Allgemeinen. stackoverflow.com/a/11546263/1968

    – Konrad Rudolf

    13. Februar 2018 um 12:23 Uhr

  • @Voo Es ist fair zu sagen, dass dies der Fall ist plausibel. Aber abgesehen von Beweisen ist dies bei aktuellen Compilern einfach nicht der Fall (-O3 gewöhnt an experimentell und daher fehlerhaft sein; aber schon lange nicht mehr). Zu sagen, dass es „bekannt dafür ist, dass es manchmal fehlerhaften Code generiert“, ist absolut falsch.

    – Konrad Rudolf

    13. Februar 2018 um 16:37 Uhr


Benutzer-Avatar
Kunst

  1. -2147483648 macht nicht das, was du denkst. C hat keine negativen Konstanten. Enthalten limits.h und verwenden INT_MIN stattdessen (so ziemlich jede INT_MIN Definition auf Zweierkomplementmaschinen definiert es als (-INT_MAX - 1) aus einem guten Grund).

  2. A = ~A + 1; ruft undefiniertes Verhalten auf, weil ~A + 1 verursacht einen ganzzahligen Überlauf.

Es ist nicht der Compiler, es ist Ihr Code.

  • Ruft die zweite Zeile undefiniertes Verhalten auf nur wenn die Eingabe ist INT_MIN?

    – merlin2011

    13. Februar 2018 um 8:03 Uhr

  • @merlin2011 ja.

    – Kunst

    13. Februar 2018 um 8:06 Uhr

  • stackoverflow.com/a/3803981/995714 stackoverflow.com/q/26003893/995714 -214748364 ist eigentlich ein unsigned long konstant auf C90 stackoverflow.com/q/25658485/995714

    – phuklv

    13. Februar 2018 um 13:18 Uhr

  • Okay, ich muss fragen. Was ist los? Wer hat das verlinkt? Ich bekomme wahnsinnig viele Stimmen für eine triviale Antwort auf eine triviale Frage.

    – Kunst

    14. Februar 2018 um 11:42 Uhr

Der Compiler ersetzt Ihre A = ~A + 1; Aussage mit einer einzigen neg Anweisung, dh dieser Code:

int just_negate(int A) {
    A = ~A + 1;
    return A;
}

wird kompiliert zu:

just_negate(int):
  mov eax, edi
  neg eax         // just negate the input parameter
  ret

Aber der Compiler ist auch schlau genug, das zu erkennen, wenn A & 0x80000000 war vor der Negation ungleich Null, it muss nach Negation null sein, es sei denn, Sie verlassen sich auf undefiniertes Verhalten.

Dies bedeutet, dass die zweite printf("negative(A) = %d\n", negative(A)); kann “sicher” optimiert werden auf:

mov edi, OFFSET FLAT:.LC0    // .string "negative(A) = %d\n"
xor eax, eax                 // just set eax to zero
call printf

Ich nutze das Online Godbolt-Compiler-Explorer um die Assembly auf verschiedene Compiler-Optimierungen zu überprüfen.

  • Dies ist eine nützliche Erklärung für warum Der Compiler erzeugt bei hohen Optimierungsstufen unerwartete Ergebnisse. Es sucht nicht nach Möglichkeiten, den Programmierer auszutricksen, wenn er UB aufruft; Es sucht nach Möglichkeiten, den Code schneller laufen zu lassen, und geht davon aus, dass kein UB dies tun kann.

    – Martin Bonner unterstützt Monika

    13. Februar 2018 um 10:28 Uhr

Um im Detail zu erklären, was hier vor sich geht:

  • In dieser Antwort gehe ich davon aus long ist 32 Bit und long long ist 64 Bit. Dies ist der häufigste Fall, aber nicht garantiert.

  • C hat keine vorzeichenbehafteten ganzzahligen Konstanten. -2147483648 ist eigentlich typ long longauf die Sie den unären Minusoperator anwenden.

    Der Compiler wählt den Typ der Integer-Konstante aus, nachdem er überprüft hat, ob 2147483648 kann passen:

    • Innen ein int? Nein, ich kann nicht.
    • In einem long? Nein, ich kann nicht.
    • In einem long long? Ja, kann es. Der Typ der ganzzahligen Konstante wird daher sein long long. Wenden Sie dann ein unäres Minus darauf an long long.
  • Dann versuchst du, dieses Negativ zu zeigen long long zu einer Funktion, die ein erwartet int. Ein guter Compiler könnte hier warnen. Sie erzwingen eine implizite Konvertierung in einen kleineren Typ (“Lvalue-Konvertierung”).
    Unter der Annahme des Zweierkomplements ist der Wert jedoch -2147483648 kann in ein passen intsodass für die Konvertierung kein implementierungsdefiniertes Verhalten erforderlich ist, was sonst der Fall gewesen wäre.
  • Der nächste knifflige Teil ist die Funktion negative wo Sie verwenden 0x80000000. Dies ist kein int weder, noch ist es ein long longaber ein unsigned int (Siehe dies für eine Erklärung).

    Beim Vergleich Ihrer bestandenen int mit einem unsigned int“die üblichen arithmetischen Konvertierungen” (siehe hier) erzwingen eine implizite Konvertierung in die int zu unsigned int. Es beeinflusst das Ergebnis in diesem speziellen Fall nicht, aber das ist der Grund gcc -Wconversion Benutzer erhalten hier eine nette Warnung.

    (Hinweis: aktivieren -Wconversion schon! Es ist gut, um subtile Fehler zu fangen, aber nicht Teil davon -Wall oder -Wextra.)

  • Als nächstes tust du es ~Aeine bitweise Umkehrung der binären Darstellung des Werts, die mit dem Wert endet 0x7FFFFFFF. Dies ist, wie sich herausstellt, derselbe Wert wie INT_MAX auf Ihrem 32- oder 64-Bit-System. Daher 0x7FFFFFFF + 1 ergibt einen vorzeichenbehafteten Integer-Überlauf, der zu undefiniertem Verhalten führt. Aus diesem Grund verhält sich das Programm falsch.

    Frech könnten wir den Code ändern in A = ~A + 1u; und plötzlich funktioniert alles wie erwartet, wieder wegen impliziter Integer-Promotion.


Gewonnene Erkenntnisse:

In C sind Integer-Konstanten sowie implizite Integer-Promotions sehr gefährlich und nicht intuitiv. Sie können die Bedeutung des Programms auf subtile Weise vollständig ändern und Fehler einführen. Bei jeder einzelnen Operation in C müssen Sie die tatsächlichen Typen der beteiligten Operanden berücksichtigen.

Herumspielen mit C11 _Generic könnte eine gute Möglichkeit sein, die tatsächlichen Typen zu sehen. Beispiel:

#define TYPE_SAFE(val, type) _Generic((val), type: val)
...
(void) TYPE_SAFE(-2147483648, int); // won't compile, type is long or long long
(void) TYPE_SAFE(0x80000000, int);  // won't compile, type is unsigned int

Eine gute Sicherheitsmaßnahme, um sich vor Fehlern wie diesen zu schützen, besteht darin, immer stdint.h und MISRA-C zu verwenden.

  • @Groo _Generic ist ein wirklich nettes Feature und allein schon ein Grund, sich für C11 zu entscheiden. Es gibt anscheinend welche mehrdeutiges Verhalten darüber noch, wo verschiedene Compiler den Standard unterschiedlich interpretieren. Hoffentlich wird dies in Cxx behoben.

    – Ludin

    14. Februar 2018 um 11:04 Uhr

Benutzer-Avatar
Matteo Italien

Sie verlassen sich auf undefiniertes Verhalten. 0x7fffffff + 1 für 32-Bit-Ganzzahlen mit Vorzeichen führt zu einem Überlauf von vorzeichenbehafteten Ganzzahlen, was gemäß dem Standard ein undefiniertes Verhalten ist, sodass alles möglich ist.

In gcc können Sie Wraparound-Verhalten erzwingen, indem Sie übergeben -fwrapv; dennoch, wenn Sie keine Kontrolle über die Flags haben – und ganz allgemein, wenn Sie ein tragbareres Programm wollen – sollten Sie all diese Tricks anwenden unsigned Ganzzahlen, die vom Standard umbrochen werden müssen (und im Gegensatz zu vorzeichenbehafteten Ganzzahlen eine gut definierte Semantik für bitweise Operationen haben).

Konvertieren Sie zuerst die int zu unsigned (nach dem Standard gut definiert, liefert das erwartete Ergebnis), mach dein Zeug, konvertiere zurück zu int – implementierungsdefiniert (≠ undefiniert) für Werte größer als der Bereich von intsondern tatsächlich von jedem Compiler definiert, der im 2er-Komplement arbeitet, um das “Richtige” zu tun.

int divide(int A, int B) {
    printf("A = %d\n", A);
    printf("negative(A) = %d\n", negative(A));
    if (negative(A)) {
        A = ~((unsigned)A) + 1;
        printf("A = %d\n", A);
        printf("negative(A) = %d\n", negative(A));
    }
    if (A < B) return 0;
    return 1;
}

Ihre Version (bei -O3):

A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0

Meine Version (bei -O3):

A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1

1370090cookie-checkWie kann ich verhindern, dass der gcc-Optimierer falsche Bitoperationen erzeugt?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy