Сколько битов игнорировать при проверке на NULL? - PullRequest
0 голосов
/ 15 сентября 2010

Следующие сбои с seg-V:

// my code
int* ipt;
int bool set = false;
void Set(int* i) {
  ASSERT(i);
  ipt = i;
  set = true;
}

int Get() {
  return set ? *ipt : 0;
}

// code that I don't control.
struct S { int I, int J; }
int main() {
  S* ip = NULL;
  // code that, as a bug, forgets to set ip...
  Set(&ip->J);
  // gobs of code
  return Get();
}

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

Одним из решений этой проблемы является обрезка младших битов:

void Set(int* i) {
  ASSERT((reinterpret_cast<size_t>(i))>>10);
  ipt = i;
  set = true;
}

Но от скольких бит я должен / могу избавиться?


Редактировать, меня не беспокоит неопределенное поведение, так как я все равно прерву (но более аккуратно, чем seg-v) в этом случае.

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

Вещи, которые можно предположить ради аргумента:

  • Если Set вызывается с чем-то, что вызовет seg-v, это ошибка
  • Set может вызываться кодом, который я не могу исправить. (Например, я сообщаю об ошибке)
  • Набор может быть вызван кодом, который я пытаюсь исправить. (Например, я добавляю проверки работоспособности как часть моей работы по отладке.)
  • Получить мой вызов так, чтобы не было никакой информации о том, куда был вызван Сет. (То есть разрешение Get to seg-v не является эффективным способом отладки чего-либо.)
  • Код не должен быть переносимым или отлавливать 100% неверных указателей. Он должен работать только в моей нынешней системе достаточно часто, чтобы я мог определить, где что-то идет не так.

Ответы [ 8 ]

12 голосов
/ 15 сентября 2010

Не существует переносимого способа проверки любого недопустимого указателя, кроме NULL.Оценка &ip[3] дает неопределенное поведение, прежде чем что-либо делать с ним;Единственное решение состоит в том, чтобы проверить, чтобы NULL перед выполнял арифметику с указателем.

Если вам не нужна переносимость и вам не нужно гарантировать, что вы перехватываете все ошибки,на большинстве основных платформ вы можете проверить, находится ли адрес на первой странице памяти;Обычно NULL определяется как нулевой адрес и резервирует первую страницу для перехвата большинства разыменований нулевого указателя.На платформе POSIX это будет выглядеть примерно так:

static size_t page_size = sysconf(_SC_PAGESIZE);
assert(reinterpret_cast<intptr_t>(i) >= page_size);

Но это не полное решение.Единственное реальное решение - это исправить то, что неправильно использует нулевые указатели.

2 голосов
/ 15 сентября 2010

Вы не должны вообще делать арифметику указателей (включая индексирование массивов) для нулевого указателя.

И вам следует использовать 0, а не NULL в c ++.NULL - это функция c, все еще поддерживаемая, но не идиоматическая в c ++.


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

1 голос
/ 15 сентября 2010

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

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

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

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

Что касается дебатов NULL Vs 0, #define NULL 0 лучше по нескольким причинам:

1) Вы можете легче видеть, когда имеете дело с указателем.

2) Использование NULL обеспечивает не меньшую или большую безопасность, чем использование 0. Так почему бы не сделать ваш код более читабельным?

3) Когда наконец выйдет C ++ 11, #define NULL nullptr будет намного легче изменить, чем все эти нули. (Вы можете пойти другим путем и, наверное, #define nullptr 0 сегодня, но это, вероятно, вызовет проблемы в будущем, если вы будете разрабатывать кроссплатформенный код.)

И для записи, стандарт C ++ явно заявляет, что константа нулевого указателя является целочисленным типом rvalue, который оценивается как ноль. Поэтому, пожалуйста, давайте не будем больше глупости о том, что нулевые указатели не должны равняться нулю.

1 голос
/ 15 сентября 2010

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

int test = *(volatile int *)i;
*(volatile int *)i = test;

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

*(volatile int *)i = *(volatile int *)i;
1 голос
/ 15 сентября 2010

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

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

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

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

1 голос
/ 15 сентября 2010

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

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

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

0 голосов
/ 15 сентября 2010

Вы должны учитывать вашу конкретную операционную систему и аппаратную архитектуру. Если вас интересует только обнаружение указателей, которые «близки к нулю», вы можете использовать ASSERT (i> pageSize), предполагая, что первая страница всегда защищена от записи в вашей ОС.

Но ... очевидный вопрос: зачем? В этом случае ОС обнаружит нулевое значение и SEGV, как вы указали, что так же хорошо, как ASSERT, не так ли?

0 голосов
/ 15 сентября 2010

Одна из многих причин, по которым вы не можете сделать это в переносимом режиме, состоит в том, что NULL не гарантированно равен 0. Только указано, что нулевые указатели будут сравниваться равными 0. Вы можете написать 0 (или макрос препроцессора "NULL") в вашем коде, но компилятор знает, что это 0 находится в контексте указателя, поэтому он генерирует соответствующий код для сравнения его с нулевым указателем, какой бы ни была фактическая реализация нулевого указателя. См. здесь и здесь для получения дополнительной информации об этом. Переинтерпретация указателя NULL как целочисленного типа может привести к тому, что он будет иметь значение true вместо false.

...