Testen von Zeigern auf Gültigkeit (C / C ++)


90

Gibt es eine Möglichkeit (natürlich programmatisch) festzustellen, ob ein bestimmter Zeiger "gültig" ist? Das Überprüfen auf NULL ist einfach, aber was ist mit Dingen wie 0x00001234? Beim Versuch, diese Art von Zeiger zu dereferenzieren, tritt eine Ausnahme / ein Absturz auf.

Eine plattformübergreifende Methode wird bevorzugt, aber auch plattformspezifisch (für Windows und Linux) ist in Ordnung.

Update zur Verdeutlichung: Das Problem liegt nicht bei veralteten / freigegebenen / nicht initialisierten Zeigern. Stattdessen implementiere ich eine API, die Zeiger vom Aufrufer übernimmt (wie einen Zeiger auf eine Zeichenfolge, ein Dateihandle usw.). Der Anrufer kann (absichtlich oder versehentlich) einen ungültigen Wert als Zeiger senden. Wie verhindere ich einen Absturz?



Ich denke, die beste positive Antwort für Linux gibt George Carrette. Wenn dies nicht ausreicht, sollten Sie die Funktionssymboltabelle in die Bibliothek oder sogar eine andere Tabellenebene verfügbarer Bibliotheken mit eigenen Funktionstabellen einbauen. Überprüfen Sie dann anhand dieser genauen Tabellen. Natürlich sind diese negativen Antworten auch richtig: Sie können nicht 100% sicher sein, ob ein Funktionszeiger gültig ist oder nicht, es sei denn, Sie haben der Benutzeranwendung viele zusätzliche Einschränkungen auferlegt.
Minghua

Gibt die API-Spezifikation tatsächlich eine solche Verpflichtung an, die von der Implementierung erfüllt werden muss? Ich gebe übrigens vor, nicht angenommen worden zu sein, dass Sie sowohl Entwickler als auch Designer sind. Mein Punkt ist, ich glaube nicht, dass eine API so etwas wie "Wenn ein ungültiger Zeiger als Argument übergeben wird, muss die Funktion das Problem behandeln und NULL zurückgeben." Eine API verpflichtet sich, einen Dienst unter angemessenen Nutzungsbedingungen und nicht durch Hacks bereitzustellen. Trotzdem schadet es nicht, ein bisschen dumm zu sein. Die Verwendung einer Referenz führt dazu, dass sich solche Fälle weniger verheerend ausbreiten. :)
Poniros

Antworten:


75

Update zur Verdeutlichung: Das Problem liegt nicht bei veralteten, freigegebenen oder nicht initialisierten Zeigern. Stattdessen implementiere ich eine API, die Zeiger vom Aufrufer übernimmt (wie einen Zeiger auf eine Zeichenfolge, ein Dateihandle usw.). Der Anrufer kann (absichtlich oder versehentlich) einen ungültigen Wert als Zeiger senden. Wie verhindere ich einen Absturz?

Sie können diese Prüfung nicht durchführen. Sie können einfach nicht überprüfen, ob ein Zeiger "gültig" ist. Sie müssen darauf vertrauen, dass Menschen, die eine Funktion verwenden, die einen Zeiger verwendet, wissen, was sie tun. Wenn sie Ihnen 0x4211 als Zeigerwert übergeben, müssen Sie darauf vertrauen, dass er auf die Adresse 0x4211 zeigt. Und wenn sie "versehentlich" auf ein Objekt treffen, würden Sie selbst dann, wenn Sie eine beängstigende Betriebssystemfunktion (IsValidPtr oder was auch immer) verwenden würden, in einen Fehler geraten und nicht schnell ausfallen.

Verwenden Sie Nullzeiger, um diese Art von Dingen zu signalisieren, und teilen Sie dem Benutzer Ihrer Bibliothek mit, dass er keine Zeiger verwenden soll, wenn er dazu neigt, versehentlich ungültige Zeiger zu übergeben, ernsthaft :)


Dies ist wahrscheinlich die richtige Antwort, aber ich denke, eine einfache Funktion, die gängige Hexspeak-Speicherorte überprüft, wäre nützlich für das allgemeine Debuggen ... Im Moment habe ich einen Zeiger, der manchmal auf 0xfeeefeee zeigt, und wenn ich eine einfache Funktion hätte, die ich könnte Verwenden Sie, um Behauptungen zu pfeffern Es würde es viel einfacher machen, den Schuldigen zu finden ... BEARBEITEN: Obwohl es nicht schwer wäre, selbst einen zu schreiben, denke ich ..
Quant

