Testare i puntatori per la validità (C / C ++)


90

C'è un modo per determinare (programmaticamente, ovviamente) se un dato puntatore è "valido"? Verificare la presenza di NULL è facile, ma per quanto riguarda cose come 0x00001234? Quando si tenta di dereferenziare questo tipo di puntatore, si verifica un'eccezione / arresto anomalo.

È preferibile un metodo multipiattaforma, ma va bene anche quello specifico della piattaforma (per Windows e Linux).

Aggiornamento per chiarimenti: il problema non è con i puntatori obsoleti / liberati / non inizializzati; invece, sto implementando un'API che prende i puntatori dal chiamante (come un puntatore a una stringa, un handle di file, ecc.). Il chiamante può inviare (intenzionalmente o per errore) un valore non valido come puntatore. Come prevengo un incidente?



Penso che la migliore risposta positiva per Linux sia data da George Carrette. Se ciò non è sufficiente, prendere in considerazione la creazione della tabella dei simboli di funzione nella libreria o anche un altro livello di tabella di librerie disponibili con le proprie tabelle di funzione. Quindi controlla quelle tabelle esatte. Naturalmente, anche quelle risposte negative sono corrette: non puoi essere sicuro al 100% se un puntatore a funzione è valido o meno a meno che non imposti molte restrizioni aggiuntive all'applicazione utente.
minghua

La specifica API specifica effettivamente tale obbligo da soddisfare mediante l'implementazione? A proposito, fingo di non aver pensato che tu sia sia lo sviluppatore che il designer. Il punto è che non credo che un'API specifichi qualcosa come "Nel caso in cui un puntatore non valido venga passato come argomento, la funzione deve gestire il problema e restituisce NULL". Un'API si assume l'obbligo di fornire un servizio in condizioni di utilizzo adeguate, non tramite hack. Tuttavia, non fa male essere un po 'stupido. L'uso di un riferimento fa sì che tali casi si diffondano meno. :)
Poniros

Risposte:


75

Aggiornamento per chiarimenti: il problema non riguarda i puntatori obsoleti, liberati o non inizializzati; invece, sto implementando un'API che prende i puntatori dal chiamante (come un puntatore a una stringa, un handle di file, ecc.). Il chiamante può inviare (intenzionalmente o per errore) un valore non valido come puntatore. Come prevengo un incidente?

Non puoi fare quel controllo. Semplicemente non è possibile verificare se un puntatore è "valido". Devi fidarti che quando le persone usano una funzione che accetta un puntatore, quelle persone sanno cosa stanno facendo. Se ti passano 0x4211 come valore del puntatore, devi fidarti che punta all'indirizzo 0x4211. E se "accidentalmente" colpiscono un oggetto, allora anche se usereste qualche spaventosa funzione del sistema operativo (IsValidPtr o qualsiasi altra cosa), vi ritrovereste comunque in un bug e non fallirete velocemente.

Inizia a usare puntatori nulli per segnalare questo genere di cose e dì all'utente della tua libreria che non dovrebbero usare puntatori se tendono a passare accidentalmente puntatori non validi, seriamente :)


Questa è probabilmente la risposta giusta ma penso che una semplice funzione che controlla le posizioni di memoria hexspeak comuni sarebbe utile per il debug generale ... In questo momento ho un puntatore che a volte punta a 0xfeeefeee e se avessi una semplice funzione che potrei Usa per pepare affermazioni in giro Renderebbe molto più facile trovare il colpevole ... EDIT: Anche se non sarebbe difficile scriverne uno da solo immagino ..
quant

@quant il problema è che alcuni codici C e C ++ potrebbero eseguire operazioni aritmetiche sui puntatori su un indirizzo non valido senza controllare (in base al principio garbage-in, garbage-out) e quindi passeranno un puntatore "aritmeticamente modificato" da uno di questi pozzetti - indirizzi non validi noti. Casi comuni sono la ricerca di un metodo da un vtable inesistente basato su un indirizzo di oggetto non valido o di un tipo sbagliato, o semplicemente la lettura di campi da un puntatore a una struttura che non punta a uno.
rwong

Ciò significa fondamentalmente che puoi prendere solo indici di array dal mondo esterno. Un'API che deve difendersi dal chiamante non può avere puntatori nell'interfaccia. Tuttavia, sarebbe comunque utile disporre di macro da utilizzare nelle asserzioni sulla validità dei puntatori (che sei tenuto ad avere internamente). Se è garantito che un puntatore punti all'interno di un array di cui si conoscono il punto di partenza e la lunghezza, è possibile verificarlo esplicitamente. È meglio morire per una violazione asserita (errore documentato) che per deref (errore non documentato).
Rob

