Как разыменование нулевого указателя в 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 ]

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

Как к сведению, когда я вижу эту строку:

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

Я вижу до трех разных разыменований указателя:

  1. информация
  2. имя
  3. данные (если это указатель, а не фиксированный массив)

Вы проверяете информацию на ненулевое, но не имя и не данные. Почему вы так уверены, что они правы?

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

Видел, что вы на Mac - игнорируйте комментарий gflags - он может помочь кому-то еще, кто читает это. Если вы работаете на чем-то более раннем, чем OS X, есть несколько удобных инструментов Macsbugs, которые помогают подчеркнуть кучу (например, команда heap scramble, 'hs').

0 голосов
/ 16 июля 2012

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

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

* результат = malloc (realLength); // ???

Адрес вновь выделенного сегмента памяти сохраняется в местоположении, указанном в адресе, содержащемся в переменной «result».

Это намерение? Если это так, возможно, потребуется изменить strlcpy.

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

Вы всегда должны проверять, имеет ли информация-> 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.

0 голосов
/ 26 августа 2009
char * p = NULL;

p [i] похоже на

p += i;

, которая является допустимой операцией даже для нулевого указателя. затем он указывает на ячейку памяти 0x0000 [...] i

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

Несмотря на то, что разыменование нулевого указателя приводит к неопределенному поведению и не обязательно к краху, вы должны проверить значение information->kind.name->data, а не содержимое information->kind.name->data[1].

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

Ничего себе, это странно. Одна вещь выглядит немного подозрительно для меня, хотя это может не помочь:

Что бы произошло, если бы информация и данные были хорошими указателями (не нулевыми), но information.kind.name был нулевым. Вы не разыменовываете этот указатель до строки strlcpy, поэтому, если он был нулевым, он не может произойти сбой до тех пор. Конечно, раньше чем вы разыменовываете данные [1], чтобы установить их на \ 0, что также должно привести к сбою, но из-за случайности ваша программа может просто иметь доступ на запись к 0x01, но не к 0x00.

Кроме того, я вижу, что вы используете информацию-> name.length в одном месте, но информацию-> kind.name.length в другом, не уверенный, является ли это опечаткой или это желательно.

...