Co robi program Visual Studio z usuniętym wskaźnikiem i dlaczego?

130

Książka C ++, którą czytałem, stwierdza, że ​​kiedy wskaźnik jest usuwany za pomocą deleteoperatora, pamięć w miejscu, na które wskazuje, jest „zwalniana” i można ją nadpisać. Stwierdza również, że wskaźnik będzie nadal wskazywał tę samą lokalizację, dopóki nie zostanie ponownie przypisany lub ustawiony na NULL.

Jednak w programie Visual Studio 2012; wydaje się, że tak nie jest!

Przykład:

#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;
}

Kiedy kompiluję i uruchamiam ten program, otrzymuję następujące dane wyjściowe:

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

Oczywiście adres wskazywany przez wskaźnik zmienia się po wywołaniu funkcji usuwania!

Dlaczego to się dzieje? Czy ma to coś wspólnego z programem Visual Studio?

A jeśli delete może zmienić adres, na który i tak wskazuje, dlaczego nie miałby automatycznie ustawiać wskaźnika na NULLzamiast jakiegoś losowego adresu?

tjwrona1992
źródło
4
Usuń wskaźnik, nie oznacza to, że będzie ustawiony na NULL, musisz się tym zająć.
Matt
11
Wiem o tym, ale książka, którą czytam, wyraźnie mówi, że nadal będzie zawierać ten sam adres, na który wskazywał przed usunięciem, ale zawartość tego adresu może zostać nadpisana.
tjwrona1992
6
@ tjwrona1992, tak, bo tak się zwykle dzieje. Książka zawiera tylko najbardziej prawdopodobny wynik, a nie twardą regułę.
SergeyA
5
@ tjwrona1992 Książka w języku C ++, którą czytałem - a tytuł książki to ...?
PaulMcKenzie
4
@ tjwrona1992: To może być zaskakujące, ale wszystko to polega na użyciu nieprawidłowej wartości wskaźnika, co jest niezdefiniowanym zachowaniem, a nie tylko wyłuskiwaniem. „Sprawdzanie, gdzie wskazuje”, używa wartości w niedozwolony sposób.
Ben Voigt

Odpowiedzi:

175

Zauważyłem, że zapisany adres ptrbył zawsze nadpisywany przez 00008123...

Wydawało się to dziwne, więc poszperałem trochę i znalazłem ten wpis na blogu firmy Microsoft zawierający sekcję omawiającą „Automatyczne oczyszczanie wskaźnika podczas usuwania obiektów C ++”.

... sprawdzanie wartości NULL jest typową konstrukcją kodu, co oznacza, że ​​istniejące sprawdzenie wartości NULL w połączeniu z użyciem NULL jako wartości oczyszczającej mogłoby przypadkowo ukryć prawdziwy problem z bezpieczeństwem pamięci, którego główna przyczyna naprawdę wymaga rozwiązania.

Z tego powodu wybraliśmy 0x8123 jako wartość sanityzacji - z punktu widzenia systemu operacyjnego jest to ta sama strona pamięci co adres zerowy (NULL), ale naruszenie dostępu pod adresem 0x8123 będzie lepiej widoczne dla programisty, ponieważ wymaga bardziej szczegółowej uwagi .

Nie tylko wyjaśnia, co program Visual Studio robi ze wskaźnikiem po jego usunięciu, ale także wyjaśnia, dlaczego NIE ustawili go na NULLautomatycznie!


Ta „funkcja” jest włączona w ramach ustawienia „Kontrole SDL”. Aby go włączyć / wyłączyć, przejdź do: PROJECT -> Właściwości -> Właściwości konfiguracji -> C / C ++ -> Ogólne -> Testy SDL

Aby to potwierdzić:

Zmiana tego ustawienia i ponowne uruchomienie tego samego kodu daje następujący wynik:

ptr = 007CBC10
ptr = 007CBC10

„funkcja” jest w cudzysłowie, ponieważ w przypadku, gdy masz dwa wskaźniki do tej samej lokalizacji, wywołanie funkcji usuwania wyczyści tylko JEDNO z nich. Drugi będzie wskazywał nieprawidłową lokalizację ...


AKTUALIZACJA:

Po kolejnych 5 latach programowania w C ++ zdałem sobie sprawę, że cała ta kwestia jest w zasadzie kwestią sporną. Jeśli jesteś programistą C ++ i nadal używasz surowych wskaźników newi deletezarządzasz nimi zamiast inteligentnych wskaźników (które omijają cały ten problem), możesz rozważyć zmianę ścieżki kariery, aby zostać programistą C. ;)