34

Ecco tre semplici modi in cui un programma C sotto Linux può essere introspettivo sullo stato della memoria in cui è in esecuzione e perché la domanda ha risposte appropriate e sofisticate in alcuni contesti.

  1. Dopo aver chiamato getpagesize () e arrotondato il puntatore al limite di una pagina, puoi chiamare mincore () per scoprire se una pagina è valida e se fa parte del working set del processo. Nota che questo richiede alcune risorse del kernel, quindi dovresti confrontarlo e determinare se chiamare questa funzione è davvero appropriato nella tua API. Se la tua API gestirà gli interrupt o leggerà dalle porte seriali in memoria, è opportuno chiamarlo per evitare comportamenti imprevedibili.
  2. Dopo aver chiamato stat () per determinare se è disponibile una directory / proc / self, è possibile aprire e leggere / proc / self / maps per trovare informazioni sulla regione in cui risiede un puntatore. Studia la pagina man di proc, lo pseudo-file system delle informazioni di processo. Ovviamente questo è relativamente costoso, ma potresti riuscire a farla franca memorizzando nella cache il risultato dell'analisi in un array che puoi cercare in modo efficiente utilizzando una ricerca binaria. Considera anche / proc / self / smaps. Se la tua API è per l'elaborazione ad alte prestazioni, il programma vorrà conoscere / proc / self / numa che è documentato nella pagina man di numa, l'architettura di memoria non uniforme.
  3. La chiamata get_mempolicy (MPOL_F_ADDR) è appropriata per il lavoro di API di elaborazione ad alte prestazioni in cui sono presenti più thread di esecuzione e si sta gestendo il proprio lavoro in modo che abbia affinità per la memoria non uniforme in relazione ai core della CPU e alle risorse del socket. Tale API ovviamente ti dirà anche se un puntatore è valido.

Sotto Microsoft Windows c'è la funzione QueryWorkingSetEx che è documentata sotto l'API Process Status (anche nell'API NUMA). Come corollario alla sofisticata programmazione dell'API NUMA, questa funzione ti consentirà anche di eseguire semplici operazioni di "verifica dei puntatori di validità (C / C ++)", in quanto tale è improbabile che venga deprecata per almeno 15 anni.


12
Prima risposta che non cerca di essere morale sulla domanda in sé e anzi risponde perfettamente. Le persone a volte non si rendono conto che è davvero necessario questo tipo di approccio di debug per trovare bug ad es. Librerie di terze parti o codice legacy perché anche valgrind trova puntatori selvaggi solo quando vi accede effettivamente, non ad es. Se si desidera controllare regolarmente la validità dei puntatori in una tabella della cache che è stata sovrascritta da qualche altra parte nel codice ...
lumpidu

Questa dovrebbe essere la risposta accettata. L'ho fatto simliarly su una piattaforma non Linux. Fondamentalmente sta esponendo le informazioni sul processo al processo stesso. Con questo aspetto, sembra che Windows faccia un lavoro migliore di Linux esponendo le informazioni più significative attraverso l'API dello stato del processo.
minghua

31

Prevenire un crash causato dall'invio di un puntatore non valido da parte del chiamante è un buon modo per creare bug silenziosi difficili da trovare.

Non è meglio per il programmatore che utilizza la tua API ricevere un messaggio chiaro che il suo codice è fasullo bloccandolo invece di nasconderlo?


8
In alcuni casi, tuttavia, verificare la presenza di un cattivo puntatore immediatamente quando viene chiamata l'API è il modo in cui fallisci presto. Ad esempio, cosa succede se l'API memorizza il puntatore in una struttura di dati in cui verrà rinviato solo in seguito? Quindi passare all'API un puntatore errato causerà un arresto anomalo in un punto successivo casuale. In tal caso sarebbe meglio fallire prima, alla chiamata API in cui è stato originariamente introdotto il valore errato.
peterflynn

28

Su Win32 / 64 c'è un modo per farlo. Tenta di leggere il puntatore e catturare l'eccezione SEH risultante che verrà lanciata in caso di errore. Se non lancia, allora è un puntatore valido.

Il problema con questo metodo è che restituisce solo se puoi leggere o meno i dati dal puntatore. Non garantisce l'indipendenza dai tipi o un numero qualsiasi di altre invarianti. In generale questo metodo è utile per poco altro che dire "sì, posso leggere quel particolare luogo nella memoria in un tempo che è ormai passato".

In breve, non farlo;)

