Тестирование указателей на валидность (C / C++)

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

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

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

28 ответов


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

вы не можете сделать, что проверить. Вы просто не можете проверить, является ли указатель "допустимым". Ты должен доверять этому. когда люди используют функцию, которая принимает указатель, эти люди знают, что они делают. Если они передают вам 0x4211 в качестве значения указателя, то вы должны доверять ему указывает на адрес 0x4211. И если они "случайно" попали в объект, то даже если вы будете использовать какую-то страшную функцию операционной системы (IsValidPtr или что-то еще), вы все равно проскользнете в ошибку и не потерпите неудачу быстро.

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


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

не лучше ли программисту, использующему ваш API, получить четкое сообщение о том, что его код является фиктивным, разбив его, а не скрывая его?


на Win32 / 64 есть способ сделать это. Попытайтесь прочитать указатель и поймать результирующий SEH exeception, который будет брошен при сбое. Если он не бросает, то это действительный указатель.

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

короче говоря, не делайте этого;)

Реймонд Чен в блоге на эту тему: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


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

  1. после вызова getpagesize () и округления указателя на страницу boundary, вы можете вызвать mincore (), чтобы узнать, является ли страница допустимой и если это происходит, чтобы быть частью рабочего набора процесса. Обратите внимание, что это требует некоторые ресурсы ядра, поэтому вы должны проверить это и определить, если вызов этой функции действительно подходит для вашего api. Если ваш api будет обрабатывать прерывания или чтение из последовательных портов в память уместно назвать это, чтобы избежать непредсказуемости поведения.
  2. после вызова stat (), чтобы определить, есть ли каталог /proc/self, вы можете открыть и прочитать /proc / self / maps найти информацию о регионе, в котором находится указатель. Изучите man-страницу для proc, информацию о процессе псевдо-файл система. Очевидно, что это относительно дорого, но вы можете быть возможность уйти с кэшированием результата синтаксического анализа в массив вы можете эффективно искать с помощью двоичного поиска. Также рассмотрите / proc / self / smaps. Если ваш api предназначен для высокопроизводительных вычислений, то программа захочет узнать о /proc / self/numa, который документировано под страницей руководства для numa, неоднородной памяти архитектура.
  3. вызов get_mempolicy(MPOL_F_ADDR) подходит для высокопроизводительный вычислительный api работает там, где есть несколько потоков выполнение, и вы управляете своей работой, чтобы иметь сродство к неоднородной памяти как это относится к ядрам cpu и ресурсам сокетов. Такой api конечно, также скажет вам, если указатель действителен.

в Microsoft Windows есть функция QueryWorkingSetEx, которая документирована в API состояния процесса (также в API NUMA). Как следствие сложного программирования API NUMA эта функция также позволит вам сделать простую работу " тестовые указатели на валидность (C/C++)", так как она вряд ли будет устаревшей в течение как минимум 15 лет.


AFAIK нет никакого способа. Вы должны попытаться избежать этой ситуации, всегда устанавливая указатели на NULL после освобождения памяти.


посмотреть этой и этой вопрос. Также взгляните на смарт-указатели.


относительно ответа немного в этой теме:

IsBadReadPtr(), IsBadWritePtr(), IsBadCodePtr (), IsBadStringPtr () для Windows.

мой совет-держаться от них подальше, кто-то уже опубликовал этот: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

еще один пост на ту же тему и того же автора (Я думаю) это один: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ("IsBadXxxPtr действительно следует называть CrashProgramRandomly").

Если пользователи вашего API отправляет плохие данные, пусть это сбой. Если проблема в том, что переданные данные не используются до более позднего времени (и это затрудняет поиск причины), добавьте режим отладки, где строки и т. д. регистрируются при входе. Если они плохие, это будет очевидно (и, вероятно, сбой). Если это происходит так часто, это может быть стоит переместить API из процесса и позволить им сбой процесса API вместо основного процесса.


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

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

мы используем несколько различных методов в зависимости от требований к производительности/пропускной способности и надежности. Из наиболее предпочтительных:

  • отдельные процессы, использующие сокеты (часто проходящие данные в виде текста).

  • отдельные процессы, использующие общую память (при передаче больших объемов данных).

  • тот же процесс отделяет потоки через очередь сообщений (если частые короткие сообщения).

  • тот же процесс отдельные потоки все переданные данные, выделенные из пула памяти.

  • тот же процесс через прямой вызов процедуры - все переданные данные, выделенные из пула памяти.

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

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


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

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

вот как я нашел, чтобы сделать это (в Windows):http://www.cplusplus.com/reference/clibrary/csignal/signal/

чтобы дать краткий обзор:

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

вот этот появляется вести себя так, как я надеюсь (он печатает сообщение об ошибке , затем завершает программу) - но если кто-то может обнаружить недостаток, пожалуйста, дайте мне знать!


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

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

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


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


установка указателя на NULL до и после использования является хорошей техникой. Это легко сделать в C++ , если вы управляете указателями в классе, например (строка):

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

