Указатели проверки на валидность (C / C ++)

91

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

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

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

noamtm
источник
См. Также stackoverflow.com/questions/496034/…
ChrisW
Я думаю, что лучший положительный ответ о Linux дает Джордж Карретт. Если этого недостаточно, рассмотрите возможность создания таблицы символов функций в библиотеке или даже таблицы доступных библиотек другого уровня с их собственными таблицами функций. Затем сверьтесь с этими точными таблицами. Конечно, эти отрицательные ответы также верны: вы не можете быть на 100% уверены, действителен ли указатель на функцию или нет, если вы не наложите множество дополнительных ограничений на пользовательское приложение.
Минхуа
Действительно ли в спецификации API указывается такое обязательство, которое должна выполнять реализация? Кстати, я делаю вид, что не предполагал, что вы и разработчик, и дизайнер. Я хочу сказать, что я не думаю, что API мог бы указывать что-то вроде «В случае передачи недопустимого указателя в качестве аргумента, функция должна решить проблему и вернуть NULL». API берет на себя обязательство предоставлять услуги при надлежащих условиях использования, а не путем взлома. Тем не менее, быть немного глупо защищенным - не вредно. Использование ссылки делает такие случаи менее разрушительными. :)
Понирос

Ответы:

75

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

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

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

Йоханнес Шауб - litb
источник
Вероятно, это правильный ответ, но я думаю, что простая функция, которая проверяет общие ячейки памяти hexspeak, была бы полезна для общей отладки ... Прямо сейчас у меня есть указатель, который иногда указывает на 0xfeeefeee, и если бы у меня была простая функция, которую я мог бы использовать для перчаточных утверждений вокруг. Было бы намного проще найти виновника ... РЕДАКТИРОВАТЬ: Хотя, я думаю, было бы несложно написать его самостоятельно ..
Кант
@quant проблема заключается в том, что некоторый код C и C ++ может выполнять арифметические операции с указателями на недопустимом адресе без проверки (на основе принципа "мусор на входе и выходе") и, таким образом, будет передавать "арифметически измененный" указатель из одного из них. -известные недопустимые адреса. Распространенные случаи: поиск метода из несуществующей vtable на основе недопустимого адреса объекта или одного из неправильного типа или простое чтение полей из указателя на структуру, которая не указывает на одну.
rwong
Это в основном означает, что вы можете получать только индексы массивов из внешнего мира. API, который должен защищаться от вызывающего, просто не может иметь указателей в интерфейсе. Тем не менее, было бы хорошо иметь макросы для использования в утверждениях о действительности указателей (которые у вас обязательно должны быть внутри). Если указатель гарантированно указывает внутри массива, начальная точка и длина которого известны, это можно проверить явно. Лучше умереть от нарушения assert (документированная ошибка), чем от deref (недокументированная ошибка).
Роб
34

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

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

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

Джордж Карретт
источник
13
Первый ответ, который не пытается быть моральным по отношению к самому вопросу и на самом деле идеально отвечает на него. Люди иногда не понимают, что такой подход к отладке действительно нужен для поиска ошибок, например, в сторонних библиотеках или в устаревшем коде, потому что даже valgrind находит дикие указатели только при фактическом доступе к ним, а не, например, если вы хотите регулярно проверять указатели на достоверность в таблице кеша, которая была перезаписана из другого места в вашем коде ...
lumpidu
Это должен быть принятый ответ. Я сделал это аналогично на платформе, отличной от Linux. По сути, это раскрытие информации о процессе самому процессу. С этим аспектом кажется, что окна работают лучше, чем Linux, предоставляя более значимую информацию через API статуса процесса.
Минхуа
31

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

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

Nailer
источник
8
В некоторых случаях , хотя, проверка на плохой указатель сразу , когда API называется это , как вы не рано. Например, что, если API сохранит указатель в структуре данных, где он будет использоваться только позже? Затем передача API плохого указателя вызовет сбой в какой-то случайный момент позже. В этом случае было бы лучше выйти из строя раньше, при вызове API, где изначально было введено неверное значение.
peterflynn
28

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

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

Короче, не делайте этого;)

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

