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


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

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

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

28 75

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

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

насколько мне известно, нет никакой возможности. Вы должны попытаться избежать этой ситуации, всегда устанавливая указатели на 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" значения до и после данных для проверки допустимого типа данных и переполнения данных.

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

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

вот как я нашел, чтобы сделать это (на 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++ нет положений для проверки правильности указателя в общем случае. Можно, очевидно, предположить, что нуль (от 0x00000000) это плохо, и различных компиляторов и библиотек, как использовать "специальные ценности" здесь и там, чтобы сделать отладку легче (например, если я когда-нибудь увижу указатель как 0xCECECECE в Visual студии я знаю, что я сделал что-то неправильно) но правда в том, что, поскольку указатель-это просто индекс в памяти это практически невозможно сказать, просто взглянув на указатель, если это "правильный" индекс.

существуют различные трюки, которые вы можете сделать с 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, стека или кучи, но не указывая на допустимый объект. Один вариант этого является указатель, который используется для указания на допустимый объект, прежде чем что-то плохое случилось с объектом. Плохие вещи в этом контексте включают освобождение, повреждение памяти или повреждение указателя.

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

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

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

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

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

но я собрал исходные коды 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;  
}  

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

    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(ФД, баф, szie установки) системный вызов может помочь вам сделать волшебство: пусть ФД-файл-дескриптор временного файла, который вы создаете для тестирования, а buf указывает на строку, вы тесинг, если указатель недействителен записать() возвращает -1 и errno устанавливается в EFAULT, который указывает, что buf находится за пределами доступного адресного пространства.

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

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

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

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

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

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

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

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

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

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

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

предположим, что ваш указатель может содержать только три значения -- 0, 1 и -1, где 1 означает допустимый указатель, -1 недопустимый и 0 другой недопустимый. Какова вероятность того, что ваш указатель - это 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 вы должны иметь возможность использовать системный вызов ядра, который выполняет проверку указателя и возвращает 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], так как есть вероятность, что это может привести к фактическому созданию файла codepath и последующему закрытию требования.