не очень хорошая политика принимать произвольные указатели в качестве входных параметров в общедоступном 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)
}

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

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

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

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

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

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

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


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

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), поэтому вы не можете надежно защитить против его на любом платформа.


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

и даже в документации microsoft msdn утверждается, что IsBadPtr запрещен. О, хорошо-я предпочитаю рабочее приложение, а не сбой. Даже если срок работы может работать неправильно (до тех пор, пока конечный пользователь может продолжить работу с приложением).

по googling я не нашел никакого полезного примера для 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-битные приложения, поэтому это решение не сработало для меня.

но я собрал исходные коды вина и сумел приготовить аналогичный код, который будет работать и для 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;  
}  

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

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

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

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

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


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


IsBadReadPtr(), IsBadWritePtr(), IsBadCodePtr (), IsBadStringPtr () для Windows.
Они занимают время, пропорциональное длине блока, поэтому для проверки здравомыслия я просто проверяю начальный адрес.


Я видел, как различные библиотеки используют какой-то метод для проверки неферментированной памяти и тому подобное. Я считаю, что они просто "переопределяют" методы выделения и освобождения памяти (malloc/free), которые имеют некоторую логику, которая отслеживает указатели. Я полагаю, что это излишне для вашего случая использования, но это был бы один из способов сделать это.


технически вы можете переопределить оператор newудалить) и собирать информацию о всех выделенной памяти, так что вы можете иметь метод, чтобы проверить, если память действует. но:

  1. вам все еще нужен способ проверить, выделен ли указатель на stack ()

  2. вам нужно будет определить, что такое "действительный" указатель:

a) память по этому адресу выделено

b) память по этому адресу is старт адрес объекта (напр. адрес не в середине огромного array)

C) памяти по этому адресу is старт адрес объекта ожидается тип

нижняя строка: подход, о котором идет речь, не является способом c++, вам нужно определить некоторые правила, которые гарантируют, что функция получает действительные указатели.


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


добавление к утвержденному ответу(ответам):

предположим, что ваш указатель может содержать только три значения -- 0, 1 и -1, где 1 означает допустимый указатель, -1 недопустимый и 0 другой недопустимый. Какова вероятность того, что ваш указатель is NULL, все значения одинаково вероятны? 1/3. Теперь возьмите допустимый случай, поэтому для каждого недопустимого случая у вас есть соотношение 50:50, чтобы поймать все ошибки. Хорошо выглядит, правда? Шкале это 4-байтовый указатель. Есть 2^32 или 4294967294 возможное значение. Из них только одно значение является правильным, одно-нулевым, и вы все еще остаетесь с 4294967292 другими недопустимыми случаями. Пересчитать: у вас есть тест для 1 из (4294967292+ 1) недопустимых случаев. Вероятность 2.xe-10 или 0 для большинства практических целей. Такова тщетность нулевой проверки.


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

с другой стороны, было бы глупо создавать такие программы. Если у вас нет какого-то конкретного и одноразового использования для такой вещи, я бы не рекомендовал его. Если вы построили большое приложение, загруженное с постоянными проверками достоверности указателя, оно, вероятно, будет ужасно медленным.


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


вы должны избегать этих методов, потому что они не работают. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx – JaredPar 15 февраля '09 В 16:02

если они не работают-следующее обновление windows исправит это ? Если они не работают на уровне концепции - функция, вероятно, будет полностью удалена из Windows api.

документация MSDN утверждает, что они запрещены, и причиной этого, вероятно, является недостаток дальнейшего проектирования приложения (например, как правило, вы не должны есть недопустимые указатели молча - если вы отвечаете за дизайн всего приложения, конечно), и производительность / время проверки указателя.

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


эти ссылки могут быть полезны

_CrtIsValidPointer Проверяет допустимость указанного диапазона памяти для чтения и записи (только отладочная версия). http://msdn.microsoft.com/en-us/library/0w1ekd5e.aspx

_CrtCheckMemory Подтверждает целостность блоков памяти, выделенных в отладочной куче (только отладочная версия). http://msdn.microsoft.com/en-us/library/e73x0s4b.aspx


следующее работает в Windows (кто-то предложил это раньше):

статический недействительным копия(недействительными * цель, как const Void*, который Источник, размер инт ) { __пытаться { CopyMemory (цель, источник, размер); } __except(EXCEPTION_EXECUTE_HANDLER) { сделать (...что бы--); } }

функция должна быть статическим, автономным или статическим методом некоторого класса. Чтобы проверить только для чтения, скопируйте данные в локальный буфер. Тест на запись без изменения содержание, запишите их. Вы можете проверить только первый / последний адреса. Если указатель недействителен, управление будет передано в "doSomething", а потом за скобками. Просто не используйте ничего, что требует деструкторов, таких как CString.


в Unix вы должны иметь возможность использовать ядро syscall, которое проверяет указатель и возвращает 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

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