ДжаредПар
источник
3
@Tim, в C ++ нет возможности сделать это.
JaredPar
6
Это только «правильный ответ», если вы определяете «действительный указатель» как «не вызывает нарушения прав доступа / segfault». Я бы предпочел определить его как «указывает на значимые данные, выделенные для той цели, для которой вы собираетесь их использовать». Я бы сказал, что это лучшее определение действительности указателя ...;)
jalf
Даже если указатель действителен, это невозможно проверить. Подумайте о thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {сна (1); удалить p; ...}
Кристофер
2
@ Кристофер, это правда. Я должен был сказать: «Я могу прочесть это конкретное место в памяти в то время, которое уже прошло»
JaredPar
@JaredPar: действительно плохое предложение. Может запускать охранную страницу, поэтому стек не будет расширяться позже или что-то подобное.
Дедупликатор
16

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

Фердинанд Бейер
источник
4
Установка указателя на null ничего не дает, кроме, может быть, ложного чувства безопасности.
Это неправда. В частности, в C ++ вы можете определить, следует ли удалять объекты-члены, проверив значение null. Также обратите внимание, что в C ++ допустимо удалять нулевые указатели, поэтому популярно безоговорочное удаление объектов в деструкторах.
Фердинанд Бейер,
4
int * p = новый int (0); int * p2 = p; удалить p; p = NULL; удалить p2; // авария
1
забзонк, а ?? он сказал, что вы можете удалить нулевой указатель. p2 не является нулевым указателем, но является недопустимым указателем. вы должны установить для него значение null раньше.
Йоханнес Шауб - лит,
2
Если у вас есть псевдонимы для указанной памяти, только один из них будет иметь значение NULL, другие псевдонимы будут болтаться.
jdehaan
7

Взгляните на этот и этот вопрос. Также обратите внимание на умные указатели .

Tunnuz
источник
7

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

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 вместо основного процесса.

Фредрик
источник
Возможно, другой способ - использовать _CrtIsValidHeapPointer . Эта функция вернет TRUE, если указатель действителен, и выдаст исключение, когда указатель будет освобожден. Как указано в документации, эта функция доступна только в CRT отладки.
Crend King
6

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

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

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

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

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

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

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

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

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

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

Щуп
источник
5

В 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 () [возможно, доступ], поскольку есть вероятность, что это может привести к фактическому кодовому пути создания файла и последующему требованию закрытия.

Пеэтер Джут
источник
1
Это блестящий взлом. Я хотел бы увидеть советы по различным системным вызовам для проверки диапазонов памяти, особенно если они гарантированно не будут иметь побочных эффектов. Вы можете оставить дескриптор файла открытым для записи в / dev / null, чтобы проверить, находятся ли буферы в читаемой памяти, но, вероятно, есть более простые решения. Лучшее, что я могу найти, - это символическая ссылка (ptr, ""), которая установит для errno значение 14 для плохого адреса или 2 для хорошего адреса, но изменения ядра могут поменять порядок проверки.
Престон
1
@Preston В DB2, я думаю, мы использовали access () из unistd.h. Я использовал open () выше, потому что он немного менее неясен, но вы, вероятно, правы, что есть много возможных системных вызовов. Раньше в Windows был API явной проверки указателя, но он оказался небезопасным для потоков (я думаю, он использовал SEH, чтобы попытаться записать, а затем восстановить границы диапазона памяти.)
Пеэтер Джут,
4

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

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

Вот как я это сделал (в 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);
}

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

Майк Сэдлер
источник
Не используйте exit(), он обходит RAII и может вызвать утечку ресурсов.
Себастьян Мах,
Интересно - есть ли другой способ аккуратного завершения в этой ситуации? И является ли оператор exit единственной проблемой в том, чтобы сделать это таким образом? Я заметил, что получил «-1» - это только из-за «выхода»?
Майк Сэдлер,
Ой, я понимаю, что это довольно исключительная ситуация. Я только что увидел, exit()и мой Portable C ++ Alarm Bell начал звонить. Это должно быть нормально в этой конкретной ситуации Linux, когда ваша программа все равно завершится, извините за шум.
Себастьян Мах
1
signal (2) не переносится. Используйте sigaction (2). man 2 signalв Linux есть параграф, объясняющий почему.
rptb1
1
В этой ситуации я обычно вызываю abort (3), а не exit (3), потому что это с большей вероятностью приведет к некоторой обратной трассировке отладки, которую вы можете использовать для диагностики проблемы посмертно. В большинстве Unixen команда abort (3) сбрасывает дамп ядра (если дамп ядра разрешен), а в Windows он предлагает запустить отладчик, если он установлен.
rptb1
2

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

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

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