@quant das Problem ist, dass einige C- und C ++ - Codes Zeigerarithmetik für eine ungültige Adresse ausführen können, ohne dies zu überprüfen (basierend auf dem Grundsatz "Garbage-In", "Garbage-Out") und somit einen "arithmetisch modifizierten" Zeiger von einem dieser Brunnen übergeben -bekannte ungültige Adressen. Häufige Fälle sind das Nachschlagen einer Methode aus einer nicht vorhandenen vtable basierend auf einer ungültigen Objektadresse oder einer falschen Typadresse oder das einfache Lesen von Feldern von einem Zeiger auf eine Struktur, die nicht auf eine zeigt.
Rwong

Dies bedeutet im Grunde, dass Sie nur Array-Indizes von der Außenwelt nehmen können. Eine API, die sich gegen den Aufrufer verteidigen muss, kann keine Zeiger in der Schnittstelle haben. Es wäre jedoch immer noch gut, Makros zu haben, die in Aussagen über die Gültigkeit von Zeigern verwendet werden können (die Sie intern haben müssen). Wenn garantiert wird, dass ein Zeiger innerhalb eines Arrays zeigt, dessen Startpunkt und Länge bekannt sind, kann dies explizit überprüft werden. Es ist besser, an einem Assert-Verstoß (dokumentierter Fehler) zu sterben als an einem Deref (undokumentierter Fehler).
Rob

34

Hier sind drei einfache Möglichkeiten für ein C-Programm unter Linux, sich einen Überblick über den Status des Speichers zu verschaffen, in dem es ausgeführt wird, und warum die Frage in einigen Kontexten angemessene Antworten bietet.

  1. Nachdem Sie getpagesize () aufgerufen und den Zeiger auf eine Seitengrenze gerundet haben, können Sie mincore () aufrufen, um herauszufinden, ob eine Seite gültig ist und ob sie Teil des Prozessarbeitssatzes ist. Beachten Sie, dass hierfür einige Kernelressourcen erforderlich sind. Sie sollten diese daher vergleichen und feststellen, ob der Aufruf dieser Funktion in Ihrer API wirklich angemessen ist. Wenn Ihre API Interrupts verarbeitet oder von seriellen Schnittstellen in den Speicher liest, sollten Sie dies aufrufen, um unvorhersehbare Verhaltensweisen zu vermeiden.
  2. Nachdem Sie stat () aufgerufen haben, um festzustellen, ob ein Verzeichnis / proc / self verfügbar ist, können Sie / proc / self / maps öffnen und lesen, um Informationen zu der Region zu finden, in der sich ein Zeiger befindet. Studieren Sie die Manpage für proc, das Pseudo-Dateisystem für Prozessinformationen. Dies ist natürlich relativ teuer, aber Sie können möglicherweise das Ergebnis der Analyse in einem Array zwischenspeichern, das Sie mithilfe einer binären Suche effizient nachschlagen können. Berücksichtigen Sie auch / proc / self / smaps. Wenn Ihre API für Hochleistungsrechnen vorgesehen ist, möchte das Programm Informationen zu / proc / self / numa erhalten, das unter der Manpage für numa, der ungleichmäßigen Speicherarchitektur, dokumentiert ist.
  3. Der Aufruf von get_mempolicy (MPOL_F_ADDR) eignet sich für API-Arbeiten mit hoher Leistung, bei denen mehrere Ausführungsthreads vorhanden sind und Sie Ihre Arbeit so verwalten, dass sie eine Affinität für ungleichmäßigen Speicher in Bezug auf die CPU-Kerne und Socket-Ressourcen aufweist. Eine solche API sagt Ihnen natürlich auch, ob ein Zeiger gültig ist.

Unter Microsoft Windows gibt es die Funktion QueryWorkingSetEx, die unter der Prozessstatus-API (auch in der NUMA-API) dokumentiert ist. Als Folge einer ausgeklügelten NUMA-API-Programmierung können Sie mit dieser Funktion auch einfache "Testzeiger auf Gültigkeit (C / C ++)" ausführen, da es unwahrscheinlich ist, dass sie für mindestens 15 Jahre veraltet sind.


12
Erste Antwort, die nicht versucht, moralisch über die Frage selbst zu sein und sie tatsächlich perfekt beantwortet. Menschen merken manchmal nicht, dass man diese Art von Debugging-Ansatz wirklich benötigt, um Fehler in Bibliotheken von Drittanbietern oder in Legacy-Code zu finden, da selbst valgrind nur wilde Zeiger findet, wenn tatsächlich darauf zugegriffen wird, und nicht, wenn Sie Zeiger regelmäßig auf ihre Gültigkeit überprüfen möchten in einer Cache-Tabelle, die von einer anderen Stelle in Ihrem Code überschrieben wurden ...
lumpidu

Dies sollte die akzeptierte Antwort sein. Ich habe es ähnlich auf einer Nicht-Linux-Plattform gemacht. Grundsätzlich werden die Prozessinformationen dem Prozess selbst ausgesetzt. Unter diesem Aspekt sieht es so aus, als ob Windows einen besseren Job als Linux macht, indem es die aussagekräftigeren Informationen über die Prozessstatus-API verfügbar macht.
Minghua