Raymond Chen ha un post sul blog su questo argomento: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


3
@Tim, non c'è modo di farlo in C ++.
JaredPar

6
È solo la "risposta giusta" se si definisce "puntatore valido" come "non causa una violazione / segfault di accesso". Preferisco definirlo come "punta a dati significativi allocati per lo scopo che intendi utilizzare". Direi che è una definizione migliore di validità del puntatore ...;)
jalf

Anche se il puntatore è valido non può essere verificato in questo modo. Pensa a thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {sleep (1); eliminare p; ...}
Christopher,

2
@Christopher, molto vero. Avrei dovuto dire "Posso leggere quel particolare luogo nella memoria in un momento che è ormai passato"
JaredPar

@ JaredPar: suggerimento davvero pessimo. Può attivare una pagina di guardia, quindi la pila non verrà espansa in seguito o qualcosa di altrettanto carino.
Deduplicatore

16

AFAIK non c'è modo. Dovresti cercare di evitare questa situazione impostando sempre i puntatori su NULL dopo aver liberato memoria.


4
L'impostazione di un puntatore a null non ti dà nulla, tranne forse un falso senso di sicurezza.

Quello non è vero. Specialmente in C ++ è possibile determinare se eliminare gli oggetti membro controllando null. Si noti inoltre che, in C ++, è valido eliminare i puntatori nulli, quindi l'eliminazione incondizionata di oggetti nei distruttori è popolare.
Ferdinand Beyer

4
int * p = nuovo int (0); int * p2 = p; eliminare p; p = NULL; eliminare p2; // incidente

1
zabzonk e ?? quello che ha detto è che puoi eliminare un puntatore nullo. p2 non è un puntatore nullo, ma è un puntatore non valido. devi prima impostarlo su null.
Johannes Schaub - litb

2
Se hai degli alias alla memoria puntati, solo uno di essi sarebbe impostato su NULL, altri alias penzolano intorno.
jdehaan


7

Per quanto riguarda la risposta un po 'più in alto in questo thread:

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () per Windows.

Il mio consiglio è di stare lontano da loro, qualcuno ha già pubblicato questo: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

Un altro post sullo stesso argomento e dello stesso autore (credo) è questo: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ("IsBadXxxPtr dovrebbe davvero chiamarsi CrashProgramRandomly ").

Se gli utenti della tua API inviano dati non validi, lascia che si blocchi. Se il problema è che i dati passati non vengono utilizzati fino a tardi (e questo rende più difficile trovare la causa), aggiungi una modalità di debug in cui le stringhe ecc. Vengono registrate all'ingresso. Se sono cattivi sarà ovvio (e probabilmente si bloccherà). Se accade molto spesso, potrebbe valere la pena spostare la tua API fuori dal processo e lasciarli bloccare il processo API invece del processo principale.


Probabilmente un altro modo è usare _CrtIsValidHeapPointer . Questa funzione restituirà TRUE se il puntatore è valido e genererà un'eccezione quando il puntatore viene liberato. Come documentato, questa funzione è disponibile solo in CRT di debug.
Crend King

6

In primo luogo, non vedo alcun motivo per cercare di proteggersi dal chiamante che cerca deliberatamente di causare un incidente. Potrebbero farlo facilmente tentando di accedere tramite un puntatore non valido. Ci sono molti altri modi: potrebbero semplicemente sovrascrivere la tua memoria o lo stack. Se è necessario proteggersi da questo genere di cose, è necessario eseguire un processo separato utilizzando socket o un altro IPC per la comunicazione.

Scriviamo un bel po 'di software che consente a partner / clienti / utenti di estendere le funzionalità. Inevitabilmente qualsiasi bug ci viene segnalato per primo, quindi è utile poter mostrare facilmente che il problema è nel codice del plug-in. Inoltre ci sono problemi di sicurezza e alcuni utenti sono più affidabili di altri.

Utilizziamo una serie di metodi diversi a seconda dei requisiti di prestazioni / velocità effettiva e affidabilità. Dal più preferito:

  • processi separati utilizzando socket (spesso passando i dati come testo).

  • processi separati utilizzando la memoria condivisa (se grandi quantità di dati da trasferire).

  • lo stesso processo separa i thread tramite la coda dei messaggi (se frequenti messaggi brevi).

  • stesso processo thread separati tutti i dati passati allocati da un pool di memoria.

  • stesso processo tramite chiamata di procedura diretta - tutti i dati passati allocati da un pool di memoria.

