Cosa fa Visual Studio con un puntatore eliminato e perché?


130

Un libro in C ++ che ho letto afferma che quando un puntatore viene eliminato utilizzando l' deleteoperatore, la memoria nella posizione a cui punta viene "liberata" e può essere sovrascritta. Indica inoltre che il puntatore continuerà a puntare nella stessa posizione fino a quando non viene riassegnato o impostato su NULL.

In Visual Studio 2012 tuttavia; questo non sembra essere il caso!

Esempio:

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

Quando compilo ed eseguo questo programma ottengo il seguente output:

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

Chiaramente l'indirizzo a cui punta il puntatore cambia quando viene chiamato delete!

Perché sta succedendo? Questo ha qualcosa a che fare con Visual Studio in particolare?

E se cancellare può cambiare l'indirizzo a cui punta comunque, perché eliminare non dovrebbe impostare automaticamente il puntatore NULLinvece di un indirizzo casuale?


4
Elimina un puntatore, non significa che sarà impostato su NULL, devi occupartene.
Matt,

11
Lo so, ma il libro che sto leggendo dice che conterrà comunque lo stesso indirizzo a cui puntava prima dell'eliminazione, ma il contenuto di quell'indirizzo potrebbe essere sovrascritto.
tjwrona1992,

6
@ tjwrona1992, sì, perché questo è ciò che accade di solito. Il libro elenca solo i risultati più probabili, non la regola difficile.
SergeyA

5
@ tjwrona1992 Un libro in C ++ che ho letto - e il nome del libro è ...?
PaulMcKenzie,

4
@ tjwrona1992: può essere sorprendente, ma è tutto l'utilizzo del valore del puntatore non valido che è un comportamento indefinito, non solo la dereferenziazione. "Verifica dove sta puntando" IS utilizza il valore in modo non consentito.
Ben Voigt,

Risposte:


175

Ho notato che l'indirizzo memorizzato ptrveniva sempre sovrascritto con 00008123...

Mi è sembrato strano, quindi ho fatto un po 'di ricerche e ho trovato questo post sul blog di Microsoft contenente una sezione che discute "Sanificazione automatizzata del puntatore durante l'eliminazione di oggetti C ++".

... i controlli per NULL sono un costrutto di codice comune, il che significa che un controllo esistente per NULL combinato con l'utilizzo di NULL come valore di sanificazione potrebbe nascondere casualmente un vero problema di sicurezza della memoria la cui causa principale necessita davvero di essere risolta.

Per questo motivo abbiamo scelto 0x8123 come valore di sanificazione: dal punto di vista del sistema operativo questo si trova nella stessa pagina di memoria dell'indirizzo zero (NULL), ma una violazione di accesso a 0x8123 si distinguerà meglio per lo sviluppatore che necessita di un'attenzione più dettagliata .

Non solo spiega cosa fa Visual Studio con il puntatore dopo che è stato eliminato, ma risponde anche perché hanno scelto di NON impostarlo NULLautomaticamente!


Questa "funzione" è abilitata come parte dell'impostazione "Controlli SDL". Per abilitarlo / disabilitarlo vai a: PROGETTO -> Proprietà -> Proprietà di configurazione -> C / C ++ -> Generale -> Controlli SDL

Per confermare questo:

La modifica di questa impostazione e la riesecuzione dello stesso codice producono il seguente output:

ptr = 007CBC10
ptr = 007CBC10

"feature" è racchiuso tra virgolette perché nel caso in cui si disponga di due puntatori nella stessa posizione, la chiamata a delete eliminerà solo UNO di essi. L'altro verrà lasciato puntando alla posizione non valida ...


AGGIORNARE:

Dopo altri 5 anni di esperienza nella programmazione C ++, mi rendo conto che l'intero problema è sostanzialmente un punto controverso. Se sei un programmatore C ++ e stai ancora utilizzando newe deletegestendo puntatori non elaborati anziché utilizzare puntatori intelligenti (che eludono l'intero problema), potresti prendere in considerazione un cambiamento nel percorso di carriera per diventare un programmatore C. ;)


