Warum ist diese behauptete Dereferenzierungs-Typ-Punned-Zeigerwarnung compilerspezifisch?

38

Ich habe verschiedene Beiträge zu Stack Overflow RE gelesen : Der dereferenzierende typgesteuerte Zeigerfehler. Nach meinem Verständnis ist der Fehler im Wesentlichen die Compiler-Warnung vor der Gefahr des Zugriffs auf ein Objekt über einen Zeiger eines anderen Typs (obwohl eine Ausnahme gemacht zu sein scheint char*), was eine verständliche und vernünftige Warnung ist.

Meine Frage bezieht sich speziell auf den folgenden Code: Warum qualifiziert sich das Umwandeln der Adresse eines Zeigers auf eine void**für diese Warnung (über Fehler zum Fehler befördert -Werror)?

Darüber hinaus wird dieser Code für mehrere Zielarchitekturen kompiliert, von denen nur eine die Warnung / den Fehler generiert. Könnte dies bedeuten, dass es sich zu Recht um einen compilerspezifischen Mangel des Compilers handelt?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

Wenn mein oben angegebenes Verständnis richtig ist und a void**immer noch nur ein Zeiger ist, sollte dies ein sicheres Casting sein.

Gibt es eine Problemumgehung , bei der keine l-Werte verwendet werden , um diese compilerspezifische Warnung / diesen Fehler zu beruhigen? Dh ich verstehe das und warum dies das Problem lösen wird, aber ich möchte diesen Ansatz vermeiden, weil ich die freeFunc() NULL eines beabsichtigten Out-Arg nutzen möchte :

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Problem Compiler (einer von einem):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

Nicht beschwerender Compiler (einer von vielen):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

Update: Ich habe außerdem festgestellt, dass die Warnung beim Kompilieren speziell generiert zu werden scheint -O2(immer noch nur mit dem angegebenen "Problem-Compiler").

StoneThrow
quelle
1
"a void**, da dies immer noch nur ein Zeiger ist, sollte dies ein sicheres Casting sein." Woah da skippy! Klingt so, als hätten Sie einige grundlegende Annahmen. Versuchen Sie, weniger in Bytes und Hebeln als in Abstraktionen zu denken, denn damit programmieren Sie tatsächlich
Lightness Races in Orbit
7
Tangential sind die Compiler, die Sie verwenden, 15 und 17 Jahre alt! Ich würde mich auf keinen verlassen.
Tavian Barnes
4
@TavianBarnes Wenn Sie sich aus irgendeinem Grund auf GCC 3 verlassen müssen, ist es meiner Meinung nach am besten, die End-of-Lifed-End-Version 3.4.6 zu verwenden. Nutzen Sie alle verfügbaren Korrekturen für diese Serie, bevor sie zur Ruhe gelegt wurde.
Kaz
Welche Art von C ++ - Codierungsstandard schreibt all diese Leerzeichen vor?
Peter Mortensen

Antworten:

33

Ein Wert vom Typ void**ist ein Zeiger auf ein Objekt vom Typ void*. Ein Objekt vom Typ Foo*ist kein Objekt vom Typ void*.

Es gibt eine implizite Konvertierung zwischen Werten vom Typ Foo*und void*. Diese Konvertierung kann die Darstellung des Werts ändern. In ähnlicher Weise können Sie schreiben, int n = 3; double x = n;und dies hat das genau definierte Verhalten x, den Wert festzulegen 3.0, double *p = (double*)&n;hat jedoch ein undefiniertes Verhalten (und wird in der Praxis in keiner gängigen Architektur pauf einen „Zeiger auf 3.0“ gesetzt).

Architekturen, bei denen verschiedene Arten von Zeigern auf Objekte unterschiedliche Darstellungen haben, sind heutzutage selten, aber nach dem C-Standard zulässig. Es gibt (seltene) alte Maschinen mit Wortzeigern, die Adressen eines Wortes im Speicher sind, und Bytezeigern, die Adressen eines Wortes zusammen mit einem Byte-Offset in diesem Wort sind. Foo*wäre ein Wortzeiger und void*wäre ein Bytezeiger auf solchen Architekturen. Es gibt (seltene) Maschinen mit fetten Zeigern, die nicht nur Informationen über die Adresse des Objekts enthalten, sondern auch über seinen Typ, seine Größe und seine Zugriffssteuerungslisten. Ein Zeiger auf einen bestimmten Typ hat möglicherweise eine andere Darstellung als ein Zeiger, der zur void*Laufzeit zusätzliche Typinformationen benötigt.

