Тестирование указателей на валидность (C / C ++) - PullRequest
78 голосов
/ 15 февраля 2009

Есть ли способ определить (программно, конечно), является ли данный указатель "действительным"? Проверить NULL легко, но как насчет таких вещей, как 0x00001234? При попытке разыменования этого типа указателя возникает исключение / сбой.

Кроссплатформенный метод предпочтительнее, но также подходит для конкретной платформы (для Windows и Linux).

Обновление для уточнения: Проблема не в устаревших / освобожденных / неинициализированных указателях; вместо этого я реализую API, который получает указатели от вызывающей стороны (например, указатель на строку, дескриптор файла и т. д.). Вызывающая сторона может отправить (намеренно или по ошибке) неверное значение в качестве указателя. Как предотвратить сбой?

Ответы [ 28 ]

2 голосов
/ 15 февраля 2009

В C ++ нет положений для проверки действительности указателя в общем случае. Очевидно, можно предположить, что NULL (0x00000000) является плохим, и различные компиляторы и библиотеки любят использовать «особые значения» здесь и там, чтобы упростить отладку (например, если я когда-либо увижу указатель, отображаемый как 0xCECECECE в Visual Studio, я знаю, Я сделал что-то не так), но правда в том, что, поскольку указатель - это просто индекс в памяти, почти невозможно определить, просто посмотрев на указатель, является ли он "правильным" индексом.

Существуют различные приемы, которые вы можете сделать с dynamic_cast и RTTI, например, чтобы убедиться, что объект, на который вы указываете, относится к тому типу, который вам нужен, но все они требуют, чтобы вы указывали на что-то допустимое в первую очередь.

Если вы хотите убедиться, что ваша программа может обнаруживать «недействительные» указатели, то мой совет таков: установите каждый указатель, который вы объявляете, либо на NULL, либо на действительный адрес сразу после создания и установите его на NULL сразу после освобождения памяти, которую он указывает на. Если вы усердно относитесь к этой практике, то проверка на NULL - это все, что вам когда-либо понадобится.

2 голосов
/ 30 мая 2018

В Unix вы сможете использовать системный вызов ядра, который выполняет проверку указателя и возвращает EFAULT, например:

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

возвращение:

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

Вероятно, лучше использовать системный вызов, чем open () [возможно, access], поскольку есть вероятность, что это может привести к фактическому пути к файлу при создании файла и последующему требованию закрытия.

2 голосов
/ 16 февраля 2009

Не очень хорошая политика - принимать произвольные указатели в качестве входных параметров в публичном API. Лучше иметь типы «простых данных», такие как целое число, строка или структура (конечно, я имею в виду классическую структуру с простыми данными внутри; официально все может быть структурой).

Почему? Ну, потому что, как говорят другие, не существует стандартного способа узнать, был ли вам дан действительный указатель или указатель на мусор.

Но иногда у вас нет выбора - ваш API должен принимать указатель.

В этих случаях обязанность вызывающего абонента передать хороший указатель. NULL может быть принято как значение, но не указатель на мусор.

Можете ли вы еще раз проверить? Хорошо, что я сделал в подобном случае, чтобы определить инвариант для типа, на который указывает указатель, и вызвать его, когда вы его получите (в режиме отладки). По крайней мере, если инвариант дает сбой (или падает), вы знаете, что вам передали неверное значение.

// 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)
}
2 голосов
/ 15 февраля 2009

Нет никакого портативного способа сделать это, и выполнение этого для определенных платформ может быть где угодно между трудным и невозможным. В любом случае, вы никогда не должны писать код, который зависит от такой проверки - не позволяйте указателям в первую очередь принимать недопустимые значения.

1 голос
/ 25 марта 2018

Действительно, что-то может быть сделано при конкретном случае: например, если вы хотите проверить, является ли строка указателя строки допустимой, использование syscall write (fd, buf, szie) может помочь вам сделать магию: пусть fd будет файлом дескриптор временного файла, который вы создаете для теста, и buf, указывающий на строку, которую вы тестируете, если указатель недействителен, write () вернет -1 и значение errno будет равно EFAULT, что указывает на то, что buf находится за пределами вашего доступного адресного пространства.