12
È una bella scoperta. Vorrei che MS documentasse meglio il comportamento di debug in questo modo. Ad esempio, sarebbe bello sapere quale versione del compilatore ha iniziato a implementare questo e quali opzioni abilitano / disabilitano il comportamento.
Michael Burr,

5
"dal punto di vista del sistema operativo questo è nella stessa pagina di memoria dell'indirizzo zero" - eh? Le dimensioni della pagina standard (ignorando le pagine di grandi dimensioni) su x86 non sono ancora 4kb sia per Windows che per Linux? Anche se ricordo vagamente qualcosa sul primo 64kb di spazio degli indirizzi sul blog di Raymond Chen, quindi in pratica prendo lo stesso risultato,
Voo

12
@Voo windows riserva il primo (e ultimo) 64 KB di RAM come spazio morto per il trapping. 0x8123 cade bene lì
maniaco del cricchetto,

7
In realtà, non incoraggia le cattive abitudini e non ti consente di saltare l'impostazione del puntatore su NULL - questa è l'intera ragione che stanno usando 0x8123invece di 0. Il puntatore non è ancora valido, ma provoca un'eccezione quando si tenta di dereferenziarlo (buono) e non passa i controlli NULL (anche buono, perché è un errore non farlo). Dov'è il posto delle cattive abitudini? È davvero solo qualcosa che ti aiuta a eseguire il debug.
Luaan,

3
Bene, non è possibile impostare entrambi (tutti), quindi questa è la seconda opzione migliore. Se non ti piace, basta disattivare i controlli SDL: li trovo piuttosto utili, specialmente quando eseguo il debug del codice di qualcun altro.
Luaan,

30

Vedi gli effetti collaterali /sdldell'opzione di compilazione. Attivato per impostazione predefinita per i progetti VS2015, consente controlli di sicurezza aggiuntivi oltre a quelli forniti da / gs. Usa Progetto> Proprietà> C / C ++> Generale> Impostazioni dei controlli SDL per modificarlo.

Citando l' articolo di MSDN :

  • Esegue una sanificazione limitata del puntatore. Nelle espressioni che non implicano dereferenze e nei tipi che non hanno un distruttore definito dall'utente, i riferimenti del puntatore vengono impostati su un indirizzo non valido dopo una chiamata da eliminare. Questo aiuta a prevenire il riutilizzo di riferimenti a puntatori non aggiornati.

Tieni presente che l'impostazione dei puntatori eliminati su NULL è una cattiva pratica quando si utilizza MSVC. Elimina l'aiuto che ricevi sia dall'heap di debug sia da questa opzione / sdl, non puoi più rilevare chiamate libere / cancellate non valide nel tuo programma.


1
Confermato. Dopo aver disabilitato questa funzione, il puntatore non viene più reindirizzato. Grazie per aver fornito l'impostazione effettiva che la modifica!
tjwrona1992,

Hans, è ancora considerata una cattiva pratica impostare i puntatori eliminati su NULL nel caso in cui due puntatori puntino nella stessa posizione? Quando lo fai delete, Visual Studio lascerà il secondo puntatore che punta alla sua posizione originale che ora non è valida.
tjwrona1992,

1
Abbastanza poco chiaro per me che tipo di magia ti aspetti di accadere impostando il puntatore su NULL. L'altro puntatore non è quindi non risolve nulla, è ancora necessario l'allocatore di debug per trovare il bug.
Hans Passant,

3
VS non pulisce i puntatori. Li corrompe. Quindi il tuo programma andrà in crash quando li usi comunque. L'allocatore di debug fa la stessa cosa con la memoria heap. Il grosso problema con NULL non è abbastanza corrotto. Altrimenti una strategia comune, google "0xdeadbeef".
Hans Passant,

