Was macht Visual Studio mit einem gelöschten Zeiger und warum?


130

Ein C ++ - Buch, das ich gelesen habe, besagt, dass beim Löschen eines Zeigers mit dem deleteOperator der Speicher an der Stelle, auf die er zeigt, "freigegeben" wird und überschrieben werden kann. Außerdem wird angegeben, dass der Zeiger weiterhin auf dieselbe Position zeigt, bis er neu zugewiesen oder auf gesetzt wird NULL.

In Visual Studio 2012 jedoch; das scheint nicht der Fall zu sein!

Beispiel:

#include <iostream>

using namespace std;

int main()
{
    int* ptr = new int;
    cout << "ptr = " << ptr << endl;
    delete ptr;
    cout << "ptr = " << ptr << endl;

    system("pause");

    return 0;
}

Wenn ich dieses Programm kompiliere und ausführe, erhalte ich die folgende Ausgabe:

ptr = 0050BC10
ptr = 00008123
Press any key to continue....

Die Adresse, auf die der Zeiger zeigt, ändert sich eindeutig, wenn delete aufgerufen wird!

Warum passiert dies? Hat dies speziell mit Visual Studio zu tun?

Und wenn Löschen die Adresse ändern kann, auf die es zeigt, warum sollte Löschen dann nicht automatisch den Zeiger auf NULLanstelle einer zufälligen Adresse setzen?


4
Löschen Sie einen Zeiger, das heißt nicht, dass er auf NULL gesetzt wird. Sie müssen sich darum kümmern.
Matt

11
Ich weiß das, aber das Buch, das ich gerade lese, besagt ausdrücklich, dass es immer noch dieselbe Adresse enthält, auf die es vor dem Löschen verwiesen hat, aber der Inhalt dieser Adresse kann überschrieben werden.
tjwrona1992

6
@ tjwrona1992, ja, weil dies normalerweise passiert. Das Buch listet nur das wahrscheinlichste Ergebnis auf, nicht die harte Regel.
SergeyA

5
@ tjwrona1992 Ein C ++ - Buch, das ich gelesen habe - und der Name des Buches ist ...?
PaulMcKenzie

4
@ tjwrona1992: Es mag überraschend sein, aber die Verwendung des ungültigen Zeigerwerts ist ein undefiniertes Verhalten, nicht nur eine Dereferenzierung. "Überprüfen, wohin es zeigt" verwendet den Wert nicht zulässig.
Ben Voigt

Antworten:


175

Mir ist aufgefallen, dass die in gespeicherte Adresse ptrimmer überschrieben wurde mit 00008123...

Das schien seltsam, also habe ich ein wenig gegraben und diesen Microsoft-Blog-Beitrag gefunden , der einen Abschnitt über "Automatisierte Zeigerbereinigung beim Löschen von C ++ - Objekten" enthält.

... Überprüfungen auf NULL sind ein gängiges Codekonstrukt, das bedeutet, dass eine vorhandene Überprüfung auf NULL in Kombination mit der Verwendung von NULL als Bereinigungswert zufällig ein echtes Speichersicherheitsproblem verbergen kann, dessen Hauptursache tatsächlich behoben werden muss.

Aus diesem Grund haben wir 0x8123 als Bereinigungswert ausgewählt - aus Sicht des Betriebssystems befindet sich dieser auf derselben Speicherseite wie die Nulladresse (NULL), aber eine Zugriffsverletzung bei 0x8123 wird dem Entwickler besser auffallen, da er detailliertere Aufmerksamkeit erfordert .

Es erklärt nicht nur, was Visual Studio mit dem Zeiger nach dem Löschen macht, sondern auch, warum sie ihn NICHT NULLautomatisch eingestellt haben!


Diese "Funktion" wird im Rahmen der Einstellung "SDL-Prüfungen" aktiviert. Zum Aktivieren / Deaktivieren gehen Sie zu: PROJEKT -> Eigenschaften -> Konfigurationseigenschaften -> C / C ++ -> Allgemein -> SDL-Prüfungen

Um dies zu bestätigen:

Wenn Sie diese Einstellung ändern und denselben Code erneut ausführen, wird die folgende Ausgabe ausgegeben:

ptr = 007CBC10
ptr = 007CBC10

"feature" steht in Anführungszeichen, da in einem Fall, in dem Sie zwei Zeiger auf denselben Speicherort haben, durch Aufrufen von delete nur EINER von ihnen bereinigt wird . Der andere zeigt auf den ungültigen Ort ...


AKTUALISIEREN:

Nach weiteren 5 Jahren Erfahrung in der C ++ - Programmierung ist mir klar, dass dieses gesamte Problem im Grunde genommen ein strittiger Punkt ist. Wenn Sie ein C ++ - Programmierer sind und weiterhin Rohzeiger verwenden newund deleteverwalten, anstatt intelligente Zeiger zu verwenden (die dieses gesamte Problem umgehen), sollten Sie eine Änderung des Karrierewegs in Betracht ziehen, um ein C-Programmierer zu werden. ;)