Solche Maschinen sind selten, aber nach dem C-Standard zulässig. Einige C-Compiler nutzen die Berechtigung, typgesteuerte Zeiger als unterschiedlich zu behandeln, um den Code zu optimieren. Das Risiko von Zeiger-Aliasing ist eine wesentliche Einschränkung für die Fähigkeit eines Compilers, Code zu optimieren. Daher nutzen Compiler solche Berechtigungen in der Regel.

Ein Compiler kann Ihnen sagen, dass Sie etwas falsch machen oder leise tun, was Sie nicht wollten, oder leise tun, was Sie wollten. Undefiniertes Verhalten erlaubt all dies.

Sie können freefuncein Makro erstellen:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

Dies ist mit den üblichen Einschränkungen von Makros verbunden: Mangelnde Typensicherheit pwird zweimal bewertet. Beachten Sie, dass dies Ihnen nur die Sicherheit gibt, keine baumelnden Zeiger herumzulassen, wenn dies pder einzige Zeiger auf das freigegebene Objekt war.

Gilles 'SO - hör auf böse zu sein'
quelle
1
Und gut ist es zu wissen , dass selbst wenn Foo*und void*haben die gleiche Darstellung von der Architektur, ist es noch nicht definiert , um Typ-pun sie.
Tavian Barnes
12

A void *wird teilweise vom C-Standard speziell behandelt, weil es auf einen unvollständigen Typ verweist. Diese Behandlung ist nicht zu erweitern , um , void **wie es tut Punkt zu einer vollständigen Art, speziell void *.

Die strengen Aliasing-Regeln besagen, dass Sie einen Zeiger eines Typs nicht in einen Zeiger eines anderen Typs konvertieren und diesen Zeiger anschließend dereferenzieren können, da dies bedeutet, dass die Bytes eines Typs als andere neu interpretiert werden. Die einzige Ausnahme ist die Konvertierung in einen Zeichentyp, mit dem Sie die Darstellung eines Objekts lesen können.

Sie können diese Einschränkung umgehen, indem Sie anstelle einer Funktion ein funktionsähnliches Makro verwenden:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Was Sie so nennen können:

freeFunc(f);

Dies hat jedoch eine Einschränkung, da das obige Makro objzweimal ausgewertet wird. Wenn Sie GCC verwenden, kann dies mit einigen Erweiterungen vermieden werden, insbesondere mit den typeofSchlüsselwort- und Anweisungsausdrücken:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
dbush
quelle
3
+1 für eine bessere Umsetzung des beabsichtigten Verhaltens. Das einzige Problem, das ich mit dem sehe, #defineist, dass es objzweimal ausgewertet wird. Ich kenne jedoch keinen guten Weg, um diese zweite Bewertung zu vermeiden. Selbst ein Anweisungsausdruck (GNU-Erweiterung) reicht nicht aus, da Sie ihn zuweisen müssen, objnachdem Sie seinen Wert verwendet haben.
cmaster - wieder Monica
2
@cmaster: Wenn Sie bereit sind, GNU-Erweiterungen wie Anweisungsausdrücke zu verwenden, können Sie typeofvermeiden, objzweimal auszuwerten : #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
Ruakh
@ruakh Sehr cool :-) Wäre toll, wenn dbush das in der Antwort bearbeiten würde, damit es nicht mit den Kommentaren massenweise gelöscht wird.
cmaster
9

Das Dereferenzieren eines Typ-Punned-Zeigers ist UB und Sie können nicht damit rechnen, was passieren wird.

Unterschiedliche Compiler generieren unterschiedliche Warnungen, und zu diesem Zweck können unterschiedliche Versionen desselben Compilers als unterschiedliche Compiler betrachtet werden. Dies scheint eine bessere Erklärung für die Varianz zu sein, die Sie sehen, als eine Abhängigkeit von der Architektur.

Ein Fall, der Ihnen helfen kann zu verstehen, warum Typ Punning in diesem Fall schlecht sein kann, ist, dass Ihre Funktion auf einer Architektur, für die, nicht funktioniert sizeof(Foo*) != sizeof(void*). Das ist durch den Standard autorisiert, obwohl ich keinen aktuellen kenne, für den dies zutrifft.