tjwrona1992
źródło
12
To miłe znalezisko. Chciałbym, żeby MS lepiej dokumentowało takie zachowanie debugowania. Na przykład dobrze byłoby wiedzieć, która wersja kompilatora zaczęła to implementować i jakie opcje włączają / wyłączają to zachowanie.
Michael Burr
5
„z perspektywy systemu operacyjnego jest to ta sama strona pamięci co adres zerowy” - co? Czy standardowy rozmiar strony (pomijając duże strony) na x86 nie jest nadal 4kb dla systemu Windows i Linux? Chociaż słabo pamiętam coś o pierwszych 64kb przestrzeni adresowej na blogu Raymonda Chena, więc w praktyce przyjmuję ten sam wynik
Voo
12
@Voo Windows rezerwuje pierwsze (i ostatnie) 64kB pamięci RAM jako martwą przestrzeń do przechwytywania. 0x8123 spada tam ładnie
grzechotką cudo
7
Właściwie nie zachęca to do złych nawyków i nie pozwala pominąć ustawiania wskaźnika na NULL - to cały powód, dla którego używają 0x8123zamiast 0. Wskaźnik jest nadal nieprawidłowy, ale powoduje wyjątek podczas próby wyłuskiwania go (dobrze) i nie przechodzi kontroli NULL (również dobrze, ponieważ nie można tego zrobić). Gdzie jest miejsce na złe nawyki? To naprawdę jest coś, co pomaga w debugowaniu.
Luaan
3
Cóż, nie może ustawić obu (wszystkich), więc jest to druga najlepsza opcja. Jeśli ci się to nie podoba, po prostu wyłącz testy SDL - uważam je za raczej przydatne, zwłaszcza podczas debugowania czyjegoś kodu.
Luaan
30

Zobaczysz efekty uboczne /sdlopcji kompilacji. Domyślnie włączona dla projektów VS2015, umożliwia dodatkowe kontrole bezpieczeństwa poza tymi zapewnianymi przez / gs. Użyj ustawienia Projekt> Właściwości> C / C ++> Ogólne> SDL sprawdza ustawienie, aby je zmienić.

Cytując z artykułu MSDN :

  • Wykonuje ograniczoną dezynfekcję wskaźnika. W wyrażeniach, które nie obejmują wyłuskiwania, oraz w typach, które nie mają elementu destruktora zdefiniowanego przez użytkownika, odwołania do wskaźników są ustawiane na nieprawidłowy adres po wywołaniu funkcji delete. Pomaga to zapobiegać ponownemu użyciu nieaktualnych odwołań do wskaźników.

Należy pamiętać, że ustawienie usuniętych wskaźników na NULL jest złą praktyką podczas korzystania z MSVC. To pokonuje pomoc, którą otrzymujesz zarówno ze sterty debugowania, jak i tej opcji / sdl, nie możesz już wykrywać nieprawidłowych wywołań free / delete w swoim programie.

Hans Passant
źródło
1
Potwierdzony. Po wyłączeniu tej funkcji wskaźnik nie jest już przekierowywany. Dziękujemy za podanie rzeczywistych ustawień, które je modyfikują!
tjwrona1992
Hans, czy nadal uważane jest za złą praktykę ustawianie usuniętych wskaźników na NULL w przypadku, gdy masz dwa wskaźniki wskazujące tę samą lokalizację? Gdy to zrobisz delete, program Visual Studio pozostawi drugi wskaźnik wskazujący jego oryginalną lokalizację, która jest teraz nieprawidłowa.
tjwrona1992
1
Dość niejasne dla mnie, jakiego rodzaju magii spodziewasz się, ustawiając wskaźnik na NULL. Ten drugi wskaźnik nie jest, więc niczego nie rozwiązuje, nadal potrzebujesz alokatora debugowania, aby znaleźć błąd.
Hans Passant
3
VS nie czyści wskaźników. To je psuje. Więc twój program i tak będzie się zawieszał, kiedy ich użyjesz. Alokator debugowania robi to samo z pamięcią sterty. Duży problem z NULL polega na tym, że nie jest wystarczająco uszkodzony. W przeciwnym razie powszechna strategia, Google „0xdeadbeef”.
Hans Passant
1
Ustawienie wskaźnika na NULL jest nadal znacznie lepsze niż pozostawienie go wskazującego na jego poprzedni adres, który jest teraz nieprawidłowy. Próba zapisu do wskaźnika NULL nie uszkodzi żadnych danych i prawdopodobnie spowoduje awarię programu. Próba ponownego użycia wskaźnika w tym momencie może nawet nie spowodować awarii programu, może po prostu dać bardzo nieprzewidywalne rezultaty!
tjwrona1992
19