1 голос
/ 12 марта 2014

Невероятно, сколько вводящей в заблуждение информации вы можете прочитать в статьях выше ...

И даже в документации Microsoft msdn IsBadPtr объявлен забаненным. Ну да ладно - я предпочитаю рабочее приложение, а не сбой. Даже если термин «работа» может работать неправильно (если конечный пользователь может продолжить работу с приложением).

По поиску, я не нашел ни одного полезного примера для Windows - нашел решение для 32-битных приложений,

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver%2Fsrc%2FdrvCppLib%2Frtti.cpp&obid=58895&obtid=2&ovid=2

но мне нужно также поддерживать 64-битные приложения, поэтому это решение не сработало для меня.

Но я собрал исходные коды Wine и сумел приготовить подобный код, который будет работать и для 64-битных приложений - прикрепив код здесь:

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

И следующий код может определить, является ли указатель действительным или нет, вам, вероятно, нужно добавить проверку 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();

Это получает имя класса из указателя - я думаю, этого должно быть достаточно для ваших нужд.

Одна вещь, которую я все еще боюсь, это производительность проверки указателя - в приведенном выше фрагменте кода уже выполняется 3-4 вызова API - это может быть излишним для приложений, критичных ко времени.

Было бы хорошо, если бы кто-то мог измерить накладные расходы на проверку указателя по сравнению, например, с C # / управляемыми вызовами c ++.

1 голос
/ 11 апреля 2012

Эта статья MEM10-C. Определите и используйте функцию проверки указателя , которая говорит, что в некоторой степени возможно выполнить проверку, особенно в ОС Linux

1 голос
/ 16 февраля 2009

Как уже говорили другие, вы не можете надежно обнаружить неверный указатель. Рассмотрим некоторые формы, которые может принимать недопустимый указатель:

Вы можете иметь нулевой указатель. Это тот, который вы могли бы легко проверить и сделать что-нибудь.

У вас может быть указатель на что-то вне допустимой памяти. То, что составляет действительную память, зависит от того, как среда выполнения вашей системы устанавливает адресное пространство. В системах Unix это, как правило, виртуальное адресное пространство, начинающееся с 0 до нескольких мегабайт. На встроенных системах он может быть довольно маленьким. Это может не начинаться с 0, в любом случае. Если ваше приложение работает в режиме супервизора или аналогичном, то ваш указатель может ссылаться на реальный адрес, который может быть или не быть зарезервирован реальной памятью.

У вас может быть указатель куда-то внутри вашей действительной памяти, даже внутри вашего сегмента данных, bss, стека или кучи, но не указывающий на действительный объект. Вариант этого - указатель, который указывал на действительный объект до того, как что-то случилось с объектом. Плохие вещи в этом контексте включают освобождение, повреждение памяти или повреждение указателя.

У вас может быть плоский недопустимый указатель, такой как указатель с недопустимым выравниванием для объекта, на который делается ссылка.

Проблема становится еще хуже, когда вы рассматриваете архитектуры на основе сегментов / смещений и другие реализации нечетных указателей. Такие вещи обычно скрыты от разработчика хорошими компиляторами и разумным использованием типов, но если вы хотите преодолеть завесу и попытаться перехитрить операционную систему и разработчиков компиляторов, вы можете, но не существует единого общего способа чтобы решить все проблемы, с которыми вы можете столкнуться.

Лучшее, что вы можете сделать, это разрешить сбой и выдать хорошую диагностическую информацию.

1 голос
/ 15 февраля 2009

В общем, это невозможно сделать. Вот один особенно неприятный случай:

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

На многих платформах это распечатает:

[0 1 2]

Вы заставляете систему времени исполнения неправильно интерпретировать биты памяти, но в этом случае она не будет аварийно завершена, потому что биты имеют смысл. Это часть дизайна языка (посмотрите на полиморфизм в стиле C с struct inaddr, inaddr_in, inaddr_in6), поэтому вы не можете надежно защитить его от любой платформы.

0 голосов
/ 16 февраля 2009

Нет способа сделать эту проверку в C ++. Что делать, если другой код передает вам неверный указатель? Вы должны потерпеть крах. Почему? Проверьте эту ссылку: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx

...