Testowanie wskaźników poprawności (C / C ++)


91

Czy istnieje sposób na ustalenie (oczywiście programowo), czy dany wskaźnik jest „prawidłowy”? Sprawdzanie wartości NULL jest łatwe, ale co z takimi rzeczami, jak 0x00001234? Podczas próby wyłuskiwania tego rodzaju wskaźnika następuje wyjątek / awaria.

Preferowana jest metoda wieloplatformowa, ale specyficzna dla platformy (dla Windows i Linux) jest również w porządku.

Aktualizacja w celu wyjaśnienia: problem nie dotyczy nieaktualnych / zwolnionych / niezainicjowanych wskaźników; zamiast tego implementuję API, które pobiera wskaźniki od wywołującego (jak wskaźnik do ciągu, uchwyt pliku itp.). Wzywający może wysłać (celowo lub przez pomyłkę) nieprawidłową wartość jako wskaźnik. Jak zapobiec awariom?



Myślę, że najlepszą pozytywną odpowiedź dotyczącą Linuksa udziela George Carrette. Jeśli to nie wystarczy, rozważ zbudowanie tabeli symboli funkcji w bibliotece lub nawet na innym poziomie tabeli dostępnych bibliotek z własnymi tabelami funkcji. Następnie sprawdź dokładnie te tabele. Oczywiście te negatywne odpowiedzi są również poprawne: nie możesz być naprawdę w 100% pewien, czy wskaźnik funkcji jest prawidłowy, czy nie, chyba że nałożysz wiele dodatkowych ograniczeń na aplikację użytkownika.
minghua

Czy specyfikacja API faktycznie określa taki obowiązek spełnienia przez wdrożenie? Nawiasem mówiąc, udaję, że nie założyłem, że jesteś zarówno programistą, jak i projektantem. Chodzi mi o to, że nie sądzę, aby API określało coś w rodzaju „W przypadku przekazania nieprawidłowego wskaźnika jako argumentu, funkcja musi obsłużyć problem i zwrócić NULL”. API zobowiązuje się do świadczenia usługi w odpowiednich warunkach użytkowania, a nie przez hacki. Niemniej jednak nie zaszkodzi być trochę głupim. Korzystanie z referencji sprawia, że ​​takie przypadki szerzą się mniej spustoszenia. :)
Poniros

Odpowiedzi:


75

Aktualizacja w celu wyjaśnienia: problem nie dotyczy przestarzałych, zwolnionych lub niezainicjowanych wskaźników; zamiast tego implementuję API, które pobiera wskaźniki od wywołującego (jak wskaźnik do ciągu, uchwyt pliku itp.). Wzywający może wysłać (celowo lub przez pomyłkę) nieprawidłową wartość jako wskaźnik. Jak zapobiec awariom?

Nie możesz tego sprawdzić. Po prostu nie ma możliwości sprawdzenia, czy wskaźnik jest „prawidłowy”. Musisz ufać, że kiedy ludzie używają funkcji, która pobiera wskaźnik, ci ludzie wiedzą, co robią. Jeśli przekażą ci 0x4211 jako wartość wskaźnika, musisz zaufać, że wskazuje adres 0x4211. A jeśli „przypadkowo” uderzą w obiekt, to nawet jeśli użyjesz jakiejś przerażającej funkcji systemu operacyjnego (IsValidPtr lub cokolwiek innego), nadal wpadniesz w błąd i szybko nie zawiedziesz.

Zacznij używać pustych wskaźników do sygnalizowania tego rodzaju rzeczy i powiedz użytkownikowi swojej biblioteki, że nie powinni używać wskaźników, jeśli mają tendencję do przypadkowego przekazywania nieprawidłowych wskaźników, poważnie :)


Prawdopodobnie jest to właściwa odpowiedź, ale myślę, że prosta funkcja, która sprawdza typowe lokalizacje pamięci hexspeak, byłaby przydatna do ogólnego debugowania ... W tej chwili mam wskaźnik, który czasami wskazuje na 0xfeeefeee i gdybym miał prostą funkcję, którą mógłbym użyj do pieprzenia twierdzeń wokół To znacznie ułatwiłoby znalezienie winowajcy ... EDYCJA: Chociaż nie byłoby trudno napisać coś samemu, myślę, że ..
Quant

