Wann ist es gültig, auf einen Zeiger auf ein “totes” Objekt zuzugreifen?
Lesezeit: 5 Minuten
Oliver Charlesworth
Erstens, um das klarzustellen, ich bin es nicht wir sprechen über das Dereferenzieren ungültiger Zeiger!
Betrachten Sie die folgenden zwei Beispiele.
Beispiel 1
typedef struct { int *p; } T;
T a = { malloc(sizeof(int) };
free(a.p); // a.p is now indeterminate?
T b = a; // Access through a non-character type?
Beispiel 2
void foo(int *p) {}
int *p = malloc(sizeof(int));
free(p); // p is now indeterminate?
foo(p); // Access through a non-character type?
Frage
Ruft eines der obigen Beispiele undefiniertes Verhalten hervor?
Kontext
Diese Frage wird als Antwort auf diese Diskussion gestellt. Der Vorschlag war, dass beispielsweise Zeigerargumente über x86-Segmentregister an eine Funktion übergeben werden können, was eine Hardware-Ausnahme verursachen könnte.
Aus dem C99-Standard lernen wir Folgendes (Hervorhebung von mir):
[3.17]unbestimmter Wert – entweder ein unbestimmter Wert oder a Fallendarstellung
und dann:
[6.2.4 p2] Der Wert eines Zeigers wird unbestimmt wenn das Objekt, auf das es zeigt, das Ende seiner Lebensdauer erreicht.
und dann:
[6.2.6.1 p5] Bestimmte Objektdarstellungen müssen keinen Wert des Objekttyps darstellen. Wenn der gespeicherte Wert eines Objekts eine solche Darstellung hat und von einem lvalue-Ausdruck gelesen wird, der keinen Zeichentyp hat, Das Verhalten ist undefiniert. Wenn eine solche Darstellung durch einen Nebeneffekt erzeugt wird, der das gesamte oder einen Teil des Objekts durch einen Lvalue-Ausdruck ändert, der keinen Zeichentyp hat, ist das Verhalten nicht definiert. Eine solche Darstellung heißt a Fallendarstellung.
Wenn wir all dies zusammenfassen, welche Einschränkungen haben wir beim Zugriff auf Zeiger auf “tote” Objekte?
Nachtrag
Obwohl ich oben den C99-Standard zitiert habe, würde mich interessieren, ob sich das Verhalten in einem der C++-Standards unterscheidet.
Sie haben den Standard auf hervorragende Weise zitiert – aus diesen Worten ist mir klar, dass die Verwendung eines ungültigen Zeigers in irgendeiner Weise, auch ohne ihn zu dereferenzieren, ein undefiniertes Verhalten hervorruft.
– Benutzer529758
10. Juni 2013 um 13:20 Uhr
@Devolus: Ja, das war auch meine Intuition. Aber der Standard scheint relativ eindeutig. Und AProgrammer hat (in der verlinkten Diskussion) einen guten Punkt gemacht, dass, wenn Segmentregister beteiligt sind, dies wirklich zu einer HW-Ausnahme führen könnte.
– Oliver Charlesworth
10. Juni 2013 um 13:31 Uhr
@willj: Das ist richtig. Aber nichtsdestotrotz sagt uns der Standard, dass der Zeiger jetzt unbestimmt ist.
– Oliver Charlesworth
10. Juni 2013 um 13:42 Uhr
“Selbst rollen” malloc und free ruft bereits undefiniertes Verhalten auf. 7.1.3: “Wenn das Programm einen Bezeichner in einem Kontext deklariert oder definiert, in dem er reserviert ist (anders als in 7.1.4 erlaubt), oder einen reservierten Bezeichner als Makronamen definiert, ist das Verhalten undefiniert.”
– R.. GitHub HÖR AUF, EIS ZU HELFEN
10. Juni 2013 um 13:46 Uhr
@willj, es geht nicht darum, diesen Wert zu ändern. Höchstwahrscheinlich hat der Zeiger immer noch den gleichen Wert. Wenn dieser Wert jedoch irgendwo kopiert wird, kann er durch ein spezielles Zeigerregister (z. B. Segmentregister in x86) laufen, wo die Hardware einen Trap verursachen könnte, weil der Zeiger ungültig ist.
– Shahbaz
10. Juni 2013 um 14:03 Uhr
Beispiel 2 ist ungültig. Die Analyse in Ihrer Frage ist richtig.
Beispiel 1 ist gültig. Ein Strukturtyp enthält niemals eine Trap-Darstellung, selbst wenn eines seiner Mitglieder dies tut. Dies bedeutet, dass die Strukturzuweisung auf einem System, auf dem Trap-Darstellungen Probleme verursachen würden, als byteweises Kopieren implementiert werden muss, anstatt als Mitglied-für-Mitglied-Kopie.
6.2.6 Repräsentationen von Typen
6.2.6.1 Allgemeines
6 […] Der Wert einer Struktur oder eines Vereinigungsobjekts ist niemals eine Rap-Darstellung, selbst wenn der Wert eines Mitglieds der Struktur oder eines Vereinigungsobjekts eine Trap-Darstellung sein kann.
Ach, das ist interessant. Diese Klausel war mir nicht aufgefallen. Vielen Dank!
– Oliver Charlesworth
10. Juni 2013 um 13:47 Uhr
Da es sich nicht um Trap-Darstellungen, sondern um unbestimmte Werte handelt, glaube ich nicht, dass das Problem durch den zitierten Text gelöst wird. Gemäß J.2 (wenn auch nicht normativ) ergibt sich UB, wenn “der Wert eines Objekts mit automatischer Speicherdauer verwendet wird, während er unbestimmt ist (6.2.4, 6.7.8, 6.8).” Möglicherweise ist in diesem Fall jedoch der Wert des Mitglieds unbestimmt, nicht der Wert der Struktur, in diesem Fall wird der Wert des Objekts mit unbestimmtem Wert nicht verwendet.
– R.. GitHub HÖR AUF, EIS ZU HELFEN
11. Juni 2013 um 1:17 Uhr
@RJ2 ist veraltet. Der normative Text (jedenfalls von C99) verbietet nur das Lesen von Objekten, die Fallendarstellungen enthalten. Wenn sie unbestimmt sind, aber keine Fallendarstellungen enthalten können, ist das Lesen erlaubt. Dies ist wichtig für z. unsigned char zu.
– Benutzer743382
11. Juni 2013 um 6:49 Uhr
@R.. Es gibt DR 338 das soll die Regeln wieder etwas verschärfen, aber ich sehe es nicht in einem Entwurf von C11 (vielleicht wurde es nach dem letzten öffentlichen Entwurf aufgenommen), daher bin ich mir nicht sicher, wie sich das auf meine Antwort hier auswirkt.
– Benutzer743382
11. Juni 2013 um 6:56 Uhr
@Superkatze Analysierbarkeit könnte dafür von Interesse sein (aber nicht für Ihre früheren Kommentare). Ab C11 kann eine Implementierung definieren __STDC_ANALYZABLE__ um anzuzeigen, dass die Auswirkungen von undefiniertem Verhalten begrenzt sind, mit Ausnahme von kritischem undefiniertem Verhalten. Und das Lesen von Trap-Darstellungen ist kein kritisches undefiniertes Verhalten: if __STDC_ANALYZABLE__ definiert ist, kann dies zum Abbruch des Programms führen, die Ausführung des Programms kann jedoch nicht vollständig beschädigt werden.
– Benutzer743382
28. April 2015 um 20:51 Uhr
Meine Interpretation ist, dass zwar nur Nicht-Zeichentypen Trap-Darstellungen haben können, aber jeder Typ einen unbestimmten Wert haben kann und dass der Zugriff auf ein Objekt mit unbestimmtem Wert in irgendeiner Weise ein undefiniertes Verhalten hervorruft. Das berüchtigtste Beispiel könnte OpenSSLs ungültige Verwendung von nicht initialisierten Objekten als Zufallsstartwert sein.
Die Antwort auf Ihre Frage wäre also: Niemals.
Übrigens eine interessante Folge nicht nur des Objekts, auf das gezeigt wird, sondern des Zeiger selbst danach unbestimmt sein free oder realloc ist, dass dieses Idiom undefiniertes Verhalten aufruft:
Re “Auf ein Objekt zugreifen …”; Es gibt eine Fußnote in der Norm, die ich oben nicht zitiert habe: “Somit kann eine automatische Variable mit einer Trap-Darstellung initialisiert werden, ohne undefiniertes Verhalten zu verursachen, aber der Wert der Variablen kann nicht verwendet werden, bis ein richtiger Wert darin gespeichert ist.” Hört sich an wie Schreiben zu einem solchen Objekt ist akzeptabel.
– Oliver Charlesworth
10. Juni 2013 um 13:36 Uhr
@OliCharlesworth, natürlich ist es das. Wie kann man sonst so etwas machen: free(x); x = NULL;?
– Shahbaz
10. Juni 2013 um 13:37 Uhr
@OliCharlesworth, ich denke der Teil, der sagt: Wenn der gespeicherte Wert eines Objekts eine solche Darstellung hat und ist lesen durch einen Lvalue-Ausdruck …zeigt, dass darauf geschrieben, aber nicht gelesen werden kann.
– Shahbaz
10. Juni 2013 um 14:06 Uhr
void *tmp = realloc(ptr, newsize); << wenn realloc fehlschlägt, dann ist tmp gültig (NULL) und ptr bleibt ebenfalls gültig. Dies ist nicht UB, wenn tmp==NULL.
– Jim Mcnamara
10. Juni 2013 um 15:13 Uhr
@jimmcnamara: Natürlich. Aber es ist UB im Erfolgsfall, das war der Punkt.
– R.. GitHub HÖR AUF, EIS ZU HELFEN
10. Juni 2013 um 20:18 Uhr
C++-Diskussion
Kurze Antwort: In C++ gibt es so etwas wie den Zugriff auf eine Klasseninstanz nicht; Sie können nur Nicht-Klassen-Objekte “lesen”, und dies geschieht durch eine lvalue-zu-rvalue-Konvertierung.
Ausführliche Antwort:
typedef struct { int *p; } T;
T bezeichnet eine unbenannte Klasse. Der Diskussion halber nennen wir diese Klasse T:
struct T {
int *p;
};
Da Sie keinen Kopierkonstruktor deklariert haben, deklariert der Compiler implizit einen, sodass die Klassendefinition lautet:
struct T {
int *p;
T (const T&);
};
Also haben wir:
T a;
T b = a; // Access through a non-character type?
Ja, in der Tat; Dies ist die Initialisierung durch den Kopierkonstruktor, sodass die Kopierkonstruktordefinition vom Compiler generiert wird. die Definition ist äquivalent mit
inline T::T (const T& rhs)
: p(rhs.p) {
}
So Sie greifen auf den Wert als Zeiger zunicht ein paar Bytes.
Wenn der Zeigerwert ungültig ist (nicht initialisiert, freigegeben), ist das Verhalten nicht definiert.
Tatsächlich kann eine Lvalue-zu-rvalue-Konvertierung auch für Klassen-lvalues durchgeführt werden. Der Kontext liegt vor, wenn ein Klassen-lvalue durch die Auslassungspunkte in einem Funktionsaufruf übergeben wird.
– Johannes Schaub – litb
16. Juni 2013 um 9:33 Uhr
@JohannesSchaub-litb Ja, das kannst du. [conv.lval]”Andernfalls, wenn der glvalue einen Klassentyp hat, initialisiert die Konvertierung eine temporäre Kopie des Typs T aus dem glvalue und das Ergebnis der Konvertierung ist ein prvalue für die temporäre” Also wird diese Konvertierung in Bezug auf den ctor und wir definiert Gehen Sie zurück, um auf jedes Mitglied einzeln zuzugreifen, mit lvalue-zu-rvalue-Konvertierung für jedes einzelne.
– Neugieriger
16. Juni 2013 um 10:35 Uhr
das ist richtig. Zumindest was Nonunion-Klassenobjekte betrifft. Unions werden “bitweise” kopiert.
– Johannes Schaub – litb
16. Juni 2013 um 11:20 Uhr
Das alles hat nichts mit der Frage zu tun, bis auf den letzten Satz … den Sie nicht begründen.
– MM
7. Juni 2015 um 14:30 Uhr
Die Beispiele in der Frage beziehen sich auf die Verwendung eines Zeigers, nachdem das Leerzeichen, auf das er zeigt, freigegeben wurde. In Ihrem Code kopieren Sie einen nicht initialisierten Zeiger, was anders ist. Außerdem ist das ganze Zeug über die Klasse irrelevant, das hättest du genauso gut schreiben können int *a; int *b = a;
– MM
7. Juni 2015 um 15:05 Uhr
14107200cookie-checkWann ist es gültig, auf einen Zeiger auf ein “totes” Objekt zuzugreifen?yes
Sie haben den Standard auf hervorragende Weise zitiert – aus diesen Worten ist mir klar, dass die Verwendung eines ungültigen Zeigers in irgendeiner Weise, auch ohne ihn zu dereferenzieren, ein undefiniertes Verhalten hervorruft.
– Benutzer529758
10. Juni 2013 um 13:20 Uhr
@Devolus: Ja, das war auch meine Intuition. Aber der Standard scheint relativ eindeutig. Und AProgrammer hat (in der verlinkten Diskussion) einen guten Punkt gemacht, dass, wenn Segmentregister beteiligt sind, dies wirklich zu einer HW-Ausnahme führen könnte.
– Oliver Charlesworth
10. Juni 2013 um 13:31 Uhr
@willj: Das ist richtig. Aber nichtsdestotrotz sagt uns der Standard, dass der Zeiger jetzt unbestimmt ist.
– Oliver Charlesworth
10. Juni 2013 um 13:42 Uhr
“Selbst rollen”
malloc
undfree
ruft bereits undefiniertes Verhalten auf. 7.1.3: “Wenn das Programm einen Bezeichner in einem Kontext deklariert oder definiert, in dem er reserviert ist (anders als in 7.1.4 erlaubt), oder einen reservierten Bezeichner als Makronamen definiert, ist das Verhalten undefiniert.”– R.. GitHub HÖR AUF, EIS ZU HELFEN
10. Juni 2013 um 13:46 Uhr
@willj, es geht nicht darum, diesen Wert zu ändern. Höchstwahrscheinlich hat der Zeiger immer noch den gleichen Wert. Wenn dieser Wert jedoch irgendwo kopiert wird, kann er durch ein spezielles Zeigerregister (z. B. Segmentregister in x86) laufen, wo die Hardware einen Trap verursachen könnte, weil der Zeiger ungültig ist.
– Shahbaz
10. Juni 2013 um 14:03 Uhr