Stwierdza również, że wskaźnik będzie nadal wskazywał tę samą lokalizację, dopóki nie zostanie ponownie przypisany lub ustawiony na NULL.

To jest zdecydowanie myląca informacja.

Oczywiście adres wskazywany przez wskaźnik zmienia się po wywołaniu funkcji usuwania!

Dlaczego to się dzieje? Czy ma to coś wspólnego z programem Visual Studio?

Jest to wyraźnie zgodne ze specyfikacjami językowymi. ptrnie jest ważny po wywołaniu delete. Używanie ptrpo deleted jest przyczyną niezdefiniowanego zachowania. Nie rób tego. Środowisko ptrwykonawcze może wykonywać dowolne czynności po wywołaniu delete.

A jeśli delete może zmienić adres, na który i tak wskazuje, dlaczego nie miałby automatycznie ustawiać wskaźnika na NULL zamiast jakiegoś losowego adresu ???

Zmiana wartości wskaźnika na jakąkolwiek starą wartość mieści się w specyfikacji języka. Jeśli chodzi o zmianę na NULL, powiedziałbym, że byłoby źle. Program zachowywałby się bardziej rozsądnie, gdyby wartość wskaźnika była ustawiona na NULL. Jednak to ukryje problem. Gdy program zostanie skompilowany z różnymi ustawieniami optymalizacji lub przeniesiony do innego środowiska, problem prawdopodobnie pojawi się w najbardziej nieodpowiednim momencie.

R Sahu
źródło
1
Nie wierzę, że odpowiada na pytanie OP.
SergeyA
Nie zgadzam się nawet po edycji. Ustawienie jej na NULL nie spowoduje ukrycia problemu - w rzeczywistości narażałoby go w większej liczbie przypadków niż bez tego. Jest powód, dla którego normalne implementacje tego nie robią, a przyczyna jest inna.
SergeyA
4
@SergeyA, większość implementacji nie robi tego ze względu na wydajność. Jeśli jednak implementacja zdecyduje się ją ustawić, lepiej ustawić ją na coś innego niż NULL. Ujawniłoby problemy wcześniej, niż gdyby było ustawione na NULL. Jest ustawiony na NULL, deletedwukrotne wywołanie wskaźnika nie spowodowałoby problemu. To zdecydowanie nie jest dobre.
R Sahu
Nie, nie wydajność - przynajmniej nie jest to głównym problemem.
SergeyA
7
@SergeyA Ustawienie wskaźnika na wartość NULLznajdującą się poza przestrzenią adresową procesu, ale także zdecydowanie poza przestrzenią adresową procesu, ujawni więcej przypadków niż dwie alternatywy. Pozostawienie go wiszącego niekoniecznie spowoduje segfault, jeśli zostanie użyty po uwolnieniu; ustawienie go na NULLnie spowoduje segfault, jeśli zostanie deleteponownie d.
Blacklight Shining
10
delete ptr;
cout << "ptr = " << ptr << endl;

Ogólnie rzecz biorąc, nawet czytanie (tak jak powyżej, uwaga: różni się to od wyłuskiwania) wartości nieprawidłowych wskaźników (wskaźnik staje się nieprawidłowy, na przykład, gdy deletego wykonujesz) jest zachowaniem zdefiniowanym w implementacji. Zostało to wprowadzone w CWG # 1438 . Zobacz także tutaj .

Zwróć uwagę, że przed tym odczytem wartości nieprawidłowych wskaźników było niezdefiniowane zachowanie, więc to, co masz powyżej, byłoby niezdefiniowanym zachowaniem, co oznacza, że ​​wszystko może się zdarzyć.