12
Das ist ein schöner Fund. Ich wünschte, MS würde ein solches Debugging-Verhalten besser dokumentieren. Zum Beispiel wäre es schön zu wissen, welche Compiler-Version dies implementiert hat und welche Optionen das Verhalten aktivieren / deaktivieren.
Michael Burr

5
"Aus Sicht des Betriebssystems befindet sich dies auf derselben Speicherseite wie die Nulladresse" - huh? Ist die Standard-Seitengröße (ohne große Seiten) unter x86 nicht immer noch 4 KB für Windows und Linux? Obwohl ich mich nur schwach an etwas über die ersten 64 KB Adressraum in Raymond Chens Blog erinnere, nehme ich in der Praxis das gleiche Ergebnis
Voo

12
@Voo Windows reserviert den ersten (und letzten) 64-KB-RAM als Totraum für das Überfüllen. 0x8123 fällt dort schön
Ratschenfreak

7
Tatsächlich fördert es keine schlechten Gewohnheiten und erlaubt Ihnen nicht, das Setzen des Zeigers auf NULL zu überspringen - das ist der ganze Grund, warum sie 0x8123stattdessen verwenden 0. Der Zeiger ist immer noch ungültig, verursacht jedoch eine Ausnahme, wenn versucht wird, ihn zu dereferenzieren (gut), und er besteht keine NULL-Prüfungen (auch gut, da es ein Fehler ist, dies nicht zu tun). Wo ist der Ort für schlechte Gewohnheiten? Es ist wirklich nur etwas, das Ihnen beim Debuggen hilft.
Luaan

3
Nun, es können nicht beide (alle) eingestellt werden, daher ist dies die zweitbeste Option. Wenn es Ihnen nicht gefällt, deaktivieren Sie einfach die SDL-Prüfungen - ich finde sie ziemlich nützlich, insbesondere beim Debuggen des Codes eines anderen.
Luaan

30

Sie sehen die Nebenwirkungen der /sdlKompilierungsoption. Standardmäßig für VS2015-Projekte aktiviert, werden zusätzliche Sicherheitsüberprüfungen aktiviert, die über die von / gs bereitgestellten hinausgehen. Verwenden Sie Projekt> Eigenschaften> C / C ++> Allgemein> SDL prüft die Einstellung, um sie zu ändern.

Zitat aus dem MSDN-Artikel :

  • Führt eine eingeschränkte Zeigerbereinigung durch. In Ausdrücken, die keine Dereferenzen beinhalten, und in Typen, die keinen benutzerdefinierten Destruktor haben, werden Zeigerreferenzen nach einem Aufruf zum Löschen auf eine ungültige Adresse gesetzt. Dies hilft, die Wiederverwendung veralteter Zeigerreferenzen zu verhindern.

Beachten Sie, dass das Setzen gelöschter Zeiger auf NULL eine schlechte Vorgehensweise ist, wenn Sie MSVC verwenden. Es verhindert die Hilfe, die Sie sowohl vom Debug-Heap als auch von dieser / sdl-Option erhalten. Sie können keine ungültigen Frei- / Löschaufrufe mehr in Ihrem Programm erkennen.


1
Bestätigt. Nach dem Deaktivieren dieser Funktion wird der Zeiger nicht mehr umgeleitet. Vielen Dank für die Bereitstellung der tatsächlichen Einstellung, die es ändert!
tjwrona1992

Hans, wird es immer noch als schlechte Praxis angesehen, gelöschte Zeiger auf NULL zu setzen, wenn zwei Zeiger auf dieselbe Position zeigen? Wenn Sie deleteeinen haben, belässt Visual Studio den zweiten Zeiger auf seinen ursprünglichen Speicherort, der jetzt ungültig ist.
tjwrona1992

1
Mir ist ziemlich unklar, welche Art von Magie Sie erwarten, wenn Sie den Zeiger auf NULL setzen. Dieser andere Zeiger ist nicht so, dass er nichts löst. Sie benötigen immer noch den Debug-Allokator, um den Fehler zu finden.
Hans Passant

3
VS bereinigt keine Zeiger. Es korrumpiert sie. Ihr Programm stürzt also ab, wenn Sie es trotzdem verwenden. Der Debug-Allokator macht das Gleiche mit dem Heap-Speicher. Das große Problem mit NULL ist, dass es nicht korrupt genug ist. Ansonsten eine gängige Strategie, googeln Sie "0xdeadbeef".
Hans Passant