Cerchiamo di non ricorrere mai a ciò che stai cercando di fare quando hai a che fare con software di terze parti, specialmente quando ci vengono forniti i plug-in / la libreria come codice binario anziché come codice sorgente.

L'uso di un pool di memoria è abbastanza facile nella maggior parte dei casi e non deve essere inefficiente. Se VOI allocare i dati in primo luogo, è banale controllare i puntatori rispetto ai valori assegnati. È inoltre possibile memorizzare la lunghezza allocata e aggiungere valori "magici" prima e dopo i dati per verificare il tipo di dati valido e gli overrun dei dati.


4

Ho molta simpatia per la tua domanda, poiché anch'io sono in una posizione quasi identica. Apprezzo ciò che dicono molte delle risposte e sono corrette: la routine che fornisce il puntatore dovrebbe fornire un puntatore valido. Nel mio caso, è quasi inconcepibile che possano aver danneggiato il puntatore, ma se ci fossero riusciti, sarebbe stato il MIO software a bloccarsi e ME a prendersi la colpa :-(

Il mio requisito non è che io continui dopo un errore di segmentazione - sarebbe pericoloso - Voglio solo segnalare cosa è successo al cliente prima di terminare in modo che possa correggere il suo codice piuttosto che incolpare me!

Ecco come ho scoperto di farlo (su Windows): http://www.cplusplus.com/reference/clibrary/csignal/signal/

Per dare una sinossi:

#include <signal.h>

using namespace std;

void terminate(int param)
/// Function executed if a segmentation fault is encountered during the cast to an instance.
{
  cerr << "\nThe function received a corrupted reference - please check the user-supplied  dll.\n";
  cerr << "Terminating program...\n";
  exit(1);
}

...
void MyFunction()
{
    void (*previous_sigsegv_function)(int);
    previous_sigsegv_function = signal(SIGSEGV, terminate);

    <-- insert risky stuff here -->

    signal(SIGSEGV, previous_sigsegv_function);
}

Ora sembra che si comporti come spero (stampa il messaggio di errore, quindi termina il programma) - ma se qualcuno può individuare un difetto, per favore fatemelo sapere!


Non utilizzare exit(), aggira RAII e quindi può causare perdite di risorse.
Sebastian Mach

Interessante: esiste un altro modo per terminare ordinatamente questa situazione? E l'istruzione di uscita è l'unico problema nel farlo in questo modo? Ho notato di aver acquisito un "-1" - è solo a causa dell '"uscita"?
Mike Sadler

Ops, mi rendo conto che è per una situazione piuttosto eccezionale. Ho appena visto exit()e il mio campanello d'allarme portatile C ++ ha iniziato a suonare. Dovrebbe andare bene in questa situazione specifica di Linux, dove il tuo programma uscirà comunque, scusa per il rumore.
Sebastian Mach

1
il segnale (2) non è portatile. Usa sigaction (2). man 2 signalsu Linux ha un paragrafo che spiega perché.
rptb1

1
In questa situazione di solito chiamerei abort (3) piuttosto che exit (3) perché è più probabile che produca un qualche tipo di backtrace di debug che puoi usare per diagnosticare il problema post mortem. Sulla maggior parte degli Unixen, abort (3) eseguirà il dump del core (se i core dump sono consentiti) e su Windows offrirà di avviare un debugger se installato.
rptb1

4

Su Unix dovresti essere in grado di utilizzare un kernel syscall che esegue il controllo del puntatore e restituisce EFAULT, come:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>

bool isPointerBad( void * p )
{
   int fh = open( p, 0, 0 );
   int e = errno;

   if ( -1 == fh && e == EFAULT )
   {
      printf( "bad pointer: %p\n", p );
      return true;
   }
   else if ( fh != -1 )
   {
      close( fh );
   }

   printf( "good pointer: %p\n", p );
   return false;
}

int main()
{
   int good = 4;
   isPointerBad( (void *)3 );
   isPointerBad( &good );
   isPointerBad( "/tmp/blah" );

   return 0;
}

ritorno:

bad pointer: 0x3
good pointer: 0x7fff375fd49c
good pointer: 0x400793

Probabilmente c'è una migliore syscall da usare rispetto a open () [forse access], poiché c'è la possibilità che questo possa portare alla creazione di file effettivi codepath e un successivo requisito di chiusura.