Eine Problemumgehung wäre die Verwendung eines Makros anstelle einer Funktion.

Beachten Sie, dass freeNullzeiger akzeptiert werden.

Ein Programmierer
quelle
2
Faszinierend, dass es möglich ist, dass sizeof Foo* != sizeof void*. Ich habe noch nie festgestellt, dass Zeigergrößen "in the wild" typabhängig sind. Im Laufe der Jahre habe ich es als selbstverständlich angesehen, dass Zeigergrößen in einer bestimmten Architektur alle gleich sind.
StoneThrow
1
@Stonethrow Das Standardbeispiel sind fette Zeiger, mit denen Bytes in einer wortadressierbaren Architektur adressiert werden. Aber ich denke, aktuelle wortadressierbare Maschinen verwenden die alternative Größe von char == Größe des Wortes .
AProgrammer
2
Beachten Sie, dass der Typ für die Größe von ...
Antti Haapala
@StoneThrow: Unabhängig von der Zeigergröße ist die typbasierte Alias-Analyse unsicher. Dies hilft Compilern bei der Optimierung, indem angenommen wird, dass ein Speicher über float*ein int32_tObjekt nicht geändert wird. Daher int32_t*muss int32_t *restrict ptrder Compiler beispielsweise nicht annehmen, dass er nicht auf denselben Speicher verweist. Gleiches gilt für Geschäfte void**, bei denen davon ausgegangen wird, dass ein Foo*Objekt nicht geändert wird.
Peter Cordes
4

Dieser Code ist gemäß C-Standard ungültig, sodass er in einigen Fällen möglicherweise funktioniert, jedoch nicht unbedingt portierbar ist.

Die "strenge Aliasing-Regel" für den Zugriff auf einen Wert über einen Zeiger, der in einen anderen Zeigertyp umgewandelt wurde, finden Sie in 6.5 Absatz 7:

Auf einen gespeicherten Wert eines Objekts darf nur über einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen hat:

  • ein Typ, der mit dem effektiven Typ des Objekts kompatibel ist,

  • eine qualifizierte Version eines Typs, der mit dem effektiven Typ des Objekts kompatibel ist;

  • ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem effektiven Typ des Objekts entspricht;

  • ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht;

  • ein Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen unter seinen Mitgliedern enthält (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Vereinigung), oder

  • ein Zeichentyp.

In Ihrer *obj = NULL;Anweisung hat das Objekt einen effektiven Typ, auf den Foo*jedoch der lvalue-Ausdruck *objmit type zugreift void*.

In 6.7.5.1 Absatz 2 haben wir

Damit zwei Zeigertypen kompatibel sind, müssen beide identisch qualifiziert sein und beide müssen Zeiger auf kompatible Typen sein.

So void*und Foo*nicht kompatible Typen oder kompatible Typen mit Qualifier hinzugefügt, und schon gar nicht eine der anderen Optionen der strengen Aliasing - Regel passen.

Obwohl dies nicht der technische Grund ist, warum der Code ungültig ist, ist es auch relevant, Abschnitt 6.2.5 Absatz 26 zu beachten:

Ein Zeiger auf voidmuss dieselben Darstellungs- und Ausrichtungsanforderungen haben wie ein Zeiger auf einen Zeichentyp. Ebenso müssen Zeiger auf qualifizierte oder nicht qualifizierte Versionen kompatibler Typen dieselben Darstellungs- und Ausrichtungsanforderungen haben. Alle Zeiger auf Strukturtypen müssen dieselben Darstellungs- und Ausrichtungsanforderungen haben. Alle Zeiger auf Vereinigungstypen müssen die gleichen Darstellungs- und Ausrichtungsanforderungen haben. Zeiger auf andere Typen müssen nicht dieselben Darstellungs- oder Ausrichtungsanforderungen haben.

Was die Unterschiede bei den Warnungen betrifft, so ist dies kein Fall, in dem der Standard eine Diagnosemeldung erfordert. Es geht also nur darum, wie gut der Compiler oder seine Version potenzielle Probleme erkennt und auf hilfreiche Weise darauf hinweist. Sie haben festgestellt, dass Optimierungseinstellungen einen Unterschied machen können. Dies liegt häufig daran, dass intern mehr Informationen darüber generiert werden, wie verschiedene Teile des Programms in der Praxis tatsächlich zusammenpassen, und dass zusätzliche Informationen daher auch für Warnprüfungen verfügbar sind.