@quant problem polega na tym, że niektóre kody C i C ++ mogą wykonywać arytmetykę wskaźnika na nieprawidłowym adresie bez sprawdzania (w oparciu o zasadę „garbage-in, garbage-out”) iw ten sposób przekazują „zmodyfikowany arytmetycznie” wskaźnik z jednego z nich -znane nieprawidłowe adresy. Typowe przypadki to wyszukiwanie metody z nieistniejącej tabeli vtable na podstawie nieprawidłowego adresu obiektu lub innego typu niewłaściwego typu lub po prostu odczytywanie pól ze wskaźnika do struktury, która nie wskazuje na jeden.
rwong

Zasadniczo oznacza to, że możesz pobierać indeksy tablicowe tylko ze świata zewnętrznego. Interfejs API, który musi bronić się przed wywołującym, po prostu nie może mieć wskaźników w interfejsie. Jednak nadal dobrze byłoby mieć makra do wykorzystania w asercjach dotyczących ważności wskaźników (które z pewnością posiadasz wewnętrznie). Jeśli wskaźnik gwarantuje, że będzie wskazywał wewnątrz tablicy, której punkt początkowy i długość są znane, można to sprawdzić jawnie. Lepiej umrzeć z powodu naruszenia assert (udokumentowany błąd) niż deref (nieudokumentowany błąd).
Rob

34

Oto trzy proste sposoby, dzięki którym program w C pod Linuksem może introspektywnie ocenić stan pamięci, w której jest uruchomiony, i dlaczego w niektórych kontekstach pytanie ma odpowiednio wyrafinowane odpowiedzi.

  1. Po wywołaniu getpagesize () i zaokrągleniu wskaźnika do granicy strony, możesz wywołać mincore (), aby dowiedzieć się, czy strona jest prawidłowa i czy jest częścią zestawu roboczego procesu. Zauważ, że wymaga to pewnych zasobów jądra, więc powinieneś porównać go i określić, czy wywołanie tej funkcji jest naprawdę odpowiednie w twoim api. Jeśli twój interfejs API ma obsługiwać przerwania lub odczytywać z portów szeregowych do pamięci, należy to wywołać, aby uniknąć nieprzewidywalnych zachowań.
  2. Po wywołaniu stat () w celu ustalenia, czy jest dostępny katalog / proc / self, możesz otworzyć fopen i przeczytać / proc / self / maps, aby znaleźć informacje o regionie, w którym znajduje się wskaźnik. Przestudiuj stronę podręcznika systemowego dla proc, pseudo-systemu plików informacji o procesie. Oczywiście jest to stosunkowo drogie, ale możesz być w stanie uciec od buforowania wyniku parsowania do tablicy, którą możesz skutecznie przeszukiwać za pomocą wyszukiwania binarnego. Weź również pod uwagę / proc / self / smaps. Jeśli twój interfejs API jest przeznaczony do obliczeń o wysokiej wydajności, program będzie chciał wiedzieć o / proc / self / numa, które jest udokumentowane na stronie podręcznika man dla numa, niejednolitej architekturze pamięci.
  3. Wywołanie get_mempolicy (MPOL_F_ADDR) jest odpowiednie dla wysokowydajnych obliczeniowych interfejsów API, w których istnieje wiele wątków wykonywania i zarządzasz swoją pracą tak, aby mieć powinowactwo do pamięci niejednorodnej, ponieważ dotyczy ona rdzeni procesora i zasobów gniazd. Taki interfejs API oczywiście powie Ci również, czy wskaźnik jest prawidłowy.

W systemie Microsoft Windows istnieje funkcja QueryWorkingSetEx, która jest udokumentowana w interfejsie API stanu procesu (również w interfejsie API NUMA). Jako następstwo wyrafinowanego programowania NUMA API, ta funkcja pozwoli ci również wykonywać proste prace „testowania wskaźników poprawności (C / C ++)”, jako że jest mało prawdopodobne, aby była przestarzała przez co najmniej 15 lat.


13
Pierwsza odpowiedź, która nie stara się być moralna w stosunku do samego pytania, a właściwie doskonale na nie odpowiada. Ludzie czasami nie zdają sobie sprawy, że naprawdę potrzeba takiego podejścia do debugowania, aby znaleźć błędy np. W bibliotekach innych firm lub w starszym kodzie, ponieważ nawet valgrind znajduje dzikie wskaźniki tylko wtedy, gdy faktycznie uzyskuje do nich dostęp, a nie np. Jeśli chcesz regularnie sprawdzać wskaźniki pod kątem ważności w tabeli pamięci podręcznej, która została nadpisana z innego miejsca w twoim kodzie ...
lumpidu