Questo è un trucco brillante. Mi piacerebbe ricevere consigli su diverse chiamate di sistema per convalidare gli intervalli di memoria, soprattutto se è possibile garantire che non abbiano effetti collaterali. Si potrebbe tenere un descrittore di file aperto per scrivere in / dev / null per verificare se i buffer sono in memoria leggibile, ma probabilmente ci sono soluzioni più semplici. Il meglio che riesco a trovare è symlink (ptr, "") che imposterà errno a 14 su un indirizzo errato o 2 su un indirizzo buono, ma le modifiche al kernel potrebbero cambiare l'ordine di verifica.
Preston,

@Preston In DB2 penso che usavamo access () di unistd.h. Ho usato open () sopra perché è un po 'meno oscuro, ma probabilmente hai ragione sul fatto che ci sono molte possibili chiamate di sistema da usare. Windows aveva un'API di controllo del puntatore esplicita, ma si è rivelata non thread-safe (penso che abbia usato SEH per provare a scrivere e quindi ripristinare i limiti dell'intervallo di memoria.)
Peeter Joot

2

Non ci sono disposizioni in C ++ per verificare la validità di un puntatore come caso generale. Ovviamente si può presumere che NULL (0x00000000) sia pessimo, e vari compilatori e librerie amano usare "valori speciali" qua e là per rendere più facile il debug (ad esempio, se mai vedo un puntatore apparire come 0xCECECECE in visual studio lo so Ho fatto qualcosa di sbagliato) ma la verità è che dal momento che un puntatore è solo un indice in memoria è quasi impossibile dire semplicemente guardando il puntatore se è l'indice "giusto".

Ci sono vari trucchi che puoi fare con dynamic_cast e RTTI tali per assicurarti che l'oggetto puntato sia del tipo che desideri, ma tutti richiedono che tu stia puntando a qualcosa di valido in primo luogo.

Se vuoi assicurarti che il tuo programma possa rilevare puntatori "non validi", il mio consiglio è questo: imposta ogni puntatore che dichiari su NULL o su un indirizzo valido immediatamente dopo la creazione e impostalo su NULL immediatamente dopo aver liberato la memoria a cui punta. Se sei diligente in questa pratica, verificare la presenza di NULL è tutto ciò di cui hai bisogno.


Una costante puntatore nullo in C ++ (o C, se è per questo), è rappresentata da uno zero integrale costante. Molte implementazioni usano tutti gli zeri binari per rappresentarlo, ma non è qualcosa su cui contare.
David Thornley,

2

Non esiste un modo portatile per farlo e farlo per piattaforme specifiche può essere ovunque tra difficile e impossibile. In ogni caso, non dovresti mai scrivere codice che dipende da tale controllo - non lasciare che i puntatori assumano valori non validi in primo luogo.


2

Impostare il puntatore su NULL prima e dopo l'uso è una buona tecnica. Questo è facile da fare in C ++ se gestisci i puntatori all'interno di una classe, ad esempio (una stringa):

class SomeClass
{
public:
    SomeClass();
    ~SomeClass();

    void SetText( const char *text);
    char *GetText() const { return MyText; }
    void Clear();

private:
    char * MyText;
};


SomeClass::SomeClass()
{
    MyText = NULL;
}


SomeClass::~SomeClass()
{
    Clear();
}

void SomeClass::Clear()
{
    if (MyText)
        free( MyText);

    MyText = NULL;
}



void SomeClass::Settext( const char *text)
{
    Clear();

    MyText = malloc( strlen(text));

    if (MyText)
        strcpy( MyText, text);
}

La domanda aggiornata rende la mia risposta sbagliata, ovviamente (o almeno una risposta a un'altra domanda). Sono d'accordo con le risposte che fondamentalmente dicono, lasciali andare in crash se abusano dell'API. Non puoi impedire alle persone di colpire se stesse nel pollice con un martello ...
Tim Ring

2

Non è una buona politica accettare puntatori arbitrari come parametri di input in un'API pubblica. È meglio avere tipi di "dati semplici" come un intero, una stringa o una struttura (intendo una struttura classica con dati semplici all'interno, ovviamente; ufficialmente qualsiasi cosa può essere una struttura).

Perché? Bene, perché come dicono altri non esiste un modo standard per sapere se ti è stato dato un puntatore valido o uno che punta alla spazzatura.

Ma a volte non hai scelta: la tua API deve accettare un puntatore.

In questi casi, è dovere del chiamante passare un buon puntatore. NULL può essere accettato come valore, ma non un puntatore a spazzatura.

Puoi ricontrollare in qualche modo? Bene, quello che ho fatto in un caso del genere è stato definire un invariante per il tipo a cui punta il puntatore e chiamarlo quando lo ottieni (in modalità debug). Almeno se l'invariante fallisce (o va in crash) sai che ti è stato passato un valore sbagliato.

// API that does not allow NULL
void PublicApiFunction1(Person* in_person)
{
  assert(in_person != NULL);
  assert(in_person->Invariant());

  // Actual code...
}

// API that allows NULL
void PublicApiFunction2(Person* in_person)
{
  assert(in_person == NULL || in_person->Invariant());

  // Actual code (must keep in mind that in_person may be NULL)
}

re: "passare un semplice tipo di dati ... come una stringa" Ma in C ++ le stringhe vengono spesso passate come puntatori a caratteri, (char *) o (const char *), quindi si torna a passare i puntatori. E il tuo esempio passa in_person come riferimento, non come puntatore, quindi il confronto (in_person! = NULL) implica che ci sono alcuni confronti oggetto / puntatore definiti nella classe Person.
Jesse Chisholm

@JesseChisholm Per stringa intendevo una stringa, cioè uno std :: string. In nessun modo sto raccomandando di usare char * come un modo per memorizzare stringhe o passarle in giro. Non farlo.
Daniel Daranas

@JesseChisholm Per qualche motivo, ho commesso un errore quando ho risposto a questa domanda cinque anni fa. Chiaramente, non ha senso controllare se una persona & è NULL. Non verrebbe nemmeno compilato. Avevo intenzione di usare puntatori, non riferimenti. L'ho risolto ora.
Daniel Daranas

1

Come altri hanno già detto, non è possibile rilevare in modo affidabile un puntatore non valido. Considera alcune delle forme che un puntatore non valido potrebbe assumere:

Potresti avere un puntatore nullo. Questo è quello che potresti facilmente controllare e fare qualcosa.

Potresti avere un puntatore a un punto al di fuori della memoria valida. Ciò che costituisce una memoria valida varia a seconda di come l'ambiente di runtime del sistema imposta lo spazio degli indirizzi. Sui sistemi Unix, di solito è uno spazio di indirizzi virtuali che inizia da 0 e arriva a un numero elevato di megabyte. Su sistemi embedded, potrebbe essere piuttosto piccolo. In ogni caso, potrebbe non iniziare da 0. Se la tua app è in esecuzione in modalità supervisore o equivalente, il tuo puntatore potrebbe fare riferimento a un indirizzo reale, che può essere o meno sottoposto a backup con memoria reale.

Potresti avere un puntatore da qualche parte all'interno della tua memoria valida, anche all'interno del tuo segmento di dati, bss, stack o heap, ma non che punta a un oggetto valido. Una variante di questo è un puntatore che puntava a un oggetto valido, prima che accadesse qualcosa di brutto all'oggetto. Le cose cattive in questo contesto includono la deallocazione, il danneggiamento della memoria o il danneggiamento del puntatore.

Potresti avere un puntatore illegale flat-out, come un puntatore con allineamento illegale per l'oggetto a cui si fa riferimento.

Il problema peggiora se si considerano architetture basate su segmenti / offset e altre strane implementazioni di puntatori. Questo genere di cose è normalmente nascosto allo sviluppatore da buoni compilatori e un uso giudizioso dei tipi, ma se vuoi bucare il velo e provare a superare in astuzia il sistema operativo e gli sviluppatori di compilatori, beh, puoi, ma non c'è un modo generico per farlo, gestirà tutti i problemi che potresti incontrare.

La cosa migliore che puoi fare è consentire l'arresto anomalo e fornire alcune buone informazioni diagnostiche.


ri: "fornire alcune buone informazioni diagnostiche", ecco il problema. Dal momento che non puoi verificare la validità del puntatore, le informazioni su cui devi agitarti sono minime. "Un'eccezione è avvenuta qui", potrebbe essere tutto ciò che ottieni. L'intero stack di chiamate è carino, ma richiede un framework migliore rispetto a quello fornito dalla maggior parte delle librerie di runtime C ++.
Jesse Chisholm


1

In generale, è impossibile. Ecco un caso particolarmente brutto:

struct Point2d {
    int x;
    int y;
};

struct Point3d {
    int x;
    int y;
    int z;
};

void dump(Point3 *p)
{
    printf("[%d %d %d]\n", p->x, p->y, p->z);
}

Point2d points[2] = { {0, 1}, {2, 3} };
Point3d *p3 = reinterpret_cast<Point3d *>(&points[0]);
dump(p3);

Su molte piattaforme, verrà stampato:

[0 1 2]

Stai costringendo il sistema runtime a interpretare in modo errato i bit di memoria, ma in questo caso non andrà in crash, perché tutti i bit hanno un senso. Questo è parte del progetto del linguaggio (sguardo al polimorfismo in stile C con struct inaddr, inaddr_in, inaddr_in6), quindi non è possibile proteggere in modo affidabile contro di essa su qualsiasi piattaforma.


1

È incredibile quante informazioni fuorvianti puoi leggere negli articoli sopra ...

E anche nella documentazione di microsoft msdn si afferma che IsBadPtr è stato bandito. Vabbè, preferisco l'applicazione funzionante piuttosto che il crash. Anche se il termine di lavoro potrebbe non funzionare correttamente (a condizione che l'utente finale possa continuare con l'applicazione).

Cercando su Google non ho trovato alcun esempio utile per Windows: ho trovato una soluzione per app a 32 bit,

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver%2FsrobtF2Fsrcid2Fsrcid2Frpidtc&hl=it = 2

ma devo anche supportare le app a 64 bit, quindi questa soluzione non ha funzionato per me.

Ma ho raccolto i codici sorgente del vino e sono riuscito a cucinare un tipo simile di codice che funzionerebbe anche per le app a 64 bit, allegando il codice qui:

#include <typeinfo.h>   

typedef void (*v_table_ptr)();   

typedef struct _cpp_object   
{   
    v_table_ptr*    vtable;   
} cpp_object;   



#ifndef _WIN64
typedef struct _rtti_object_locator
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    const type_info *type_descriptor;
    //const rtti_object_hierarchy *type_hierarchy;
} rtti_object_locator;
#else