1
Das Setzen des Zeigers auf NULL ist immer noch viel besser, als ihn auf seine vorherige Adresse zeigen zu lassen, die jetzt ungültig ist. Der Versuch, in einen NULL-Zeiger zu schreiben, beschädigt keine Daten und stürzt wahrscheinlich das Programm ab. Der Versuch, den Zeiger an diesem Punkt wiederzuverwenden, führt möglicherweise nicht einmal zum Absturz des Programms, sondern nur zu sehr unvorhersehbaren Ergebnissen!
tjwrona1992

19

Außerdem wird angegeben, dass der Zeiger weiterhin auf dieselbe Position zeigt, bis er neu zugewiesen oder auf NULL gesetzt wird.

Das sind definitiv irreführende Informationen.

Die Adresse, auf die der Zeiger zeigt, ändert sich eindeutig, wenn delete aufgerufen wird!

Warum passiert dies? Hat dies speziell mit Visual Studio zu tun?

Dies liegt eindeutig innerhalb der Sprachspezifikationen. ptrist nach dem Aufruf von nicht gültig delete. Die Verwendung ptrnach deleted ist Ursache für undefiniertes Verhalten. Tu es nicht. Die Laufzeitumgebung kann ptrnach dem Aufruf von frei tun, was sie will delete.

Und wenn Löschen die Adresse ändern kann, auf die es zeigt, warum sollte Löschen den Zeiger dann nicht automatisch auf NULL anstatt auf eine zufällige Adresse setzen?

Das Ändern des Werts des Zeigers auf einen alten Wert liegt innerhalb der Sprachspezifikation. Was die Änderung in NULL angeht, würde ich sagen, das wäre schlecht. Das Programm würde sich vernünftiger verhalten, wenn der Wert des Zeigers auf NULL gesetzt würde. Dadurch wird das Problem jedoch ausgeblendet. Wenn das Programm mit unterschiedlichen Optimierungseinstellungen kompiliert oder in eine andere Umgebung portiert wird, tritt das Problem wahrscheinlich im ungünstigsten Moment auf.


1
Ich glaube nicht, dass es die Frage von OP beantwortet.
SergeyA

Nicht einverstanden auch nach der Bearbeitung. Wenn Sie es auf NULL setzen, wird das Problem nicht ausgeblendet - tatsächlich würde es in mehr Fällen als ohne das aufgedeckt. Es gibt einen Grund, warum normale Implementierungen dies nicht tun, und der Grund ist anders.
SergeyA

4
@SergeyA, die meisten Implementierungen tun dies aus Gründen der Effizienz nicht. Wenn sich eine Implementierung jedoch dafür entscheidet, sie festzulegen, ist es besser, sie auf etwas festzulegen, das nicht NULL ist. Es würde die Probleme früher aufdecken, als wenn es auf NULL gesetzt wäre. Es ist auf NULL gesetzt, ein deletezweimaliger Aufruf des Zeigers würde kein Problem verursachen. Das ist definitiv nicht gut.
R Sahu

Nein, nicht die Effizienz - zumindest ist es nicht das Hauptanliegen.
SergeyA

7
@SergeyA Wenn Sie einen Zeiger auf einen Wert setzen, der nicht, NULLaber definitiv außerhalb des Adressraums des Prozesses liegt, werden mehr Fälle als die beiden Alternativen angezeigt . Wenn Sie es baumeln lassen, wird dies nicht unbedingt zu einem Segfault führen, wenn es nach der Freigabe verwendet wird. NULLWenn Sie es so einstellen, wird kein Segfault verursacht, wenn es deleteerneut angezeigt wird.
Blacklight Shining

10
delete ptr;
cout << "ptr = " << ptr << endl;

Im Allgemeinen ist sogar das Lesen (wie oben beschrieben, beachten Sie: Dies unterscheidet sich von der Dereferenzierung) von Werten ungültiger Zeiger (Zeiger werden beispielsweise ungültig, wenn Sie deletedies tun ) ein implementierungsdefiniertes Verhalten. Dies wurde in CWG # 1438 eingeführt . Siehe auch hier .

Bitte beachten Sie, dass das Lesen von Werten ungültiger Zeiger zuvor ein undefiniertes Verhalten war. Was Sie also oben haben, wäre ein undefiniertes Verhalten, was bedeutet, dass alles passieren kann.


3
Ebenfalls relevant ist das Zitat aus [basic.stc.dynamic.deallocation]: "Wenn das Argument für eine Freigabefunktion in der Standardbibliothek ein Zeiger ist, der nicht der Nullzeigerwert ist, muss die Freigabefunktion den vom Zeiger referenzierten Speicher freigeben, wodurch alle Zeiger ungültig werden, die auf einen verweisen." Teil des freigegebenen Speichers "und die Regel in [conv.lval](Abschnitt 4.1), die besagt, dass das Lesen (lWert-> rWert-Konvertierung) eines ungültigen Zeigerwerts ein implementierungsdefiniertes Verhalten ist.
Ben Voigt