31

Das Verhindern eines Absturzes, der dadurch verursacht wird, dass der Anrufer einen ungültigen Zeiger sendet, ist eine gute Möglichkeit, stille Fehler zu machen, die schwer zu finden sind.

Ist es nicht besser für den Programmierer, der Ihre API verwendet, eine klare Nachricht zu erhalten, dass sein Code falsch ist, indem er ihn zum Absturz bringt, anstatt ihn auszublenden?


8
Obwohl in einigen Fällen unmittelbar für einen schlechten Zeiger überprüft , wenn die API aufgerufen werden , ist , wie Sie früh scheitern. Was ist beispielsweise, wenn die API den Zeiger in einer Datenstruktur speichert, in der er erst später zurückgestellt wird? Wenn Sie dann die API übergeben, führt ein fehlerhafter Zeiger zu einem späteren Zeitpunkt zu einem Absturz. In diesem Fall ist es besser, beim API-Aufruf, bei dem der fehlerhafte Wert ursprünglich eingeführt wurde, früher zu scheitern.
Peterflynn

28

Unter Win32 / 64 gibt es eine Möglichkeit, dies zu tun. Versuchen Sie, den Zeiger zu lesen und die resultierende SEH-Ausnahme abzufangen, die bei einem Fehler ausgelöst wird. Wenn es nicht wirft, ist es ein gültiger Zeiger.

Das Problem bei dieser Methode ist jedoch, dass nur zurückgegeben wird, ob Sie Daten vom Zeiger lesen können oder nicht. Es gibt keine Garantie für die Typensicherheit oder eine beliebige Anzahl anderer Invarianten. Im Allgemeinen ist diese Methode für nichts anderes gut, als zu sagen: "Ja, ich kann diesen bestimmten Ort im Speicher zu einem Zeitpunkt lesen, der jetzt vergangen ist."

Kurz gesagt, tu das nicht;)

Raymond Chen hat einen Blog-Beitrag zu diesem Thema: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


3
@ Tim, das gibt es in C ++ nicht.
JaredPar

6
Dies ist nur dann die "richtige Antwort", wenn Sie "gültiger Zeiger" als "verursacht keine Zugriffsverletzung / Segfault" definieren. Ich würde es vorziehen, es als "Punkte auf aussagekräftige Daten, die für den Zweck zugewiesen sind, den Sie verwenden möchten" zu definieren. Ich würde behaupten, dass dies eine bessere Definition der
Zeigergültigkeit ist

Auch wenn der Zeiger gültig ist, kann dies nicht überprüft werden. Denken Sie an thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {sleep (1); lösche p; ...}
Christopher

2
@ Christopher, sehr wahr. Ich hätte sagen sollen "Ich kann diesen bestimmten Ort im Gedächtnis zu einem Zeitpunkt lesen, der jetzt vergangen ist"
JaredPar

@ JaredPar: Wirklich schlechter Vorschlag. Kann eine Schutzseite auslösen, so dass der Stapel später nicht erweitert wird oder etwas ähnlich Schönes.
Deduplikator

16

AFAIK gibt es keinen Weg. Sie sollten versuchen, diese Situation zu vermeiden, indem Sie nach dem Freigeben des Speichers immer die Zeiger auf NULL setzen.


4
Wenn Sie einen Zeiger auf null setzen, erhalten Sie nichts, außer möglicherweise ein falsches Sicherheitsgefühl.

Das ist nicht wahr. Insbesondere in C ++ können Sie festlegen, ob Elementobjekte gelöscht werden sollen, indem Sie nach null suchen. Beachten Sie auch, dass es in C ++ gültig ist, Nullzeiger zu löschen. Daher ist das bedingungslose Löschen von Objekten in Destruktoren beliebt.
Ferdinand Beyer

4
int * p = new int (0); int * p2 = p; lösche p; p = NULL; p2 löschen; // Absturz

1
zabzonk und ?? Was er sagte ist, dass Sie einen Nullzeiger löschen können. p2 ist kein Nullzeiger, sondern ein ungültiger Zeiger. Sie müssen es vorher auf null setzen.
Johannes Schaub - litb

2
Wenn Sie Aliase im Speicher haben, auf die verwiesen wird, wird nur einer von ihnen auf NULL gesetzt, andere Aliase baumeln herum.
Jdehaan


7

In Bezug auf die Antwort etwas weiter oben in diesem Thread:

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () für Windows.

Mein Rat ist, sich von ihnen fernzuhalten, jemand hat diesen bereits gepostet: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

Ein weiterer Beitrag zum selben Thema und vom selben Autor (glaube ich) ist dieser: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ("IsBadXxxPtr sollte eigentlich CrashProgramRandomly heißen ").

