Как разыменование нулевого указателя в C не может привести к сбою программы?
Мне нужна помощь настоящего гуру C для анализа сбоя в моем коде. Не для исправления аварии; Я легко могу это исправить, но прежде чем сделать это, я хотел бы понять, как возможен этот сбой, так как он кажется мне абсолютно невозможным.
Этот сбой происходит только на компьютере клиента, и я не могу воспроизвести его локально (поэтому я не могу просмотреть код с помощью отладчика), так как не могу получить копию базы данных этого пользователя. Моя компания также не позволит мне просто изменить несколько строк в коде и сделать пользовательскую сборку для этого клиента (поэтому я не могу добавить несколько строк printf и заставить его снова выполнить код), и, конечно, у клиента есть сборка без символы отладки. Другими словами, мои способности отладки очень ограничены. Тем не менее, я мог бы зафиксировать аварию и получить отладочную информацию. Однако, когда я смотрю на эту информацию, а затем на код, я не могу понять, как поток программы может когда-либо достичь рассматриваемой строки. Код должен был произойти сбой задолго до того, как попасть в эту строку. Я полностью потерян здесь.
Давайте начнем с соответствующего кода. Это очень маленький код:
// ... code above skipped, not relevant ...
if (data == NULL) return -1;
information = parseData(data);
if (information == NULL) return -1;
/* Check if name has been correctly \0 terminated */
if (information->kind.name->data[information->kind.name->length] != '\0') {
freeParsedData(information);
return -1;
}
/* Copy the name */
realLength = information->kind.name->length + 1;
*result = malloc(realLength);
if (*result == NULL) {
freeParsedData(information);
return -1;
}
strlcpy(*result, (char *)information->kind.name->data, realLength);
// ... code below skipped, not relevant ...
Это уже все. Вылетает в strlcpy. Я могу даже рассказать, как strlcpy действительно вызывается во время выполнения. На самом деле strlcpy вызывается со следующими параметрами:
strlcpy ( 0x341000, 0x0, 0x1 );
Зная это, довольно очевидно, почему происходит сбой strlcpy. Он пытается прочитать один символ из указателя NULL, и это, конечно, приведет к сбою. И поскольку последний параметр имеет значение 1, исходная длина должна была равняться 0. Мой код явно содержит ошибку здесь, он не может проверить, что данные имени имеют значение NULL. Я могу это исправить, нет проблем.
Мой вопрос:
Как этот код может вообще добраться до strlcpy?
Почему этот код не падает в операторе if?
Я попробовал это локально на моей машине:
int main (
int argc,
char ** argv
) {
char * nullString = malloc(10);
free(nullString);
nullString = NULL;
if (nullString[0] != '\0') {
printf("Not terminated\n");
exit(1);
}
printf("Can get past the if-clause\n");
char xxx[10];
strlcpy(xxx, nullString, 1);
return 0;
}
Этот код никогда не проходит оператор if. Он вылетает в операторе if, и это определенно ожидается.
Так может кто-нибудь придумать причину, по которой первый код может пройти этот оператор if без сбоев, если name->data действительно NULL? Это абсолютно загадочно для меня. Это не кажется детерминированным.
Важная дополнительная информация:
Код между двумя комментариями действительно завершен, ничего не пропущено. Кроме того, приложение является однопоточным, поэтому нет другого потока, который мог бы неожиданно изменить любую память в фоновом режиме. Платформой, на которой это происходит, является процессор PPC (G4, на случай, если он может сыграть какую-либо роль). И в случае, если кто-то задается вопросом о "kind.", Это потому, что "information" содержит "union" с именем "kind", а name снова является структурой (kind - это union, каждое возможное значение union - это другой тип структуры); но это все не должно иметь значения здесь.
Я благодарен за любую идею здесь. Я еще более благодарен, если это не просто теория, но если есть способ, которым я могу проверить, действительно ли эта теория верна для клиента.
Решение
Я уже принял правильный ответ, но на тот случай, если кто-нибудь найдет этот вопрос в Google, вот что действительно произошло:
Указатели указывали на память, которая уже была освобождена. Освобождение памяти не приведет к нулю или к тому, что процесс сразу же вернет ее системе. Таким образом, хотя память была ошибочно освобождена, она содержала правильные значения. Указанный указатель не равен NULL во время выполнения проверки " if ".
После этой проверки я выделяю новую память, вызывая malloc. Не уверен, что именно здесь делает malloc, но каждый вызов malloc или free может иметь далеко идущие последствия для всей динамической памяти виртуального адресного пространства процесса. После вызова malloc указатель фактически равен NULL. Так или иначе, malloc (или какой-то системный вызов, используемый malloc) обнуляет уже освобожденную память, в которой находится сам указатель (а не данные, на которые он указывает, сам указатель находится в динамической памяти). Обнуляя эту память, указатель теперь имеет значение 0x0, которое равно NULL в моей системе, и когда вызывается strlcpy, он, конечно, падает.
Таким образом, настоящая ошибка, вызывающая это странное поведение, была в совершенно другом месте в моем коде. Никогда не забывайте: свободная память сохраняет свои ценности, но как долго вы не можете это контролировать. Чтобы проверить, есть ли в вашем приложении ошибка доступа к уже освобожденной памяти, просто убедитесь, что освобожденная память всегда обнуляется, прежде чем она будет освобождена. В OS X вы можете сделать это, установив переменную среды во время выполнения (не нужно ничего перекомпилировать). Конечно, это немного замедляет работу программы, но вы обнаружите эти ошибки гораздо раньше.
17 ответов
Возможно, что структура находится в памяти, которая была free()
или куча повреждена. В таком случае, malloc()
мог бы изменить память, думая, что это бесплатно.
Вы можете попробовать запустить вашу программу под контролем памяти. Valgrind поддерживает одну проверку памяти, которая поддерживает Mac OS X, хотя Mac OS X поддерживает только Intel, а не PowerPC.
Во-первых, разыменование нулевого указателя является неопределенным поведением. Он может разбиться, не разбиться, или установить ваши обои на изображение SpongeBob Squarepants.
Тем не менее, разыменование нулевого указателя обычно приводит к сбою. Таким образом, ваша проблема, вероятно, связана с повреждением памяти, например, из-за того, что вы написали после конца одной из ваших строк. Это может вызвать сбой с задержкой. Я особенно подозрительна, потому что маловероятно, что malloc(1)
потерпит неудачу, если ваша программа не столкнется с концом доступной виртуальной памяти, и вы, вероятно, заметите, если бы это было так.
Изменить: OP указал, что это не результат, который является нулевым, но information->kind.name->data
, Вот потенциальная проблема тогда:
Там нет проверки для того, information->kind.name->data
нулевой. Единственная проверка на это
if (information->kind.name->data[information->kind.name->length] != '\0') {
Давайте предположим, что information->kind.name->data
имеет значение null, но информация->kind.name->length равна, скажем, 100. Тогда это утверждение эквивалентно:
if (*(information->kind.name->data + 100) != '\0') {
Который не разыменовывает NULL, а разыменовывает адрес 100. Если это не приводит к сбою, и адрес 100, как оказалось, содержит 0, то этот тест пройдет.
Насколько я знаю, эффект разыменования нулевого указателя не определен стандартом.
Согласно стандарту C 6.5.3.2/4:
Если указателю было присвоено недопустимое значение, поведение унарного оператора * не определено.
Так что может произойти сбой или не может быть.
Возможно, вы испытываете повреждение стека. Строка кода, на которую вы ссылаетесь, может не выполняться вообще.
Моя теория такова, что information->kind.name->length
это очень большое значение, так что information->kind.name->data[information->kind.name->length]
фактически ссылается на действительный адрес памяти.
Отсутствие "{" после последнего оператора if означает, что что-то в разделе "// ... выше кода пропущено, не имеет значения..." контролирует доступ ко всему этому фрагменту кода. Из всего вставленного кода выполняется только strlcpy. Решение: никогда не используйте операторы if без фигурных скобок, чтобы уточнить контроль.
Учти это...
if(false)
{
if(something == stuff)
{
doStuff();
.. snip ..
if(monkey == blah)
some->garbage= nothing;
return -1;
}
}
crash();
Только "crash();" исполняется.
Я бы запустил вашу программу под Valgrind. Вы уже знаете, что есть проблема с указателями NULL, так что профилируйте этот код.
Преимущество valgrind здесь состоит в том, что он проверяет каждую ссылку на указатель и проверяет, было ли это место в памяти ранее объявлено, и сообщит вам номер строки, структуру и все остальное, что вы хотите знать о памяти.
Как уже упоминалось, ссылка на ячейку памяти 0 - это своего рода "que sera, sera".
Мое легкое чувство пауков говорит мне, что ты должен разорвать эти структурные прогулки на
if (information->kind.name->data[information->kind.name->length] != '\0') {
линия как
if (information == NULL) {
return -1;
}
if (information->kind == NULL) {
return -1;
}
и так далее.
Стандарт разыменования NULL-указателя не определен. Не гарантируется сбой, и часто это не произойдет, если вы на самом деле не попытаетесь записать в память.
Как к сведению, когда я вижу эту строку:
if (information->kind.name->data[information->kind.name->length] != '\0') {
Я вижу до трех разных указателей разыменования:
- Информация
- название
- данные (если это указатель, а не фиксированный массив)
Вы проверяете информацию на ненулевое, но не имя и не данные. Почему ты так уверен, что они правы?
Я также повторяю другие чувства здесь о чем-то еще, что может повредить вашу кучу ранее. Если вы работаете в Windows, подумайте о том, чтобы использовать gflags для таких вещей, как распределение страниц, которое можно использовать для определения, пишете ли вы или кто-то другой за концом буфера и наступаете на вашу кучу.
Видел, что вы на Mac - игнорируйте комментарий gflags - это может помочь кому-то еще, кто читает это. Если вы работаете на чем-то более раннем, чем OS X, есть несколько удобных инструментов Macsbugs, чтобы подчеркнуть кучу (например, команда heap scramble, 'hs').
Я заинтересован в приведении char* в вызов strlcpy.
Может ли тип данных * отличаться по размеру от символа * в вашей системе? Если указатели на символы меньше, вы можете получить подмножество указателя данных, которое может быть NULL.
Пример:
int a = 0xffff0000;
short b = (short) a; //b could be 0 if lower bits are used
Изменить: Исправлены орфографические ошибки.
Вот один конкретный способ, которым вы можете обойти указатель 'data', будучи NULL в
if (information->kind.name->data[information->kind.name->length] != '\0') {
Скажите информация->kind.name-> длина велика. По крайней мере, больше 4096, на конкретной платформе с определенным компилятором (скажем, большинство *nixes со стандартным компилятором gcc) код будет приводить к чтению из памяти "адреса вида kind.name->data + information->kind.name-> длина].
На более низком уровне это чтение - "чтение памяти по адресу (0 + 8653)" (или любой другой длины). В *nixes обычно помечают первую страницу в адресном пространстве как "недоступную", что означает, что разыменование пустого указателя, считывающего адрес памяти от 0 до 4096, приведет к распространению аппаратной ловушки в приложение и ее аварийному завершению.
Читая эту первую страницу, вы можете столкнуться с действительной отображенной памятью, например, с разделяемой библиотекой или с чем-то еще, что произошло там - и доступ к памяти не будет невозможен. И это нормально. Разыменование NULL-указателя является неопределенным поведением, ничто не требует его сбоя.
Вы всегда должны проверять, имеет ли информация->kind.name->data значение null, но в этом случае
в
if (*result == NULL)
freeParsedData(information);
return -1;
}
Вы пропустили {
так должно быть
if (*result == NULL)
{
freeParsedData(information);
return -1;
}
Это хорошая причина для этого стиля кодирования, а не
if (*result == NULL) {
freeParsedData(information);
return -1;
}
где вы можете не заметить отсутствующую фигурную скобку, потому что вы привыкли к форме блока кода без фигурной скобки, отделяющей его от предложения if.
Несмотря на то, что разыменование нулевого указателя приводит к неопределенному поведению и не обязательно к краху, вы должны проверить значение information->kind.name->data
а не содержимое information->kind.name->data[1]
,
char * p = NULL;
р [я] как
p += i;
которая является допустимой операцией, даже для нулевого указателя. затем он указывает на ячейку памяти 0x0000[...] я
* результат = malloc(realLength); //???
Адрес вновь выделенного сегмента памяти сохраняется в местоположении, на которое ссылается адрес, содержащийся в переменной "result".
Это намерение? Если это так, strlcpy может потребоваться модификация.
Вау, это странно. Одна вещь выглядит немного подозрительно для меня, хотя это может не способствовать:
Что бы произошло, если бы информация и данные были хорошими указателями (не нулевыми), а information.kind.name был нулевым. Вы не разыменовываете этот указатель до строки strlcpy, поэтому, если он был нулевым, он не может произойти сбой до тех пор. Конечно, раньше, чем вы делаете разыменование данных [1], установите их в \0, что также должно привести к сбою, но из-за случайности ваша программа может просто получить доступ к записи 0x01, но не 0x00.
Кроме того, я вижу, что вы используете информацию->name.length в одном месте, но информацию->kind.name.length в другом, не уверенный, если это опечатка или если это желательно.
Насколько я понимаю, частным случаем этой проблемы является недопустимый доступ, приводящий к попытке чтения или записи с использованием нулевого указателя. Здесь обнаружение проблемы очень сильно зависит от оборудования. На некоторых платформах доступ к памяти для чтения или записи с использованием указателя NULL приведет к исключению.