typedef struct
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    unsigned int type_descriptor;
    unsigned int type_hierarchy;
    unsigned int object_locator;
} rtti_object_locator;  

#endif

/* Get type info from an object (internal) */  
static const rtti_object_locator* RTTI_GetObjectLocator(void* inptr)  
{   
    cpp_object* cppobj = (cpp_object*) inptr;  
    const rtti_object_locator* obj_locator = 0;   

    if (!IsBadReadPtr(cppobj, sizeof(void*)) &&   
        !IsBadReadPtr(cppobj->vtable - 1, sizeof(void*)) &&   
        !IsBadReadPtr((void*)cppobj->vtable[-1], sizeof(rtti_object_locator)))  
    {  
        obj_locator = (rtti_object_locator*) cppobj->vtable[-1];  
    }  

    return obj_locator;  
}  

E il codice seguente può rilevare se il puntatore è valido o meno, probabilmente è necessario aggiungere un controllo NULL:

    CTest* t = new CTest();
    //t = (CTest*) 0;
    //t = (CTest*) 0x12345678;

    const rtti_object_locator* ptr = RTTI_GetObjectLocator(t);  

#ifdef _WIN64
    char *base = ptr->signature == 0 ? (char*)RtlPcToFileHeader((void*)ptr, (void**)&base) : (char*)ptr - ptr->object_locator;
    const type_info *td = (const type_info*)(base + ptr->type_descriptor);
