ZIG und C's undefined-behavior

Ich baue meine Win32 App mit MSVC: läuft.
Ich baue meine Win32 App mit MinGW: läuft.
Ich baue meine Win32 App mit CLANG: läuft.
Ich baue meine Win32 App mit ZIG: Prozess weg.

W-T-F? Was macht ZIG, das auf LLVM aufsetzt, anders?

Spoiler: ZIG ist einfach nur genial!


Finde den Fehler:

 1void foo(char const* ptr_chars, size_t slice_offset) {
 2  assert(
 3    ((ptr_chars == NULL) && (slice_offset == 0))
 4    || (ptr_chars != NULL)
 5  );
 6
 7  char const* const ptr_char = ptr_chars + slice_offset;
 8
 9  if(ptr_char != NULL) {
10    do_something_with(*ptr_char);
11  }
12  else {
13    printf("Nothing to do");
14  }
15}

Unser assert() geht immer OK durch, weil entweder beide Funktionsparameter gesetzt sind, oder beide auf NULL bzw 0 verweisen.

Leider führt zig cc in der Folgezeile manchmal ein abort() aus.

Und der Grund lautet auf:

Undefiniertes Verhalten

Setzt man die Zeile
char* ptr_char = ptr_chars + slice_offset; stur in Maschinensprache um, ist auch alles perfekt in Ordnung, denn entweder entsteht ein nicht-NULL-Pointer zu Daten oder ein NULL-Pointer (weil dann ja beide Parameter 0 sind).

Der nachfolgende Code prüft auf den NULL-Pointer und sortiert ihn beim Auftreten aus.

Doch leider darf man mit NULL Pointer nur zwei Dinge machen:

  • Man darf sie zuweisen
    my_ptr = NULL;
  • Man darf sie mit anderen Pointern vergleichen
    if(my_ptr == NULL)

Der Fehler liegt bereits in der Addition, denn wenn ptr_chars schon NULL ist, darf man keinen Wert addieren, subtrahieren oder anders verknüpfen.

Rein logisch ist NULL + 0 wieder NULL, und zur Laufzeit stimmt das auch.

Doch wenn der Compiler schon beim Übersetzen nachvollziehen kann, dass beide Parameter auf NULL bzw. 0 gesetzt werden, dann weiß er, dass es sich bereits um undefiniertes Verhalten handelt und dann darf er auch jeden beliebigen Blödsinn machen.

zig cc bricht dann den Prozess ab, während meine anderen Compiler einen durchlaufenden Prozess ermöglichen.

Lösung

 1void foo(char const* ptr_chars, size_t slice_offset) {
 2  assert(
 3    ((ptr_chars == NULL) && (slice_offset == 0))
 4    || (ptr_chars != NULL)
 5  );
 6
 7  if(ptr_chars == NULL) {
 8    printf("Nothing to do");
 9    return;
10  }
11  else {
12    char const* const ptr_char = ptr_chars + slice_offset;
13    do_something_with(*ptr_char);
14  }
15}

Wenn wir einen NULL-Pointer haben, darf er nicht angefasst werden, nur Vergleiche sind zulässig.

Solche Pointer-Additionen kamen bei mir leider vor, und zwar auch bei “wissentlich” leeren Objekten und diese waren schlicht falsch.
(Liebe Grüße vom Leer-String-Objekt!)

Fazit

zig, ich liebe dich!

Alle Tools und Tests laufen jetzt auch durch, wenn sie von zig cc gebaut wurden.

Natürlich kann man darüber diskutieren, dass manche Compiler den Begriff des UB (Undefined Behavior) überinterpretieren.
Täten sie genau das, was ich ihnen vorschreibe, wäre alles in Ordnung.

Doch so ist es nicht. UB Regeln sind absolut relevant und ich bin echt froh, dass mich zig nun auf ein altes “Denkproblem” aufmerksam gemacht hat.

zig hat damit maßgeblich die Korrektheit und Speichersicherheit im GATE Framework verbessert.

📧 📋 🐘 | 🔗 🔔
 

Meine Dokus über:
 
Weitere externe Links zu:
Alle extern verlinkten Webseiten stehen nicht in Zusammenhang mit opengate.at.
Für deren Inhalt wird keine Haftung übernommen.



Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!