Wenn die Benutzer Ihrer API fehlerhafte Daten senden, lassen Sie diese abstürzen. Wenn das Problem darin besteht, dass die übergebenen Daten erst später verwendet werden (und dies das Auffinden der Ursache erschwert), fügen Sie einen Debug-Modus hinzu, in dem die Zeichenfolgen usw. bei der Eingabe protokolliert werden. Wenn sie schlecht sind, wird es offensichtlich sein (und wahrscheinlich abstürzen). Wenn es viel zu oft vorkommt, lohnt es sich möglicherweise, Ihre API aus dem Prozess zu entfernen und den API-Prozess anstelle des Hauptprozesses zum Absturz zu bringen.


Wahrscheinlich besteht eine andere Möglichkeit darin, _CrtIsValidHeapPointer zu verwenden . Diese Funktion gibt TRUE zurück, wenn der Zeiger gültig ist, und löst eine Ausnahme aus, wenn der Zeiger freigegeben wird. Wie dokumentiert, ist diese Funktion nur in Debug-CRT verfügbar.
Crend King

6

Erstens sehe ich keinen Grund darin, mich vor dem Anrufer zu schützen, der absichtlich versucht, einen Absturz zu verursachen. Sie könnten dies leicht tun, indem sie versuchen, über einen ungültigen Zeiger selbst darauf zuzugreifen. Es gibt viele andere Möglichkeiten - sie könnten einfach Ihren Speicher oder den Stapel überschreiben. Wenn Sie sich vor solchen Dingen schützen müssen, müssen Sie in einem separaten Prozess mit Sockets oder einem anderen IPC für die Kommunikation ausgeführt werden.

Wir schreiben eine Menge Software, mit der Partner / Kunden / Benutzer die Funktionalität erweitern können. Unweigerlich wird jeder Fehler zuerst an uns gemeldet, daher ist es hilfreich, leicht nachweisen zu können, dass das Problem im Plug-In-Code liegt. Darüber hinaus gibt es Sicherheitsbedenken und einige Benutzer sind vertrauenswürdiger als andere.

Wir verwenden verschiedene Methoden, abhängig von den Leistungs- / Durchsatzanforderungen und der Vertrauenswürdigkeit. Von den am meisten bevorzugten:

  • separate Prozesse mithilfe von Sockets (häufig Daten als Text übergeben).

  • separate Prozesse unter Verwendung des gemeinsam genutzten Speichers (wenn große Datenmengen übertragen werden sollen).

  • Gleicher Prozess trennt Threads über die Nachrichtenwarteschlange (wenn häufige Kurznachrichten vorhanden sind).

  • Im selben Prozess werden alle übergebenen Daten aus einem Speicherpool getrennt.

  • Gleicher Prozess über direkten Prozeduraufruf - alle übergebenen Daten aus einem Speicherpool.

Wir versuchen, niemals auf das zurückzugreifen, was Sie beim Umgang mit Software von Drittanbietern versuchen - insbesondere, wenn wir die Plug-Ins / Bibliothek als Binär- und nicht als Quellcode erhalten.

Die Verwendung eines Speicherpools ist in den meisten Fällen recht einfach und muss nicht ineffizient sein. Wenn SIE die Daten zuerst zuweisen, ist es trivial, die Zeiger mit den von Ihnen zugewiesenen Werten zu vergleichen. Sie können auch die zugewiesene Länge speichern und vor und nach den Daten "magische" Werte hinzufügen, um nach gültigen Datentypen und Datenüberschreitungen zu suchen.


4