To powinna być akceptowana odpowiedź. Zrobiłem to podobnie na platformie innej niż Linux. Zasadniczo polega to na wystawieniu informacji o procesie na sam proces. W tym aspekcie wygląda na to, że Windows radzi sobie lepiej niż linux, ujawniając bardziej znaczące informacje poprzez API statusu procesu.
minghua

31

Zapobieganie awariom spowodowanym przez wysyłanie przez dzwoniącego nieprawidłowego wskaźnika to dobry sposób na tworzenie cichych błędów, które są trudne do znalezienia.

Czy nie jest lepiej dla programisty używającego twojego API, aby uzyskać jasny komunikat, że jego kod jest fałszywy, przez awarię go, a nie ukrywanie?


8
Jednak w niektórych przypadkach, sprawdzanie złym wskaźnikiem natychmiast gdy API jest nazywany jest jak nie wcześniej. Na przykład, co się stanie, jeśli interfejs API zapisze wskaźnik w strukturze danych, w której zostanie on odroczony dopiero później? Następnie przekazanie API złego wskaźnika spowoduje awarię w pewnym przypadkowym późniejszym momencie. W takim przypadku lepiej byłoby wcześniej zawieść, przy wywołaniu API, w którym pierwotnie wprowadzono złą wartość.
peterflynn

28

W Win32 / 64 jest na to sposób. Spróbuj odczytać wskaźnik i przechwycić wynikowe wykonanie SEH, które zostanie wyrzucone w przypadku niepowodzenia. Jeśli nie rzuca, jest to prawidłowy wskaźnik.

Problem z tą metodą polega jednak na tym, że zwraca ona tylko informację, czy można odczytać dane ze wskaźnika. Nie gwarantuje bezpieczeństwa typu ani żadnej liczby innych niezmienników. Ogólnie rzecz biorąc, ta metoda jest dobra tylko do powiedzenia „tak, potrafię odczytać to konkretne miejsce w pamięci w czasie, który właśnie minął”.

Krótko mówiąc, nie rób tego;)

Raymond Chen zamieścił post na blogu na ten temat: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


3
@Tim, nie ma takiej możliwości w C ++.
JaredPar

6
Jest to tylko „prawidłowa odpowiedź”, jeśli zdefiniujesz „prawidłowy wskaźnik” jako „nie powoduje naruszenia dostępu / segfault”. Wolałbym to zdefiniować jako „wskazuje na znaczące dane przydzielone do celu, w jakim zamierzasz ich użyć”. Twierdzę, że to lepsza definicja ważności wskaźnika ...;)
jalf

Nawet jeśli wskaźnik jest prawidłowy, nie można tego sprawdzić w ten sposób. Pomyśl o thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {sleep (1); usuń p; ...}
Christopher,

2
@Christopher, bardzo prawda. Powinienem był powiedzieć „Mogę odczytać to szczególne miejsce w pamięci w czasie, który już minął”
JaredPar

@JaredPar: Naprawdę zła sugestia. Może wywołać stronę ochronną, więc stos nie zostanie później rozwinięty ani nie będzie równie przyjemny.
Deduplicator,

16

AFAIK nie ma sposobu. Powinieneś spróbować uniknąć tej sytuacji, zawsze ustawiając wskaźniki na NULL po zwolnieniu pamięci.


4
Ustawienie wskaźnika na null nie daje nic, może z wyjątkiem fałszywego poczucia bezpieczeństwa.

To nie jest prawda. Zwłaszcza w C ++ możesz określić, czy usunąć obiekty członkowskie, sprawdzając, czy nie ma wartości null. Należy również zauważyć, że w C ++ można usuwać wskaźniki null, dlatego popularne jest bezwarunkowe usuwanie obiektów w destruktorach.
Ferdinand Beyer

4
int * p = new int (0); int * p2 = p; usuń p; p = NULL; usuń p2; // crash

1
zabzonk i ?? powiedział, że można usunąć wskaźnik zerowy. p2 nie jest pustym wskaźnikiem, ale jest nieprawidłowym wskaźnikiem. musisz wcześniej ustawić go na null.
Johannes Schaub - litb

2
Jeśli masz aliasy do wskazanej pamięci, tylko jeden z nich byłby ustawiony na NULL, inne aliasy wiszą wokół.
jdehaan


7

Odnośnie odpowiedzi nieco wyżej w tym wątku:

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () dla systemu Windows.

Radzę trzymać się od nich z daleka, ktoś już to opublikował: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

Innym postem na ten sam temat i tego samego autora (chyba) jest ten: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx („IsBadXxxPtr powinno naprawdę nazywać się CrashProgramRandomly ”).