giorgim
źródło
3
Istotny jest również cytat z [basic.stc.dynamic.deallocation]: „Jeśli argument podany funkcji cofnięcia alokacji w bibliotece standardowej jest wskaźnikiem, który nie jest wartością wskaźnika zerowego, funkcja cofnięcia alokacji powinna zwolnić miejsce przechowywania, do którego odwołuje się wskaźnik, unieważniając wszystkie wskaźniki odnoszące się do dowolnego część zwolnionej pamięci ”i reguła w [conv.lval](sekcja 4.1), która mówi, że odczytywanie (konwersja lwartości-> rwartości) jakiejkolwiek nieprawidłowej wartości wskaźnika jest zachowaniem zdefiniowanym przez implementację.
Ben Voigt
Nawet UB może zostać zaimplementowany w określony sposób przez konkretnego dostawcę, tak aby był niezawodny, przynajmniej dla tego kompilatora. Gdyby Microsoft zdecydował się zaimplementować swoją funkcję oczyszczania wskaźników przed CWG # 1438, nie uczyniłoby to tej funkcji mniej lub bardziej niezawodną, ​​aw szczególności po prostu nieprawdą jest, że „wszystko może się zdarzyć”, jeśli ta funkcja jest włączona niezależnie od tego, co mówi norma.
Kyle Strand
@KyleStrand: W zasadzie podałem definicję UB ( blog.regehr.org/archives/213 ).
giorgim
1
Dla większości społeczności C ++ na SO „wszystko może się zdarzyć” jest traktowane zbyt dosłownie . Myślę, że to śmieszne . Rozumiem definicję UB, ale rozumiem też, że kompilatory to po prostu fragmenty oprogramowania zaimplementowane przez prawdziwych ludzi, a jeśli ci ludzie implementują kompilator tak, aby zachowywał się w określony sposób, to tak kompilator będzie się zachowywał niezależnie od tego, co mówi norma .
Kyle Strand
1

Wydaje mi się, że używasz pewnego rodzaju trybu debugowania, a VS próbuje ponownie wskazać wskaźnik do jakiejś znanej lokalizacji, aby dalsza próba wyłuskiwania mogła zostać wyśledzona i zgłoszona. Spróbuj skompilować / uruchomić ten sam program w trybie wydania.

Wskaźniki zwykle nie są zmieniane wewnątrz deleteze względu na wydajność i uniknięcie fałszywego wyobrażenia o bezpieczeństwie. Ustawienie wskaźnika usuwania na wstępnie zdefiniowaną wartość nie przyniesie żadnych korzyści w większości złożonych scenariuszy, ponieważ usuwany wskaźnik będzie prawdopodobnie tylko jednym z kilku wskazujących na tę lokalizację.

Prawdę mówiąc, im więcej o tym myślę, tym bardziej uważam, że VS jest winny, gdy to robię, jak zwykle. A co, jeśli wskaźnik jest const? Czy nadal to zmieni?

Siergiej A.
źródło
Tak, nawet stałe wskaźniki są przekierowywane do tego tajemniczego 8123!
tjwrona1992
Kolejny kamień do VS :) Właśnie dziś rano ktoś zapytał, dlaczego powinni używać g ++ zamiast VS. Oto jest.
SergeyA
7
@SergeyA ale z drugiej strony usunięcie tego usuniętego wskaźnika pokaże ci przez segfault, że próbowałeś wyrejestrować usunięty wskaźnik i nie będzie równy NULL. W innym przypadku nastąpi awaria tylko wtedy, gdy strona również zostanie zwolniona (co jest bardzo mało prawdopodobne). Awaria szybciej; rozwiązać szybciej.
ratchet freak
1
@ratchetfreak „Szybka porażka, rozwiązanie szybciej” to bardzo cenna mantra, ale „Szybka porażka przez zniszczenie kluczowych dowodów kryminalistycznych” nie rozpoczyna tak cennej mantry. W prostych przypadkach może to być wygodne, ale w bardziej skomplikowanych przypadkach (takich, w których potrzebujemy największej pomocy), usunięcie cennych informacji zmniejsza ilość dostępnych narzędzi do rozwiązania problemu.
Cort Ammon
2
@ tjwrona1992: Microsoft moim zdaniem postępuje słusznie. Odkażanie jednego wskaźnika jest lepsze niż jego brak. A jeśli powoduje to problem z debugowaniem, umieść punkt przerwania przed złym wywołaniem usuwania. Istnieje prawdopodobieństwo, że bez czegoś takiego nigdy nie zauważysz problemu. A jeśli masz lepsze rozwiązanie do lokalizowania tych błędów, użyj go i dlaczego przejmujesz się tym, co robi Microsoft?
Zan Lynx
0

Po usunięciu wskaźnika pamięć, na którą wskazuje, może nadal działać. Aby zamanifestować ten błąd, wartość wskaźnika jest ustawiana na oczywistą wartość. To naprawdę pomaga w procesie debugowania. Jeśli wartość została ustawiona na NULL, może nigdy nie pojawić się jako potencjalny błąd w przebiegu programu. Więc może ukryć błąd podczas późniejszego testowania NULL.

Inną kwestią jest to, że jakiś optymalizator czasu wykonywania może sprawdzić tę wartość i zmienić jej wyniki.

We wcześniejszych czasach MS ustawiało tę wartość na 0xcfffffff.

Karsten
źródło