Ich habe viel Verständnis für Ihre Frage, da ich selbst in einer fast identischen Position bin. Ich schätze , was viele der Antworten sagen, und sie sind richtig - die Routine der Zeiger liefern soll einen gültigen Zeiger weitergeben. In meinem Fall ist es fast unvorstellbar, dass sie den Zeiger beschädigt haben könnten - aber wenn sie es getan hätten geschafft hätten, wäre es MEINE Software, die abstürzt, und ME, die die Schuld bekommen würde :-(

Meine Anforderung ist nicht, dass ich nach einem Segmentierungsfehler fortfahre - das wäre gefährlich - ich möchte nur dem Kunden melden, was vor der Kündigung passiert ist, damit er seinen Code reparieren kann, anstatt mich zu beschuldigen!

So habe ich es gefunden (unter Windows): http://www.cplusplus.com/reference/clibrary/csignal/signal/

Um eine Zusammenfassung zu geben:

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

Nun scheint sich dies so zu verhalten, wie ich es mir erhoffen würde (es gibt die Fehlermeldung aus und beendet dann das Programm) - aber wenn jemand einen Fehler entdecken kann, lassen Sie es mich bitte wissen!


Nicht verwenden exit(), es umgeht RAII und kann daher zu Ressourcenlecks führen.
Sebastian Mach

Interessant - gibt es eine andere Möglichkeit, in dieser Situation ordentlich zu beenden? Und ist die Exit-Anweisung das einzige Problem dabei? Ich stelle fest, dass ich ein "-1" erhalten habe - liegt das nur am "Ausgang"?
Mike Sadler

Hoppla, mir ist klar, dass dies eine ziemlich außergewöhnliche Situation ist. Ich habe es gerade gesehen exit()und meine tragbare C ++ - Alarmglocke begann zu läuten. Es sollte in dieser Linux-spezifischen Situation in Ordnung sein, in der Ihr Programm sowieso beendet wird, entschuldigen Sie das Rauschen.
Sebastian Mach

1
Signal (2) ist nicht tragbar. Verwenden Sie Sigaction (2). man 2 signalUnter Linux gibt es einen Absatz, in dem erklärt wird, warum.
rptb1

1
In dieser Situation würde ich normalerweise eher abbrechen (3) als beenden (3) nennen, da es wahrscheinlicher ist, dass eine Art Debugging-Backtrace erzeugt wird, mit der Sie das Problem post mortem diagnostizieren können. Unter den meisten Unixen wird abort (3) Core sichern (wenn Core-Dumps zulässig sind) und unter Windows wird angeboten, einen Debugger zu starten, falls installiert.
rptb1

4

Unter Unix sollten Sie in der Lage sein, einen Kernel-Systemaufruf zu verwenden, der Zeigerprüfungen durchführt und EFAULT zurückgibt, z.

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

Rückkehr:

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

Es gibt wahrscheinlich einen besseren Systemaufruf als open () [möglicherweise Zugriff], da die Möglichkeit besteht, dass dies zu einem tatsächlichen Codepfad für die Dateierstellung und einer anschließenden Anforderung zum Schließen führt.


Dies ist ein brillanter Hack. Ich würde gerne Ratschläge zu verschiedenen Systemaufrufen zur Validierung von Speicherbereichen erhalten, insbesondere wenn garantiert werden kann, dass sie keine Nebenwirkungen haben. Sie können einen Dateideskriptor offen halten, um in / dev / null zu schreiben und zu testen, ob sich Puffer im lesbaren Speicher befinden. Es gibt jedoch wahrscheinlich einfachere Lösungen. Das Beste, was ich finden kann, ist symlink (ptr, ""), der errno bei einer schlechten Adresse auf 14 oder bei einer guten Adresse auf 2 setzt, aber Kerneländerungen können die Reihenfolge der Überprüfung ändern.
Preston

@Preston In DB2 haben wir wahrscheinlich den Zugriff von unistd.h () verwendet. Ich habe oben open () verwendet, weil es etwas weniger dunkel ist, aber Sie haben wahrscheinlich Recht, dass es viele mögliche Syscalls gibt, die verwendet werden können. Windows hatte früher eine explizite Zeigerprüfungs-API, die sich jedoch als nicht threadsicher herausstellte (ich glaube, sie hat SEH verwendet, um zu versuchen, die Grenzen des Speicherbereichs zu schreiben und dann wiederherzustellen).
Peeter Joot

2

In C ++ gibt es keine Bestimmungen zum Testen der Gültigkeit eines Zeigers als allgemeiner Fall. Man kann natürlich davon ausgehen, dass NULL (0x00000000) schlecht ist, und verschiedene Compiler und Bibliotheken verwenden hier und da gerne "spezielle Werte", um das Debuggen zu vereinfachen (zum Beispiel, wenn ich jemals einen Zeiger als 0xCECECECE in Visual Studio sehe, den ich kenne Ich habe etwas falsch gemacht), aber die Wahrheit ist, dass es nahezu unmöglich ist, anhand eines Zeigers zu erkennen, ob es sich um den "richtigen" Index handelt, da ein Zeiger nur ein Index im Speicher ist.

Es gibt verschiedene Tricks, die Sie mit dynamic_cast und RTTI ausführen können, um sicherzustellen, dass das Objekt, auf das verwiesen wird, vom gewünschten Typ ist. Sie alle erfordern jedoch, dass Sie auf etwas Gültiges verweisen.

Wenn Sie sicherstellen möchten, dass Ihr Programm "ungültige" Zeiger erkennen kann, lautet mein Rat: Setzen Sie jeden Zeiger, den Sie deklarieren, sofort nach der Erstellung auf NULL oder eine gültige Adresse und setzen Sie ihn unmittelbar nach dem Freigeben des Speichers, auf den er verweist, auf NULL. Wenn Sie diese Praxis sorgfältig anwenden, ist die Überprüfung auf NULL alles, was Sie jemals brauchen.


Eine Nullzeigerkonstante in C ++ (oder C) wird durch ein konstantes Integral Null dargestellt. Viele Implementierungen verwenden rein binäre Nullen, um sie darzustellen, aber es ist nichts, worauf man zählen kann.
David Thornley

2

Es gibt keine tragbare Möglichkeit, dies zu tun, und es kann für bestimmte Plattformen zwischen schwierig und unmöglich sein. In jedem Fall sollten Sie niemals Code schreiben, der von einer solchen Prüfung abhängt - lassen Sie die Zeiger überhaupt nicht ungültige Werte annehmen.


2

Das Setzen des Zeigers auf NULL vor und nach der Verwendung ist eine gute Technik. Dies ist in C ++ einfach, wenn Sie beispielsweise Zeiger innerhalb einer Klasse (einer Zeichenfolge) verwalten:

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

Die aktualisierte Frage macht meine Antwort natürlich falsch (oder zumindest eine Antwort auf eine andere Frage). Ich stimme den Antworten zu, die im Grunde sagen, lassen Sie sie abstürzen, wenn sie die API missbrauchen. Sie können nicht verhindern, dass sich Leute mit einem Hammer in den Daumen schlagen ...
Tim Ring

2

Es ist keine sehr gute Richtlinie, beliebige Zeiger als Eingabeparameter in einer öffentlichen API zu akzeptieren. Es ist besser, "einfache Datentypen" wie eine Ganzzahl, eine Zeichenfolge oder eine Struktur zu haben (ich meine natürlich eine klassische Struktur mit einfachen Daten darin; offiziell kann alles eine Struktur sein).

Warum? Nun, wie andere sagen, gibt es keine Standardmethode, um festzustellen, ob Sie einen gültigen Zeiger erhalten haben oder einen, der auf Junk verweist.

Aber manchmal haben Sie keine Wahl - Ihre API muss einen Zeiger akzeptieren.

In diesen Fällen ist es die Pflicht des Anrufers, einen guten Zeiger zu übergeben. NULL kann als Wert akzeptiert werden, aber kein Zeiger auf Junk.

Können Sie das in irgendeiner Weise überprüfen? In einem solchen Fall habe ich eine Invariante für den Typ definiert, auf den der Zeiger zeigt, und sie aufgerufen, wenn Sie sie erhalten (im Debug-Modus). Zumindest wenn die Invariante ausfällt (oder abstürzt), wissen Sie, dass Ihnen ein schlechter Wert übergeben wurde.

// 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)
}