Jeśli użytkownicy twojego API wysyłają złe dane, pozwól mu się zawiesić. Jeśli problem polega na tym, że przekazane dane nie są używane do później (co utrudnia znalezienie przyczyny), dodaj tryb debugowania, w którym łańcuchy znaków itp. Są rejestrowane na wejściu. Jeśli są złe, będzie to oczywiste (i prawdopodobnie ulegnie awarii). Jeśli zdarza się to zbyt często, warto przenieść interfejs API poza proces i pozwolić mu na awarię procesu API zamiast głównego procesu.


Prawdopodobnie innym sposobem jest użycie _CrtIsValidHeapPointer . Ta funkcja zwróci wartość TRUE, jeśli wskaźnik jest prawidłowy, i zgłosi wyjątek, gdy wskaźnik zostanie zwolniony. Jak udokumentowano, ta funkcja jest dostępna tylko w debugowaniu CRT.
Crend King

6

Po pierwsze, nie widzę sensu w próbach ochrony przed dzwoniącym celowo próbującym spowodować awarię. Mogliby to łatwo zrobić, próbując samodzielnie uzyskać dostęp przez nieprawidłowy wskaźnik. Jest wiele innych sposobów - mogą one po prostu nadpisać pamięć lub stos. Jeśli chcesz zabezpieczyć się przed tego rodzaju rzeczami, musisz uruchomić oddzielny proces, używając gniazd lub innego IPC do komunikacji.

Piszemy sporo oprogramowania, które umożliwia partnerom / klientom / użytkownikom rozszerzenie funkcjonalności. Nieuchronnie każdy błąd jest najpierw zgłaszany do nas, więc warto mieć możliwość łatwego pokazania, że ​​problem tkwi w kodzie wtyczki. Ponadto istnieją obawy dotyczące bezpieczeństwa, a niektórzy użytkownicy są bardziej zaufani niż inni.

Używamy wielu różnych metod w zależności od wymagań dotyczących wydajności / przepustowości i wiarygodności. Od najbardziej preferowanych:

  • oddzielne procesy wykorzystujące gniazda (często przekazujące dane jako tekst).

  • oddzielne procesy korzystające z pamięci współdzielonej (w przypadku przesyłania dużych ilości danych).

  • ten sam proces oddzielne wątki za pośrednictwem kolejki komunikatów (w przypadku częstych krótkich komunikatów).

  • ten sam proces rozdziela wątki wszystkie przekazywane dane przydzielone z puli pamięci.

  • ten sam proces przez bezpośrednie wywołanie procedury - wszystkie przekazane dane przydzielone z puli pamięci.

Staramy się nigdy nie uciekać się do tego, co próbujesz zrobić, mając do czynienia z oprogramowaniem stron trzecich - zwłaszcza gdy wtyczki / bibliotekę otrzymujemy jako kod binarny, a nie źródłowy.

Korzystanie z puli pamięci jest dość łatwe w większości przypadków i nie musi być nieefektywne. Jeśli TY przydzielasz dane w pierwszej kolejności, sprawdzenie wskaźników względem przydzielonych wartości jest banalne. Można również zapisać przydzieloną długość i dodać „magiczne” wartości przed i po danych, aby sprawdzić prawidłowy typ danych i przepełnienia danych.


5

W systemie Unix powinieneś być w stanie wykorzystać wywołanie systemowe jądra, które sprawdza wskaźnik i zwraca EFAULT, takie jak:

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

powracający:

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

Prawdopodobnie istnieje lepsze wywołanie systemowe niż open () [być może dostęp], ponieważ istnieje szansa, że ​​może to doprowadzić do rzeczywistej ścieżki kodowej tworzenia pliku i późniejszego zamknięcia wymagania.


1
To genialny hack. Bardzo chciałbym zapoznać się z poradami dotyczącymi różnych wywołań systemowych w celu weryfikacji zakresów pamięci, zwłaszcza jeśli można zagwarantować, że nie będą one miały skutków ubocznych. Możesz pozostawić otwarty deskryptor pliku, aby zapisać do / dev / null, aby sprawdzić, czy bufory są w czytelnej pamięci, ale prawdopodobnie są prostsze rozwiązania. Najlepsze, co mogę znaleźć, to dowiązanie symboliczne (ptr, ""), które ustawia errno na 14 przy złym adresie lub 2 na dobrym adresie, ale zmiany w jądrze mogą zmienić kolejność weryfikacji.
Preston