1
Impostare il puntatore su NULL è ancora molto meglio che lasciarlo puntare al suo indirizzo precedente che ora non è valido. Il tentativo di scrivere su un puntatore NULL non corromperà alcun dato e probabilmente causerà l'arresto anomalo del programma. Il tentativo di riutilizzare il puntatore a quel punto potrebbe non causare l'arresto anomalo del programma, ma potrebbe solo produrre risultati molto imprevedibili!
tjwrona1992,

19

Indica inoltre che il puntatore continuerà a puntare nella stessa posizione fino a quando non viene riassegnato o impostato su NULL.

Questa è sicuramente un'informazione fuorviante.

Chiaramente l'indirizzo a cui punta il puntatore cambia quando viene chiamato delete!

Perché sta succedendo? Questo ha qualcosa a che fare con Visual Studio in particolare?

Questo è chiaramente all'interno delle specifiche della lingua. ptrnon è valido dopo la chiamata a delete. L'uso ptrdopo che è stato deleted è causa di comportamento indefinito. Non farlo L'ambiente di runtime è libero di fare tutto ciò che vuole ptrdopo la chiamata a delete.

E se delete può cambiare l'indirizzo a cui punta comunque, perché non eliminare automaticamente impostare il puntatore su NULL invece di un indirizzo casuale ???

La modifica del valore del puntatore su qualsiasi valore precedente rientra nelle specifiche della lingua. Per quanto riguarda il cambio in NULL, direi, sarebbe male. Il programma si comporterebbe in modo più sano se il valore del puntatore fosse impostato su NULL. Tuttavia, ciò nasconderà il problema. Quando il programma viene compilato con impostazioni di ottimizzazione diverse o trasferito in un ambiente diverso, il problema verrà probabilmente visualizzato nel momento più inopportuno.


1
Non credo che risponda alla domanda di OP.
Sergey,

Non sono d'accordo anche dopo la modifica. Impostarlo su NULL non nasconderà il problema, anzi lo esporrà in più casi che senza. C'è una ragione per cui le normali implementazioni non lo fanno e la ragione è diversa.
Sergey,

4
@SergeyA, la maggior parte delle implementazioni non lo fa per motivi di efficienza. Tuttavia, se un'implementazione decide di impostarla, è meglio impostarla su qualcosa che non è NULL. Rivelerebbe i problemi prima che se fosse impostato su NULL. È impostato su NULL, la chiamata deletedue volte sul puntatore non causerebbe un problema. Questo sicuramente non va bene.
R Sahu,

No, non l'efficienza - almeno, non è la preoccupazione principale.
Sergey,

7
@SergeyA L'impostazione di un puntatore su un valore che non è, NULLma anche decisamente al di fuori dello spazio degli indirizzi del processo esporrà più casi delle due alternative. Lasciarlo penzolare non causerà necessariamente un segfault se viene usato dopo essere stato liberato; impostandolo su NULLnon causerà un segfault se è di deletenuovo d.
Blacklight Shining,

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

In generale, anche la lettura (come si fa sopra, nota: questo è diverso dal dereferenziamento) dei valori di puntatori non validi (il puntatore diventa non valido ad esempio quando lo si deletefa) è un comportamento definito dall'implementazione. Questo è stato introdotto in CWG # 1438 . Vedi anche qui .

Si noti che prima di leggere i valori dei puntatori non validi era un comportamento indefinito, quindi ciò che si ha sopra sarebbe un comportamento indefinito, il che significa che potrebbe succedere di tutto.


3
Rilevante è anche la citazione di [basic.stc.dynamic.deallocation]: "Se l'argomento fornito a una funzione di deallocazione nella libreria standard è un puntatore che non è il valore del puntatore null, la funzione di deallocazione deve deallocare la memoria a cui fa riferimento il puntatore, rendendo non validi tutti i puntatori che si riferiscono a qualsiasi parte della memoria deallocata "e la regola in [conv.lval](sezione 4.1) che dice che la lettura (conversione lvalue-> rvalue) qualsiasi valore di puntatore non valido è un comportamento definito dall'implementazione.
Ben Voigt,