Betreff: "Übergeben eines einfachen Datentyps ... wie eine Zeichenfolge" In C ++ werden Zeichenfolgen jedoch meistens als Zeiger auf Zeichen (char *) oder (const char *) übergeben, sodass Sie wieder Zeiger übergeben können. In Ihrem Beispiel wird in_person als Referenz und nicht als Zeiger übergeben. Der Vergleich (in_person! = NULL) impliziert also, dass in der Person-Klasse einige Objekt- / Zeiger-Vergleiche definiert sind.
Jesse Chisholm

@JesseChisholm Mit string habe ich einen String gemeint, dh einen std :: string. In keiner Weise empfehle ich, char * zu verwenden, um Zeichenfolgen zu speichern oder weiterzugeben. Tu das nicht.
Daniel Daranas

@JesseChisholm Aus irgendeinem Grund habe ich einen Fehler gemacht, als ich diese Frage vor fünf Jahren beantwortete. Es ist natürlich nicht sinnvoll zu überprüfen, ob eine Person & NULL ist. Das würde nicht einmal kompilieren. Ich wollte Zeiger verwenden, keine Referenzen. Ich habe es jetzt behoben.
Daniel Daranas

1

Wie andere gesagt haben, können Sie einen ungültigen Zeiger nicht zuverlässig erkennen. Betrachten Sie einige der Formen, die ein ungültiger Zeiger annehmen könnte:

Sie könnten einen Nullzeiger haben. Das ist eine, nach der Sie leicht suchen und etwas unternehmen können.

Sie könnten einen Zeiger auf etwas außerhalb des gültigen Speichers haben. Was einen gültigen Speicher ausmacht, hängt davon ab, wie die Laufzeitumgebung Ihres Systems den Adressraum einrichtet. Auf Unix-Systemen ist es normalerweise ein virtueller Adressraum, der bei 0 beginnt und eine große Anzahl von Megabyte erreicht. Auf eingebetteten Systemen kann es recht klein sein. Es könnte auf keinen Fall bei 0 beginnen. Wenn Ihre App zufällig im Supervisor-Modus oder einem gleichwertigen Modus ausgeführt wird, verweist Ihr Zeiger möglicherweise auf eine reale Adresse, die möglicherweise mit echtem Speicher gesichert ist oder nicht.