Тодзи
источник
Константа нулевого указателя в C ++ (или C, если на то пошло) представлена ​​постоянным целым нулем. Во многих реализациях для его представления используются все двоичные нули, но на это не стоит рассчитывать.
Дэвид Торнли
2

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


источник
2

Установка указателя на 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);
}
Тим Ринг
источник
Обновленный вопрос, конечно, делает мой ответ неправильным (или, по крайней мере, ответом на другой вопрос). Я согласен с ответами, которые в основном гласят: пусть они рухнут, если они злоупотребляют th api. Невозможно остановить людей, которые бьют себя молотком по большому пальцу ...
Тим Ринг
2

Принимать произвольные указатели в качестве входных параметров в общедоступном 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)
}
Даниэль Даранас
источник
re: «передать простой тип данных ... например, строку» Но в C ++ строки чаще всего передаются как указатели на символы (char *) или (const char *), поэтому вы снова возвращаетесь к передаче указателей. И ваш пример передает in_person как ссылку, а не как указатель, поэтому сравнение (in_person! = NULL) подразумевает, что есть некоторые сравнения объектов / указателей, определенные в классе Person.
Джесси Чизхолм
@JesseChisholm Под строкой я имел в виду строку, то есть std :: string. Я ни в коем случае не рекомендую использовать char * как способ хранения строк или их передачи. Не делай этого.
Даниэль Даранас,
@JesseChisholm По какой-то причине я ошибся, отвечая на этот вопрос пять лет назад. Ясно, что нет смысла проверять, является ли Person & NULL. Это даже не компилируется. Я имел в виду использовать указатели, а не ссылки. Исправил сейчас.
Даниэль Даранас
1

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

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

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

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

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

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

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

Джон Григгс
источник
re: "выложить хорошую диагностическую информацию", вот и загвоздка. Поскольку вы не можете проверить правильность указателя, информация, о которой вам нужно беспокоиться, минимальна. «Здесь произошло исключение», - может быть, все, что вы получите. Весь стек вызовов хорош, но требует более совершенной структуры, чем предоставляет большинство библиотек времени выполнения C ++.
Джесси Чизхолм
1

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

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

Том
источник
1

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

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

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

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetect%v&rt&rt&obl=deppsdrc_ddc&rt&hl=ru&obddd_db_db_ddc&b_db_db_db_db_d_d_db_d_db_d_d_dd_d_d_dd_d_d_d_d_d_d_d_d_d_d_d_db_d_b_d_0_db_d_b_d_db_d_d_d_d_d_ddd = 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 ++.

TarmoPikaro
источник
1

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

Венлинь Ву
источник
1

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

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

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

Андрей Калантарян
источник
1

В Windows я использую этот код:

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

Применение:

unsigned long * pTest = (unsigned long *) 0x12345;
CheckPointer(pTest, "pTest"); //throws exception
Артков
источник
0

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

pngaz
источник
3
вам следует избегать этих методов, потому что они не работают. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx
JaredPar,
Иногда они могут быть
обходными путями,
0

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

Sebnow
источник
К сожалению, это не помогает для объектов, размещенных в стеке.
Том
0

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

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

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

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

б) память по этому адресу является начальным адресом объекта (например, адрес не в середине огромного массива)

в) память по этому адресу начинается адресом объекта ожидаемого типа

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


источник
0

Дополнение к аккредитованному ответу (-ам):

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

ласково
источник
0

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

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

дикрокосия
источник
0

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

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

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

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

TarmoPikaro
источник
0

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

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

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

Хачатур
источник