aschepler
quelle
2

Zusätzlich zu den anderen Antworten ist dies ein klassisches Anti-Muster in C, das mit Feuer verbrannt werden sollte. Es erscheint in:

  1. Free-and-Null-Out-Funktionen wie die, in der Sie die Warnung gefunden haben.
  2. Zuweisungsfunktionen, die das Standard-C-Idiom der Rückgabe meiden void *(das nicht unter diesem Problem leidet, da es eine Wertkonvertierung anstelle von Typ Punning beinhaltet ), stattdessen ein Fehlerflag zurückgeben und das Ergebnis über einen Zeiger auf Zeiger speichern.

Für ein anderes Beispiel von (1) gab es einen langjährigen berüchtigten Fall in der av_freeFunktion von ffmpeg / libavcodec . Ich glaube, es wurde irgendwann mit einem Makro oder einem anderen Trick behoben, aber ich bin mir nicht sicher.

Für (2) sind beide cudaMallocund posix_memalignBeispiele.

In keinem Fall erfordert die Schnittstelle von Natur aus eine ungültige Verwendung, sie empfiehlt dies jedoch nachdrücklich und lässt die korrekte Verwendung nur mit einem zusätzlichen temporären Objekt vom Typ zu void *, das den Zweck der Free-and-Null-Out-Funktionalität zunichte macht und die Zuweisung umständlich macht.

R .. GitHub HÖREN SIE AUF, EIS ZU HELFEN
quelle
Haben Sie einen Link, der mehr darüber erklärt, warum (1) ein Anti-Muster ist? Ich glaube nicht, dass ich mit dieser Situation / diesem Argument vertraut bin und mehr erfahren möchte.
StoneThrow
1
@StoneThrow: Es ist wirklich einfach - die Absicht ist, Missbrauch zu verhindern, indem das Objekt, das den Zeiger auf den freigegebenen Speicher speichert, auf Null gesetzt wird. Dies kann jedoch nur dann erreicht werden, wenn der Aufrufer den Zeiger tatsächlich in einem Objekt von speichert tippe void *und gieße / konvertiere es jedes Mal, wenn es dereferenziert werden soll. Dies ist sehr unwahrscheinlich. Wenn der Aufrufer einen anderen Zeigertyp speichert, besteht die einzige Möglichkeit, die Funktion aufzurufen, ohne UB aufzurufen, darin, den Zeiger auf ein temporäres Objekt vom Typ zu kopieren void *und die Adresse davon an die Freigabefunktion zu übergeben, und dann einfach ...
R .. GitHub STOP HELPING ICE
1
... setzt ein temporäres Objekt auf Null und nicht den tatsächlichen Speicher, in dem der Aufrufer den Zeiger hatte. Was tatsächlich passiert, ist natürlich, dass Benutzer der Funktion am Ende eine (void **)Besetzung durchführen und undefiniertes Verhalten erzeugen.
R .. GitHub STOP HELPING ICE
2

Obwohl C für Maschinen entwickelt wurde, die für alle Zeiger dieselbe Darstellung verwenden, wollten die Autoren des Standards die Sprache auf Maschinen verwenden, die unterschiedliche Darstellungen für Zeiger auf verschiedene Objekttypen verwenden. Daher war es nicht erforderlich, dass Maschinen, die unterschiedliche Zeigerdarstellungen für verschiedene Arten von Zeigern verwenden, einen Typ "Zeiger auf jede Art von Zeiger" unterstützen, obwohl viele Maschinen dies ohne Kosten tun könnten.

Bevor der Standard geschrieben wurde, erlaubten Implementierungen für Plattformen, die für alle Zeigertypen dieselbe Darstellung void**verwendeten, einstimmig die Verwendung von a, zumindest mit geeignetem Casting, als "Zeiger auf einen beliebigen Zeiger". Die Autoren des Standards erkannten mit ziemlicher Sicherheit, dass dies auf Plattformen nützlich sein würde, die ihn unterstützten, aber da er nicht allgemein unterstützt werden konnte, lehnten sie es ab, ihn zu beauftragen. Stattdessen erwarteten sie, dass die Qualitätsimplementierung solche Konstrukte verarbeiten würde, wie sie in der Begründung als "populäre Erweiterung" beschrieben würden, in Fällen, in denen dies sinnvoll wäre.

Superkatze
quelle