Sie könnten einen Zeiger auf irgendwo in Ihrem gültigen Speicher haben, sogar in Ihrem Datensegment, BSS, Stack oder Heap, aber nicht auf ein gültiges Objekt zeigen. Eine Variante davon ist ein Zeiger, der auf ein gültiges Objekt zeigte, bevor dem Objekt etwas Schlimmes passiert ist. Zu den schlechten Dingen in diesem Zusammenhang gehören Freigabe, Speicherbeschädigung oder Zeigerbeschädigung.

Möglicherweise haben Sie einen unzulässigen unzulässigen Zeiger, z. B. einen Zeiger mit unzulässiger Ausrichtung für das Objekt, auf das verwiesen wird.

Das Problem wird noch schlimmer, wenn Sie segment- / versatzbasierte Architekturen und andere ungerade Zeigerimplementierungen berücksichtigen. Diese Art von Dingen wird dem Entwickler normalerweise durch gute Compiler und den umsichtigen Einsatz von Typen verborgen. Wenn Sie jedoch den Schleier durchstoßen und versuchen möchten, das Betriebssystem und die Compiler-Entwickler zu überlisten, können Sie dies, aber es gibt keinen generischen Weg Um dies zu tun, werden alle Probleme behandelt, auf die Sie möglicherweise stoßen.

Das Beste, was Sie tun können, ist, den Absturz zuzulassen und einige gute Diagnoseinformationen herauszugeben.


Betreff: "Gute Diagnoseinformationen herausgeben", da ist das Problem. Da Sie die Gültigkeit des Zeigers nicht überprüfen können, sind die Informationen, um die Sie sich kümmern müssen, minimal. "Hier ist eine Ausnahme passiert", kann alles sein, was Sie bekommen. Der gesamte Aufrufstapel ist nett, erfordert jedoch ein besseres Framework als die meisten C ++ - Laufzeitbibliotheken.
Jesse Chisholm


1

Im Allgemeinen ist das unmöglich. Hier ist ein besonders schlimmer Fall:

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);

Auf vielen Plattformen wird dies ausgedruckt:

[0 1 2]

Sie zwingen das Laufzeitsystem, Speicherbits falsch zu interpretieren, aber in diesem Fall stürzt es nicht ab, da alle Bits sinnvoll sind. Dies ist Teil des Entwurfs der Sprache (siehe C-Stil - Polymorphismus mit struct inaddr, inaddr_in, inaddr_in6), so dass Sie nicht zuverlässig gegen sie auf jeder Plattform schützen.


1

Es ist unglaublich, wie viele irreführende Informationen Sie in den obigen Artikeln lesen können ...

Und selbst in der Microsoft MSDN-Dokumentation wird behauptet, IsBadPtr sei verboten. Na ja - ich arbeite lieber mit einer Anwendung als mit einem Absturz. Auch wenn das Arbeiten mit Begriffen möglicherweise nicht ordnungsgemäß funktioniert (solange der Endbenutzer mit der Anwendung fortfahren kann).

Durch Googeln habe ich kein nützliches Beispiel für Windows gefunden - eine Lösung für 32-Bit-Apps gefunden,

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

Ich muss aber auch 64-Bit-Apps unterstützen, sodass diese Lösung bei mir nicht funktioniert hat.

Aber ich habe die Quellcodes von Wein geerntet und es geschafft, einen ähnlichen Code zu erstellen, der auch für 64-Bit-Apps funktioniert - Code hier anhängen:

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

Wenn der folgende Code erkennt, ob der Zeiger gültig ist oder nicht, müssen Sie wahrscheinlich eine NULL-Prüfung hinzufügen:

    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();

Dies erhält den Klassennamen vom Zeiger - ich denke, es sollte für Ihre Bedürfnisse ausreichen.

Eine Sache, die ich immer noch befürchte, ist die Leistung der Zeigerprüfung - im obigen Code-Snipet werden bereits 3-4 API-Aufrufe ausgeführt -, die für zeitkritische Anwendungen möglicherweise zu viel des Guten sind.

Es wäre gut, wenn jemand den Overhead der Zeigerprüfung im Vergleich zu beispielsweise C # / verwalteten C ++ - Aufrufen messen könnte.


1

In der Tat könnte unter bestimmten Umständen etwas getan werden: Wenn Sie beispielsweise überprüfen möchten, ob eine Zeichenfolgezeigerzeichenfolge gültig ist, können Sie mit write (fd, buf, szie) syscall die Magie ausführen: Lassen Sie fd ein Dateideskriptor für temporär sein Datei, die Sie für den Test erstellen, und Buf, der auf die Zeichenfolge zeigt, die Sie testen. Wenn der Zeiger ungültig ist, gibt write () -1 zurück und errno wird auf EFAULT gesetzt, was darauf hinweist, dass sich Buf außerhalb Ihres zugänglichen Adressraums befindet.


1

Folgendes funktioniert unter Windows (jemand hat es zuvor vorgeschlagen):

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