1
@Preston Myślę, że w DB2 używaliśmy metody access () z unistd.h. Użyłem metody open () powyżej, ponieważ jest to trochę mniej niejasne, ale prawdopodobnie masz rację, że jest wiele możliwych wywołań systemowych do użycia. Windows miał kiedyś jawne API sprawdzania wskaźnika, ale okazało się, że nie jest bezpieczny dla wątków (myślę, że używał SEH do próby zapisu, a następnie przywrócenia granic zakresu pamięci).
Peeter Joot

4

Mam wiele zrozumienia dla twojego pytania, ponieważ sam jestem w prawie identycznej sytuacji. Doceniam to, co mówi wiele odpowiedzi, i są one poprawne - procedura dostarczająca wskaźnik powinna zapewniać prawidłowy wskaźnik. W moim przypadku jest prawie nie do pomyślenia, że ​​mogli uszkodzić wskaźnik - ale gdyby im się to udało, to MOJE oprogramowanie uległo awarii, a JA byłby winny :-(

Nie wymagam, abym kontynuował działanie po błędzie segmentacji - to byłoby niebezpieczne - po prostu chcę zgłosić klientowi, co się stało przed zakończeniem, aby mógł naprawić swój kod, zamiast obwiniać mnie!

Oto jak to zrobiłem (w systemie Windows): http://www.cplusplus.com/reference/clibrary/csignal/signal/

Aby podać streszczenie:

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

Teraz wygląda na to, że zachowuje się tak, jak bym chciał (wyświetla komunikat o błędzie, a następnie zamyka program) - ale jeśli ktoś zauważy usterkę, daj mi znać!


Nie używać exit(), omija RAII, a tym samym może powodować wycieki zasobów.
Sebastian Mach

Ciekawe - czy jest inny sposób na zgrabne zakończenie w tej sytuacji? I czy instrukcja exit jest jedynym problemem związanym z robieniem tego w ten sposób? Zauważyłem, że uzyskałem „-1” - czy to tylko z powodu „wyjścia”?
Mike Sadler

Ups, zdaję sobie sprawę, że to całkiem wyjątkowa sytuacja. Właśnie to zobaczyłem exit()i zaczął dzwonić mój Portable C ++ Alarm Bell. Powinno być w porządku w tej specyficznej dla Linuksa sytuacji, w której program i tak by się zakończył, przepraszam za hałas.
Sebastian Mach

1
signal (2) nie jest przenośny. Użyj sigaction (2). man 2 signalw Linuksie zawiera akapit wyjaśniający dlaczego.
rptb1

1
W tej sytuacji zwykle wywoływałbym abort (3) zamiast exit (3), ponieważ jest bardziej prawdopodobne, że wygeneruje jakiś rodzaj śledzenia wstecznego debugowania, którego możesz użyć do zdiagnozowania problemu po śmierci. Na większości Unixen abort (3) zrzuci core (jeśli zrzuty core są dozwolone), a na Windows zaoferuje uruchomienie debuggera, jeśli jest zainstalowany.
rptb1

2

W C ++ nie ma przepisów do testowania poprawności wskaźnika jako przypadku ogólnego. Można oczywiście założyć, że NULL (0x00000000) jest zły, a różne kompilatory i biblioteki lubią używać tu i tam "specjalnych wartości", aby ułatwić debugowanie (na przykład, jeśli kiedykolwiek zobaczę wskaźnik pokazujący się jako 0xCECECECE w Visual Studio, wiem Zrobiłem coś źle), ale prawda jest taka, że ​​skoro wskaźnik jest tylko indeksem w pamięci, prawie nie da się stwierdzić, patrząc na wskaźnik, czy jest to „właściwy” indeks.

Istnieją różne sztuczki, które możesz zrobić za pomocą dynamic_cast i RTTI, aby upewnić się, że wskazywany obiekt jest odpowiedniego typu, ale wszystkie one wymagają przede wszystkim wskazania czegoś ważnego.

Jeśli chcesz mieć pewność, że program może wykryć „nieprawidłowe” wskaźniki, moja rada jest następująca: Ustaw każdy deklarowany wskaźnik na NULL lub prawidłowy adres natychmiast po utworzeniu i ustaw go na NULL natychmiast po zwolnieniu pamięci, na którą wskazuje. Jeśli pilnie podchodzisz do tej praktyki, sprawdzenie NULL jest wszystkim, czego kiedykolwiek potrzebujesz.


Stała wskaźnika zerowego w C ++ (lub C, jeśli o to chodzi), jest reprezentowana przez stałą całkowitą zero. Wiele implementacji używa samych zer binarnych do reprezentacji, ale nie jest to coś, na co można liczyć.
David Thornley,

2

Nie ma na to żadnego przenośnego sposobu, a zrobienie tego na określonych platformach może być trudne lub niemożliwe. W każdym razie nigdy nie powinieneś pisać kodu, który zależy od takiej kontroli - nie pozwól, aby wskaźniki w pierwszej kolejności przyjmowały nieprawidłowe wartości.


2

Ustawienie wskaźnika na NULL przed i po użyciu jest dobrą techniką. Jest to łatwe do zrobienia w C ++, jeśli zarządzasz wskaźnikami w klasie, na przykład (ciąg znaków):

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

Zaktualizowane pytanie sprawia, że ​​moja odpowiedź jest oczywiście błędna (lub przynajmniej odpowiedź na inne pytanie). Zgadzam się z odpowiedziami, które w zasadzie mówią, pozwól im upaść, jeśli nadużywają api. Nie możesz powstrzymać ludzi przed uderzaniem się w kciuk młotkiem ...
Tim Ring,

2

Akceptowanie dowolnych wskaźników jako parametrów wejściowych w publicznym interfejsie API nie jest dobrą zasadą. Lepiej jest mieć typy „zwykłych danych”, takie jak liczba całkowita, ciąg znaków lub struktura (mam na myśli oczywiście klasyczną strukturę z czystymi danymi; oficjalnie strukturą może być wszystko).

Czemu? Cóż, ponieważ jak mówią inni, nie ma standardowego sposobu sprawdzenia, czy otrzymałeś prawidłowy wskaźnik, czy taki, który wskazuje na śmieci.

Ale czasami nie masz wyboru - twoje API musi akceptować wskaźnik.

W takich przypadkach obowiązkiem dzwoniącego jest przekazanie dobrego wskaźnika. NULL może być akceptowana jako wartość, ale nie jako wskaźnik do śmieci.

Czy możesz w jakikolwiek sposób sprawdzić dwukrotnie? Cóż, w takim przypadku zdefiniowałem niezmiennik typu, na który wskazuje wskaźnik, i wywołałem go, gdy go otrzymasz (w trybie debugowania). Przynajmniej jeśli niezmiennik zawiedzie (lub ulegnie awarii), wiesz, że przekazano Ci złą wartość.

// 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: "przekazuj zwykły typ danych ... jak łańcuch" Ale w C ++ łańcuchy są najczęściej przekazywane jako wskaźniki do znaków, (char *) lub (const char *), więc wracasz do przekazywania wskaźników. Twój przykład przekazuje in_person jako odniesienie, a nie jako wskaźnik, więc porównanie (in_person! = NULL) sugeruje, że istnieją pewne porównania obiekt / wskaźnik zdefiniowane w klasie Person.
Jesse Chisholm

@JesseChisholm Przez string miałem na myśli string, czyli std :: string. W żaden sposób nie polecam używania znaku * jako sposobu na przechowywanie ciągów lub przekazywanie ich. Nie rób tego.
Daniel Daranas

@JesseChisholm Z jakiegoś powodu popełniłem błąd, odpowiadając na to pytanie pięć lat temu. Oczywiście nie ma sensu sprawdzanie, czy osoba & jest NULL. To się nawet nie skompiluje. Chciałem używać wskaźników, a nie odniesień. Naprawiłem to teraz.
Daniel Daranas

1

Jak powiedzieli inni, nie można niezawodnie wykryć nieprawidłowego wskaźnika. Rozważ kilka form, jakie może przybierać nieprawidłowy wskaźnik:

Możesz mieć pusty wskaźnik. Można to łatwo sprawdzić i coś z tym zrobić.

Możesz mieć wskaźnik do miejsca poza ważną pamięcią. To, co składa się na prawidłową pamięć, różni się w zależności od tego, jak środowisko wykonawcze systemu konfiguruje przestrzeń adresową. W systemach uniksowych jest to zwykle wirtualna przestrzeń adresowa zaczynająca się od 0 i sięgająca dużej liczby megabajtów. W systemach wbudowanych może być dość mały. W każdym razie może nie zaczynać się od 0. Jeśli zdarzy się, że Twoja aplikacja działa w trybie nadzorcy lub równoważnym, wskaźnik może odnosić się do rzeczywistego adresu, który może, ale nie musi, być zarchiwizowany z rzeczywistą pamięcią.

Możesz mieć wskaźnik do jakiegoś miejsca w twojej prawidłowej pamięci, nawet wewnątrz segmentu danych, bss, stosu lub sterty, ale nie wskazujący na prawidłowy obiekt. Wariantem tego jest wskaźnik, który wskazywał na prawidłowy obiekt, zanim coś złego stało się z obiektem. Złe rzeczy w tym kontekście obejmują zwolnienie alokacji, uszkodzenie pamięci lub uszkodzenie wskaźnika.

Możesz mieć płaski, niedozwolony wskaźnik, taki jak wskaźnik z niedozwolonym wyrównaniem dla elementu, do którego się odwołujesz.

Problem staje się jeszcze gorszy, gdy weźmie się pod uwagę architektury oparte na segmentach / przesunięciu i inne dziwne implementacje wskaźników. Takie rzeczy są zwykle ukrywane przed programistą przez dobre kompilatory i rozsądne użycie typów, ale jeśli chcesz przebić zasłonę i spróbować przechytrzyć system operacyjny i programistów kompilatorów, możesz, ale nie ma jednego ogólnego sposobu zrobić to, co rozwiąże wszystkie problemy, z którymi możesz się spotkać.

Najlepsze, co możesz zrobić, to zezwolić na awarię i podać dobre informacje diagnostyczne.


re: „wystaw dobre informacje diagnostyczne”, jest problem. Ponieważ nie możesz sprawdzić poprawności wskaźnika, informacje, o które musisz się martwić, są minimalne. „Tutaj zdarzył się wyjątek”, może być wszystkim, co otrzymujesz. Cały stos wywołań jest fajny, ale wymaga lepszego środowiska niż większość bibliotek wykonawczych C ++.
Jesse Chisholm


1

Ogólnie rzecz biorąc, jest to niemożliwe. Oto jeden szczególnie nieprzyjemny przypadek:

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

Na wielu platformach wydrukuje się to:

[0 1 2]

Zmuszasz system wykonawczy do nieprawidłowej interpretacji bitów pamięci, ale w tym przypadku nie nastąpi awaria, ponieważ wszystkie bity mają sens. Jest częścią projektu języka (spojrzenie w stylu C polimorfizmu z struct inaddr, inaddr_in, inaddr_in6), więc nie można niezawodnie chroni przed nią na każdej platformie.


1

To niewiarygodne, ile mylących informacji można przeczytać w artykułach powyżej ...

Nawet w dokumentacji Microsoft MSDN twierdzi się, że IsBadPtr został zbanowany. No cóż - wolę działającą aplikację niż zawieszanie się. Nawet jeśli praca terminowa może działać nieprawidłowo (o ile użytkownik końcowy może kontynuować aplikację).

Googlując nie znalazłem żadnego przydatnego przykładu dla Windows - znalazłem rozwiązanie dla aplikacji 32-bitowych,

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

ale muszę również obsługiwać aplikacje 64-bitowe, więc to rozwiązanie nie działa dla mnie.

Ale zebrałem kody źródłowe wina i udało mi się ugotować podobny rodzaj kodu, który działałby również dla 64-bitowych aplikacji - załączam kod tutaj:

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

Poniższy kod może wykryć, czy wskaźnik jest prawidłowy, czy nie, prawdopodobnie musisz dodać sprawdzanie 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();

To pobiera nazwę klasy ze wskaźnika - myślę, że powinno wystarczyć do twoich potrzeb.

Jedną rzeczą, której wciąż się obawiam, jest wydajność sprawdzania wskaźników - we fragmencie kodu powyżej są już wykonywane 3-4 wywołania API - może to być przesada dla aplikacji, które mają krytyczne znaczenie czasowe.

Byłoby dobrze, gdyby ktoś mógł zmierzyć narzut sprawdzania wskaźników w porównaniu na przykład z wywołaniami C # / zarządzanymi C ++.


1

Rzeczywiście, coś można zrobić w określonej sytuacji: na przykład, jeśli chcesz sprawdzić, czy łańcuch będący wskaźnikiem łańcucha jest prawidłowy, użycie funkcji write (fd, buf, szie) syscall może pomóc ci zrobić magię: niech fd będzie deskryptorem pliku tymczasowego plik, który tworzysz do testu, a buf wskazuje na łańcuch, który sprawdzasz, jeśli wskaźnik jest nieprawidłowy, metoda write () zwróciłaby -1, a errno ustawione na EFAULT, co wskazuje, że buf znajduje się poza dostępną przestrzenią adresową.


1

Następujące działa w systemie Windows (ktoś wcześniej to zasugerował):

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

Funkcja musi być statyczną, samodzielną lub statyczną metodą jakiejś klasy. Aby przetestować tylko do odczytu, skopiuj dane do bufora lokalnego. Aby przetestować zapis bez modyfikowania treści, napisz je. Możesz testować tylko pierwszy / ostatni adres. Jeśli wskaźnik jest nieprawidłowy, kontrola zostanie przekazana do „doSomething”, a następnie poza nawiasy. Po prostu nie używaj niczego wymagającego destruktorów, takich jak CString.


1

W systemie Windows używam tego kodu:

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

Stosowanie:

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


0

Widziałem różne biblioteki używające jakiejś metody do sprawdzania pamięci bez odwołań i tym podobnych. Uważam, że po prostu „zastępują” metody alokacji i zwalniania pamięci (malloc / free), które mają pewną logikę, która śledzi wskaźniki. Przypuszczam, że to przesada w twoim przypadku użycia, ale byłby to jeden ze sposobów, aby to zrobić.


To niestety nie pomaga w przypadku obiektów przydzielonych na stosie.
Tom

0

Z technicznego punktu widzenia można zastąpić operator new (i usunąć ) i zebrać informacje o całej przydzielonej pamięci, dzięki czemu można mieć metodę sprawdzania, czy pamięć sterty jest poprawna. ale:

  1. nadal potrzebujesz sposobu, aby sprawdzić, czy wskaźnik jest przydzielony na stosie ()

  2. będziesz musiał zdefiniować, co jest `` prawidłowym '' wskaźnikiem:

a) pamięć pod tym adresem jest przydzielona