Anche UB può essere implementato in un modo specifico da un fornitore specifico in modo che sia affidabile, almeno per quel compilatore. Se Microsoft avesse deciso di implementare la propria funzionalità di sanificazione del puntatore prima di CWG # 1438, ciò non avrebbe reso tale funzionalità più o meno affidabile, e in particolare non è vero che "potrebbe accadere qualcosa" se tale funzionalità fosse attivata , indipendentemente da ciò che dice lo standard.
Kyle Strand,

@KyleStrand: ho praticamente dato la definizione di UB ( blog.regehr.org/archives/213 ).
giorgim,

1
Per la maggior parte della comunità C ++ su SO, "tutto potrebbe succedere" è preso alla lettera in modo troppo letterale . Penso che sia ridicolo . Capisco la definizione di UB, ma capisco anche che i compilatori sono solo pezzi di software implementati da persone reali, e se quelle persone implementano il compilatore in modo che si comporti in un certo modo, è così che si comporteranno i compilatori , indipendentemente da ciò che dice lo standard .
Kyle Strand,

1

Credo che tu stia eseguendo una sorta di modalità di debug e VS sta tentando di reimpostare il puntatore in una posizione nota, in modo che si possa rintracciare e riportare ulteriori tentativi di dereferenza. Prova a compilare / eseguire lo stesso programma in modalità di rilascio.

I puntatori di solito non vengono cambiati all'interno deleteper motivi di efficienza ed evitare di dare una falsa idea di sicurezza. L'impostazione del puntatore di eliminazione su un valore predefinito non funzionerà nella maggior parte degli scenari complessi, poiché è probabile che il puntatore che si sta eliminando sia solo uno dei tanti che puntano a questa posizione.

È un dato di fatto, più ci penso, più trovo che VS sia in colpa quando lo fa, come al solito. Cosa succede se il puntatore è const? Lo cambierà ancora?


Sì, anche i puntatori costanti vengono reindirizzati a questo misterioso 8123!
tjwrona1992,

Ecco un'altra pietra per VS :) Proprio stamattina qualcuno ha chiesto perché dovrebbero usare g ++ invece di VS. Eccolo.
Sergey,

7
@SergeyA ma dall'altra parte dereffing che il puntatore eliminato ti mostrerà per segfault che hai provato a deref un puntatore eliminato e non sarà uguale a NULL. Nell'altro caso andrà in crash solo se anche la pagina viene liberata (il che è molto improbabile). Fallire più velocemente; risolvere prima.
maniaco del cricchetto,

1
@ratchetfreak "Fallire velocemente, risolvere prima" è un mantra molto prezioso, ma "Fallire velocemente distruggendo le prove forensi chiave" non avvia un mantra così prezioso. In casi semplici, può essere conveniente, ma in casi più complicati (quelli su cui tendiamo ad avere più bisogno di aiuto), la cancellazione di informazioni preziose riduce i miei strumenti disponibili per risolvere il problema.
Cort Ammon,

2
@ tjwrona1992: Microsoft sta facendo la cosa giusta qui secondo me. Disinfettare un puntatore è meglio che non farne affatto. E se ciò causa un problema nel debug, inserire un punto di interruzione prima della chiamata di eliminazione non valida. Le probabilità sono che senza qualcosa del genere non avresti mai individuato il problema. E se hai una soluzione migliore per individuare questi bug, allora usalo e perché ti importa cosa fa Microsoft?
Zan Lynx,

0

Dopo aver eliminato il puntatore, la memoria a cui punta potrebbe essere ancora valida. Per manifestare questo errore, il valore del puntatore è impostato su un valore ovvio. Questo aiuta davvero il processo di debug. Se il valore fosse impostato su NULL, potrebbe non apparire mai come potenziale bug nel flusso del programma. Quindi potrebbe nascondere un bug quando si esegue il test in seguito contro NULL.

Un altro punto è che alcuni ottimizzatori del tempo di esecuzione possono controllare quel valore e modificarne i risultati.

In tempi precedenti MS ha impostato il valore su 0xcfffffff.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.