Die Funktion muss eine statische, eigenständige oder statische Methode einer Klasse sein. Kopieren Sie die Daten in den lokalen Puffer, um sie schreibgeschützt zu testen. Um das Schreiben zu testen, ohne den Inhalt zu ändern, schreiben Sie es über. Sie können nur die erste / letzte Adresse testen. Wenn der Zeiger ungültig ist, wird die Steuerung an 'doSomething' und dann außerhalb der Klammern übergeben. Verwenden Sie nur keine Destruktoren wie CString.


1

Unter Windows verwende ich diesen Code:

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) + "!");
}

Verwendung:

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


0

Ich habe gesehen, dass verschiedene Bibliotheken eine Methode verwenden, um nach nicht referenziertem Speicher und dergleichen zu suchen. Ich glaube, sie "überschreiben" einfach die Speicherzuweisungs- und Freigabemethoden (malloc / free), die eine Logik haben, die die Zeiger verfolgt. Ich nehme an, dies ist ein Overkill für Ihren Anwendungsfall, aber es wäre eine Möglichkeit, dies zu tun.


Das hilft leider nicht bei stapelzugewiesenen Objekten.
Tom

0

Technisch gesehen können Sie den Operator new überschreiben (und löschen ) und Informationen über den gesamten zugewiesenen Speicher sammeln, sodass Sie mithilfe einer Methode überprüfen können, ob der Heapspeicher gültig ist. aber:

  1. Sie müssen noch prüfen, ob der Zeiger auf stack () zugeordnet ist.

  2. Sie müssen definieren, was ein 'gültiger' Zeiger ist:

a) Speicher an dieser Adresse wird zugewiesen

b) Der Speicher an dieser Adresse wird gestartet Startadresse des Objekts (z. B. Adresse, die sich nicht in der Mitte eines riesigen Arrays befindet)

c) Der Speicher an dieser Adresse ist die Startadresse des Objekts des erwarteten Typs

Fazit : Der fragliche Ansatz ist kein C ++ - Ansatz. Sie müssen einige Regeln definieren, die sicherstellen, dass die Funktion gültige Zeiger empfängt.



0

Nachtrag zu den erreichten Antworten:

Angenommen, Ihr Zeiger könnte nur drei Werte enthalten - 0, 1 und -1, wobei 1 einen gültigen Zeiger, -1 einen ungültigen und 0 einen anderen ungültigen Zeiger bedeutet. Was ist die Wahrscheinlichkeit, dass Ihr Zeiger ist NULL, wobei alle Werte gleich wahrscheinlich? 1/3. Nehmen Sie nun den gültigen Fall heraus, sodass Sie für jeden ungültigen Fall ein Verhältnis von 50:50 haben, um alle Fehler abzufangen. Sieht gut aus, oder? Skalieren Sie dies für einen 4-Byte-Zeiger. Es gibt 2 ^ 32 oder 4294967294 mögliche Werte. Von diesen ist nur EIN Wert korrekt, einer ist NULL und Sie haben noch 4294967292 andere ungültige Fälle. Neu berechnen: Sie haben einen Test für 1 von (4294967292+ 1) ungültigen Fällen. Eine Wahrscheinlichkeit von 2.xe-10 oder 0 für die meisten praktischen Zwecke. Dies ist die Sinnlosigkeit des NULL-Checks.


0

Sie wissen, ein neuer Treiber (zumindest unter Linux), der dazu in der Lage ist, wäre wahrscheinlich nicht so schwer zu schreiben.

Auf der anderen Seite wäre es töricht, Ihre Programme so zu erstellen. Ich würde es nicht empfehlen, es sei denn, Sie haben eine wirklich spezifische und einmalige Verwendung für so etwas. Wenn Sie eine große Anwendung erstellen, die mit konstanten Zeiger-Gültigkeitsprüfungen geladen ist, ist diese wahrscheinlich entsetzlich langsam.


0

Sie sollten diese Methoden vermeiden, da sie nicht funktionieren. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15. Februar 09 um 16:02 Uhr

Wenn sie nicht funktionieren - wird das nächste Windows-Update das Problem beheben? Wenn sie nicht auf Konzeptebene funktionieren, wird die Funktion wahrscheinlich vollständig aus der Windows-API entfernt.

In der MSDN-Dokumentation wird behauptet, dass sie verboten sind, und der Grund dafür ist wahrscheinlich ein Fehler bei der weiteren Gestaltung der Anwendung (z. B. sollten Sie im Allgemeinen keine ungültigen Zeiger stillschweigend essen - wenn Sie natürlich für die Gestaltung der gesamten Anwendung verantwortlich sind) und der Leistung / Zeit der Zeigerprüfung.

Sie sollten jedoch nicht behaupten, dass sie aufgrund eines Blogs nicht funktionieren. In meiner Testanwendung habe ich überprüft, ob sie funktionieren.


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.