#else
    const type_info *td = ptr->type_descriptor;
#endif
    const char* n =td->name();

Questo ottiene il nome della classe dal puntatore: penso che dovrebbe essere sufficiente per le tue esigenze.

Una cosa di cui ho ancora paura sono le prestazioni del controllo del puntatore - nel codice cecchino sopra ci sono già 3-4 chiamate API in corso - potrebbe essere eccessivo per applicazioni critiche in termini di tempo.

Sarebbe utile se qualcuno potesse misurare il sovraccarico del controllo del puntatore rispetto, ad esempio, alle chiamate C # / c ++ gestite.


1

In effetti, qualcosa potrebbe essere fatto in un'occasione specifica: per esempio se vuoi controllare se una stringa puntatore a stringa è valida, usare write (fd, buf, szie) syscall può aiutarti a fare la magia: lascia che fd sia un descrittore di file temporaneo file che crei per test, e buf che punta alla stringa che stai testando, se il puntatore non è valido write () restituirebbe -1 e errno impostato su EFAULT che indica che buf è al di fuori del tuo spazio di indirizzi accessibile.


1

Quanto segue funziona in Windows (qualcuno l'ha suggerito prima):

 static void copy(void * target, const void* source, int size)
 {
     __try
     {
         CopyMemory(target, source, size);
     }
     __except(EXCEPTION_EXECUTE_HANDLER)
     {
         doSomething(--whatever--);
     }
 }

La funzione deve essere un metodo statico, autonomo o statico di qualche classe. Per eseguire il test in sola lettura, copiare i dati nel buffer locale. Per testare la scrittura senza modificare i contenuti, riscrivili. Puoi testare solo il primo / ultimo indirizzo. Se il puntatore non è valido, il controllo verrà passato a "doSomething" e quindi all'esterno delle parentesi. Basta non usare nulla che richieda distruttori, come CString.


1

Su Windows utilizzo questo codice:

void * G_pPointer = NULL;
const char * G_szPointerName = NULL;
void CheckPointerIternal()
{
    char cTest = *((char *)G_pPointer);
}
bool CheckPointerIternalExt()
{
    bool bRet = false;

    __try
    {
        CheckPointerIternal();
        bRet = true;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

    return  bRet;
}
void CheckPointer(void * A_pPointer, const char * A_szPointerName)
{
    G_pPointer = A_pPointer;
    G_szPointerName = A_szPointerName;
    if (!CheckPointerIternalExt())
        throw std::runtime_error("Invalid pointer " + std::string(G_szPointerName) + "!");
}

Utilizzo:

unsigned long * pTest = (unsigned long *) 0x12345;
CheckPointer(pTest, "pTest"); //throws exception


0

Ho visto varie librerie utilizzare un metodo per verificare la presenza di memoria non referenziata e simili. Credo che semplicemente "sovrascrivono" i metodi di allocazione e deallocazione della memoria (malloc / free), che ha una logica che tiene traccia dei puntatori. Suppongo che questo sia eccessivo per il tuo caso d'uso, ma sarebbe un modo per farlo.


Questo non aiuta per gli oggetti allocati nello stack, sfortunatamente.
Tom

0

Tecnicamente puoi sovrascrivere l'operatore new (ed eliminare ) e raccogliere informazioni su tutta la memoria allocata, in modo da avere un metodo per verificare se la memoria heap è valida. ma:

  1. hai ancora bisogno di un modo per verificare se il puntatore è allocato sullo stack ()

  2. dovrai definire cosa è il puntatore 'valido':

a) viene allocata la memoria su quell'indirizzo

b) la memoria a quell'indirizzo è l'indirizzo iniziale dell'oggetto (ad es. indirizzo non nel mezzo di un array enorme)