Sogar UB kann von einem bestimmten Anbieter auf eine bestimmte Weise implementiert werden, so dass es zumindest für diesen Compiler zuverlässig ist. Wenn Microsoft beschlossen hätte, die Funktion zur Zeigerbereinigung vor CWG # 1438 zu implementieren, hätte dies diese Funktion nicht mehr oder weniger zuverlässig gemacht, und insbesondere ist es einfach nicht wahr, dass "alles passieren könnte", wenn diese Funktion aktiviert ist , unabhängig davon, was der Standard sagt.
Kyle Strand

@KyleStrand: Grundsätzlich habe ich UB definiert ( blog.regehr.org/archives/213 ).
Giorgim

1
Für die meisten der C ++ - Community auf SO ist "alles könnte passieren" viel zu wörtlich genommen . Ich finde das lächerlich . Ich verstehe die Definition von UB, aber ich verstehe auch , dass Compiler sind nur Teile der Software von echten Menschen umgesetzt, und wenn die Leute den Compiler implementieren , so dass es bestimmte Art und Weise verhält, das ist , wie der Compiler verhält , unabhängig davon , was der Standard sagt .
Kyle Strand

1

Ich glaube, Sie führen eine Art Debug-Modus aus und VS versucht, Ihren Zeiger auf einen bekannten Ort zu verschieben, damit ein weiterer Versuch, ihn zu dereferenzieren, verfolgt und gemeldet werden kann. Versuchen Sie, dasselbe Programm im Release-Modus zu kompilieren / auszuführen.

Zeiger werden im Inneren normalerweise nicht geändert, deleteum die Effizienz zu erhöhen und um eine falsche Vorstellung von Sicherheit zu vermeiden. Das Setzen des Löschzeigers auf einen vordefinierten Wert ist in den meisten komplexen Szenarien nicht hilfreich, da der zu löschende Zeiger wahrscheinlich nur einer von mehreren Zeigern ist, die auf diese Position zeigen.

Je mehr ich darüber nachdenke, desto mehr stelle ich fest, dass VS wie üblich daran schuld ist. Was ist, wenn der Zeiger const ist? Wird es das noch ändern?


Ja, sogar konstante Zeiger werden auf diesen mysteriösen 8123 umgeleitet!
tjwrona1992

VS hat noch einen Stein mehr :) Erst heute Morgen hat jemand gefragt, warum er g ++ anstelle von VS verwenden soll. Hier kommt's.
SergeyA

7
@SergeyA, aber von der anderen Seite zeigt der dereffekt, dass der gelöschte Zeiger Ihnen durch Segfault zeigt, dass Sie versucht haben, einen gelöschten Zeiger zu derefen, und er wird nicht gleich NULL sein. Im anderen Fall stürzt es nur ab, wenn die Seite ebenfalls freigegeben wird (was sehr unwahrscheinlich ist). Schneller scheitern; früher lösen.
Ratschenfreak

1
@ratchetfreak "Schnell scheitern, früher lösen" ist ein sehr wertvolles Mantra, aber "Schnell scheitern, indem wichtige forensische Beweise zerstört werden" startet kein so wertvolles Mantra. In einfachen Fällen mag es praktisch sein, aber in komplizierteren Fällen (in denen wir normalerweise die meiste Hilfe benötigen) verringert das Löschen wertvoller Informationen meine verfügbaren Tools zur Lösung des Problems.
Cort Ammon

2
@ tjwrona1992: Microsoft macht hier meiner Meinung nach das Richtige. Einen Zeiger zu bereinigen ist besser als gar keinen. Wenn dies zu Problemen beim Debuggen führt, setzen Sie vor dem Aufruf zum Löschen einen Haltepunkt. Die Chancen stehen gut, dass Sie ohne so etwas das Problem nie erkennen würden. Und wenn Sie eine bessere Lösung haben, um diese Fehler zu finden, verwenden Sie sie und warum interessiert es Sie, was Microsoft tut?
Zan Lynx

0

Nach dem Löschen des Zeigers ist der Speicher, auf den er zeigt, möglicherweise noch gültig. Um diesen Fehler zu manifestieren, wird der Zeigerwert auf einen offensichtlichen Wert gesetzt. Dies hilft wirklich beim Debuggen. Wenn der Wert auf gesetzt wurde, NULLwird er möglicherweise nie als potenzieller Fehler im Programmablauf angezeigt. So kann es einen Fehler verbergen, wenn Sie später gegen testen NULL.

Ein weiterer Punkt ist, dass einige Laufzeitoptimierer diesen Wert überprüfen und seine Ergebnisse ändern können.

In früheren Zeiten stellte MS den Wert auf ein 0xcfffffff.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.