b) pamięć pod tym adresem to adres początkowy obiektu (np. adres nie jest pośrodku dużej tablicy)

c) pamięć pod tym adresem jest adresem początkowym obiektu oczekiwanego typu

Konkluzja : podejście, o którym mowa, nie jest sposobem C ++, musisz zdefiniować pewne reguły, które zapewnią, że funkcja otrzyma prawidłowe wskaźniki.



0

Dodatek do gotowych odpowiedzi:

Załóżmy, że twój wskaźnik może zawierać tylko trzy wartości - 0, 1 i -1, gdzie 1 oznacza prawidłowy wskaźnik, -1 nieprawidłową, a 0 inną nieprawidłową. Jakie jest prawdopodobieństwo, że wskaźnik ma wartość NULL, a wszystkie wartości są jednakowo prawdopodobne? 1/3. Teraz wyjmij poprawną wielkość przypadku, więc dla każdego nieprawidłowego przypadku masz stosunek 50:50, aby wyłapać wszystkie błędy. Wygląda dobrze, prawda? Skaluj to dla 4-bajtowego wskaźnika. Istnieją 2 ^ 32 lub 4294967294 możliwych wartości. Spośród nich tylko JEDNA wartość jest poprawna, jedna to NULL, a nadal pozostaje 4294967292 innych nieprawidłowych przypadków. Oblicz ponownie: masz test dla 1 z (4294967292+ 1) nieprawidłowych przypadków. Prawdopodobieństwo 2 x e-10 lub 0 dla większości praktycznych celów. Taka jest daremność czeku NULL.


