Mit gcc 4.9.2 20150304 64 Bit stieß ich auf dieses scheinbar seltsame Verhalten:
%Vor% Im Code ordne ich einen double
auf dem Heap zu, initialisiere ihn und gebe dann ein weiteres double
initialisiert mit der Adresse des ersten in ein intptr_t
umgewandelt zurück. Dies erzeugt mit der Optimierung -O2
im 32-Bit-Modus den folgenden Assembly-Code:
und erstaunlicherweise ist die Initialisierung des zugewiesenen double
komplett weg.
Beim Generieren von Code mit -O0
funktioniert alles wie erwartet und der entsprechende Code ist stattdessen:
Habe ich irgendetwas Ungültiges getan (ich denke speziell an Aliasing-Regeln, auch wenn es mir scheint, dass das Überspringen der Initialisierung keine Rechtfertigung hat) oder ist das nur ein GCC-Bug?
Beachten Sie, dass das gleiche Problem beim Kompilieren auf 64-Bit-Code auftritt (formal intptr_t
im 64-Bit-Modus ist 8 Bytes und daher kann ad double
es nicht genau darstellen ... das tut das nicht t passiert aber, weil auf x86-64 nur 48 der 64 Bits von Adressen verwendet werden und ein double
kann genau diese Werte darstellen).
Es scheint ein Bug ... sogar mit dem vereinfachten Code
%Vor% bei der Kompilierung mit -O2
initialisiert den Speicher nicht.
Der Code funktioniert korrekt und gibt direkt intptr_t
(oder unsigned long long
) zurück; aber es nach der Konvertierung in ein double
zurückgeben funktioniert nicht, da gcc
anscheinend davon ausgeht, dass Sie in diesem Fall nicht mehr auf den Speicher zugreifen können.
Dies ist im 32-Bit-Modus eindeutig falsch (wobei intptr_t
4 Byte und double
53 Bits Genauigkeit für Ganzzahlen angibt), aber auch für den 64-Bit-Modus, wobei uintptr_t
tatsächlich 8 Byte verwendete Werte sind sind 48 Bits).
Ich bin mir nicht sicher, aber das Problem könnte mit der "toten Code-Eliminierung auf Baum" ( -ftree-dce
) zusammenhängen. Beim Kompilieren im 32-Bit-Modus, der Optimierungen -O2
aktiviert, aber dieses spezifische mit -fno-tree-dce
deaktiviert, ändert sich die Programmausgabe und ist korrekt, aber der generierte Code ist nicht .
Genauer gesagt enthält die nicht-inlinierte Version von doit
keinen Initialisierungscode, aber der in main
erzeugte Code inlinert den Aufruf und der Optimierer "weiß", dass der Wert des Speichers 3.14
ist und druckt diesen direkt in Ausgabe.
Bestätigt als Fehler, bereits im Stamm behoben.
Workaround bis zur nächsten Veröffentlichung ist -fno-tree-pta
Die Optimierung erlaubt es, Code im Falle von UB zu entfernen, aber hier sollte es nicht.
Sie haben eine unnötige Umwandlung in Value *ptr = (Value *)malloc(sizeof(Value));
, aber das sollte harmlos sein.
Diese Zeile res.d = (unsigned long long) ptr;
sollte besser res.d = (intptr_t) ptr;
sein, weil intptr_t
explizit Zeiger empfangen darf, und Sie können dann einen ganzzahligen Wert in einer double
-Variable setzen: Sie können die Genauigkeit verlieren, sollten es aber nicht sei UB.
Ich kann es nicht testen (weil ich kein gcc 4.9 habe), aber wenn du das gleiche Problem hast:
%Vor%Ich würde auf einen gcc-Fehler schließen.
Ich könnte versuchen, die vereinfachte Version des Codes mit clang Version 3.4.1 unter FreeBSD 10.1 zu kompilieren
cc -O3 -S doit.c
gibt (zum Codeteil hinuntergestreift):
Es ist nicht die gleiche Kompilierung wie gcc, aber clang macht die 3.14
Initialisierung sogar bei -O3 Optimierungsebene (dump hex für 3.14 ist 0x40091eb851eb851f
)
Nach dem Lesen anderer Kommentare und Antworten denke ich, dass die eigentliche Ursache des Problems darin liegt, dass gcc die Zwischenbesetzung überspringt und return (double)((uintptr_t) ptr);
als return (double) ptr;
liest - na ja, nicht genau, weil es dann so wäre Sei ein Syntaxfehler, aber es gibt immer noch UB, da am Ende ein Zeigerwert in eine doppelte Variable endet. Aber wenn wir die Zeile mit der Zwischenform zerlegen, sollte sie (IMHO) wie folgt gelesen werden:
Ich sehe hier nichts Seltsames. Sie lesen nie das von Ihnen geschriebene 7
, sondern schreiben das Ergebnis von malloc
in ein double
:
Wenn Sie einen Zeiger in ein double konvertieren, verlieren Sie höchstwahrscheinlich eine Genauigkeit und machen den Speicher unzugänglich (UB).
Wenn Sie jedoch den Zeiger auf eine ganze Zahl speichern ( .u
), behandelt GCC dies als Alias-Speicher und behält die Initialisierung bei:
kompiliert zu
%Vor%Das Problem besteht also darin, dass Sie den Zeiger auf einen Doppelpunkt speichern.
BTW, (double)ptr
ist ein Kompilierfehler, wie es Standard erfordert:
6.5.4 Cast-Operatoren
[...]
4 Ein Zeigertyp darf nicht in einen Gleitkommatyp umgewandelt werden. Ein Floating-Typ darf nicht in einen beliebigen Zeigertyp konvertiert werden.
Ab N1548 Entwurf
C ist kein Assembler. C kann ein undefiniertes Verhalten auslösen, wenn jemand, der es als Assembler auf hoher Ebene ansieht, nicht sehen kann, warum. Als ein Beispiel: Bei zwei Arrays int a [10] und int b [10] ist es möglich, dass durch Zufall & amp; a [10] == & amp; b [0]. Jedoch der folgende Code
%Vor%ruft undefiniertes Verhalten auf, wenn p == & amp; b [0]. Zwei Zeiger, p und & amp; b [0], vergleichen sich gleich und bestehen aus den gleichen Bits, verhalten sich aber unterschiedlich. (Wenn Sie nicht zustimmen, schauen Sie sich "restricte" Zeiger an, wo der ganze Punkt ist, dass sich Zeiger, die sich mit Gleichem vergleichen, anders verhalten können).
Die Regeln für die Konvertierung zu uintptr_t lauten wie folgt: Jeder gültige Zeiger kann in uintptr_t konvertiert werden, und das Ergebnis kann zurück in einen Zeiger konvertiert werden, der den gleichen Zeiger enthält. Die Werte sind implementierungsdefiniert, außer dass das Konvertieren eines Nullzeigers in uintptr_t eine Null ergeben muss und das Konvertieren einer 0 in einen Zeiger einen Nullzeiger ergeben muss. Nichts erfordert, dass die Konvertierung einfach sein sollte, oder sollte sein, was Sie denken, dass es sein sollte.
Die Konvertierung in uintptr_t ist implementiert. Wenn Zeiger in einer Architektur auf n & lt; = 62 Bit beschränkt sind, dann ist es durchaus möglich, dass die Umwandlung wie folgt lautet: Wenn p ein Null-Zeiger ist, wandle es in Null um. Wenn p kein Nullzeiger ist, nimm die n Bits, verschiebe sie um (63 - n) Bits nach links, oder das Ergebnis mit 0x8000 0000 0000 0001. Das Ergebnis ist garantiert nicht ohne Verlust in das Doppelte konvertierbar. Wenn der Befehl uintptr_t in double konvertiert wird, ist das Ergebnis so, dass er nicht mehr in einen gültigen Zeiger umgewandelt werden kann.
Wenn also (double) (uintptr_t) p der einzige von p abgeleitete Wert ist, dann kann p nicht rekonstruiert werden, der Zeiger p geht verloren und eine Zuweisung zu * p kann wegoptimiert werden, da * p nicht sein kann Lies erneut.
Ich glaube, gcc hat Recht. Sie haben den Wert nicht verwendet und keinen Zeiger darauf zurückgegeben. Daher wird angenommen, dass der Wert nicht mehr erreichbar ist.
Sie hätten den Zeiger zurückgeben und an anderer Stelle doppelklicken müssen, oder Sie benötigen eine Vereinigung, damit gcc weiß, dass der Zeiger auf den Wert noch existiert:
%Vor%Tags und Links c gcc strict-aliasing