
ZIG und C's undefined-behavior
« | 05 May 2024 | »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.