c) la memoria a quell'indirizzo è l' indirizzo iniziale dell'oggetto del tipo previsto

Conclusione : l'approccio in questione non è in C ++, è necessario definire alcune regole che assicurino che la funzione riceva puntatori validi.



0

Addendum alla / e risposta / e accettata / e:

Supponiamo che il tuo puntatore possa contenere solo tre valori: 0, 1 e -1 dove 1 indica un puntatore valido, -1 uno non valido e 0 un altro non valido. Qual è la probabilità che il tuo puntatore sia NULL, tutti i valori sono ugualmente probabili? 1/3. Ora, estrai il caso valido, quindi per ogni caso non valido, hai un rapporto 50:50 per rilevare tutti gli errori. Sembra buono vero? Scala questo per un puntatore a 4 byte. Sono disponibili 2 ^ 32 o 4294967294 valori. Di questi, solo UN valore è corretto, uno è NULL e ti rimangono altri 4294967292 casi non validi. Ricalcola: hai un test per 1 su (4294967292+ 1) casi non validi. Una probabilità di 2.xe-10 o 0 per la maggior parte degli scopi pratici. Questa è l'inutilità del controllo NULL.


0

Sai, un nuovo driver (almeno su Linux) in grado di farlo probabilmente non sarebbe così difficile da scrivere.

D'altra parte, sarebbe una follia costruire i tuoi programmi in questo modo. A meno che tu non abbia un uso davvero specifico e singolo per una cosa del genere, non lo consiglierei. Se hai creato un'applicazione di grandi dimensioni caricata con controlli di validità del puntatore costanti, probabilmente sarebbe terribilmente lenta.


0

dovresti evitare questi metodi perché non funzionano. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15 febbraio 2009 alle 16:02

Se non funzionano, il prossimo aggiornamento di Windows lo risolverà? Se non funzionano a livello di concetto, la funzione verrà probabilmente rimossa completamente dall'API di Windows.

La documentazione MSDN afferma che sono vietati e la ragione di ciò è probabilmente un difetto di ulteriore progettazione dell'applicazione (ad esempio, in genere non dovresti mangiare silenziosamente i puntatori non validi, se sei responsabile della progettazione dell'intera applicazione ovviamente) e prestazioni / tempo di controllo del puntatore.

Ma non dovresti affermare che non funzionano a causa di alcuni blog. Nella mia applicazione di prova ho verificato che funzionino.


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.