Как разыменование нулевого указателя в C не может привести к сбою программы? - PullRequest
13 голосов
/ 26 августа 2009

Мне нужна помощь настоящего гуру 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 check ".

После этой проверки я выделяю новую память, вызывая malloc. Не уверен, что именно здесь делает malloc, но каждый вызов malloc или free может иметь далеко идущие последствия для всей динамической памяти виртуального адресного пространства процесса. После вызова malloc указатель фактически равен NULL. Так или иначе, malloc (или какой-то системный вызов, используемый malloc) обнуляет уже освобожденную память, в которой находится сам указатель (а не данные, на которые он указывает, сам указатель находится в динамической памяти). Обнуляя эту память, указатель теперь имеет значение 0x0, которое равно NULL в моей системе, и когда вызывается strlcpy, он, конечно, падает.

Так что настоящая ошибка, вызывающая это странное поведение, была в совершенно другом месте в моем коде. Никогда не забывайте: свободная память сохраняет свои ценности, но как долго вы не можете это контролировать. Чтобы проверить, есть ли в вашем приложении ошибка доступа к уже освобожденной памяти, просто убедитесь, что освобожденная память всегда обнуляется, прежде чем она будет освобождена. В OS X вы можете сделать это, установив переменную среды во время выполнения (не нужно ничего перекомпилировать). Конечно, это немного замедляет работу программы, но вы обнаружите эти ошибки гораздо раньше.

Ответы [ 17 ]

13 голосов
/ 26 августа 2009

Во-первых, разыменование нулевого указателя является неопределенным поведением. Он может разбиться, не разбиться, или установить ваши обои на изображение 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 равно нулю, но информация-> kind.name-> length, скажем, 100. Тогда это утверждение эквивалентно:

if (*(information->kind.name->data + 100) != '\0') {

Который не разыменовывает NULL, а скорее разыменовывает адрес 100. Если это не приводит к сбою, а адрес 100 содержит 0, то этот тест пройдет.

11 голосов
/ 26 августа 2009

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

Вы можете попробовать запустить вашу программу под проверкой памяти. Одной из проверок памяти, которая поддерживает Mac OS X, является valgrind , хотя она поддерживает Mac OS X только на Intel, но не на PowerPC.

5 голосов
/ 26 августа 2009

Насколько я знаю, эффект разыменования нулевого указателя не определен стандартом.

Согласно стандарту C 6.5.3.2/4:

Если указателю присвоено недопустимое значение, поведение унарного оператора * не определено.

Так что может произойти сбой или не может быть.

3 голосов
/ 26 августа 2009

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

2 голосов
/ 26 августа 2009

Моя теория состоит в том, что information->kind.name->length является очень большим значением, так что information->kind.name->data[information->kind.name->length] фактически ссылается на действительный адрес памяти.

1 голос
/ 26 августа 2009

Я бы запустил вашу программу под 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; 
    }

и т. Д.

1 голос
/ 26 августа 2009

Отсутствует '{' после последнего оператора if означает, что что-то в разделе "// ... код выше пропущено, не имеет значения ..." контролирует доступ ко всему этому фрагменту кода. Из всего вставленного кода выполняется только strlcpy. Решение: никогда не используйте операторы if без фигурных скобок для пояснения контроля.

Учтите это ...

if(false)
{
    if(something == stuff)
    {
        doStuff();

    .. snip ..

    if(monkey == blah)
        some->garbage= nothing;
        return -1;
    }
}
crash();

Только "crash ();" исполняется.

1 голос
/ 26 августа 2009

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

if (information->kind.name->data[information->kind.name->length] != '\0') {

Скажите, информация-> kind.name-> длина велика. По крайней мере, больше, чем 4096, на конкретной платформе с конкретным компилятором (скажем, большинство * nixes со стандартным компилятором gcc) код будет приводить к чтению из памяти "адреса вида kind.name-> data + information-> kind.name-> length ].

На более низком уровне это чтение - «чтение памяти по адресу (0 + 8653)» (или любой другой длины). В * nixes обычно помечают первую страницу в адресном пространстве как «недоступную», что означает, что разыменование пустого указателя, считывающего адрес памяти от 0 до 4096, приведет к распространению аппаратной ловушки в приложение и ее аварийному завершению.

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

1 голос
/ 26 августа 2009

Меня интересует тип char * в вызове strlcpy.

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

Пример:

int a = 0xffff0000;
short b = (short) a; //b could be 0 if lower bits are used

Редактировать : исправлены орфографические ошибки.

1 голос
/ 26 августа 2009

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

...