0

Wiesz, nowy sterownik (przynajmniej w Linuksie), który jest w stanie to zrobić, prawdopodobnie nie byłby taki trudny do napisania.

Z drugiej strony tworzenie programów w ten sposób byłoby głupotą. Jeśli nie masz naprawdę konkretnego i jednorazowego użytku do czegoś takiego, nie polecałbym tego. Jeśli zbudowałeś dużą aplikację ładowaną ze stałymi sprawdzeniami poprawności wskaźnika, prawdopodobnie będzie to horrendalnie powolne.


0

należy unikać tych metod, ponieważ nie działają. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15 lutego '09 o 16:02

Jeśli nie działają - następna aktualizacja systemu Windows to naprawi? Jeśli nie działają na poziomie koncepcyjnym - funkcja zostanie prawdopodobnie całkowicie usunięta z api systemu Windows.

Dokumentacja MSDN twierdzi, że są one zbanowane, a przyczyną tego jest prawdopodobnie błąd dalszego projektowania aplikacji (np. Generalnie nie powinieneś jeść po cichu nieprawidłowych wskaźników - oczywiście jeśli jesteś odpowiedzialny za projekt całej aplikacji) oraz wydajność / czas sprawdzania wskaźnika.

Ale nie powinieneś twierdzić, że nie działają z powodu jakiegoś bloga. W mojej aplikacji testowej zweryfikowałem, że działają.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.