Как можно разыменовать NULL-указатель в 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-statement?

Я попробовал его локально на своей машине:

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-statement без сбоев, если name- > data действительно NULL? Это совершенно таинственно для меня. Это не кажется детерминированным.

Важная дополнительная информация:
Код между двумя комментариями действительно завершен, ничего не осталось. Далее приложение однопоточное, поэтому нет другого потока, который мог бы неожиданно изменить любую память в фоновом режиме. Платформа, где это происходит, - это процессор PPC (G4, в случае, если он может играть какую-либо роль). И в случае, если кто-то задается вопросом о "добром". Это связано с тем, что "информация" содержит "союз" с именем "вид", а имя снова является структурой (вид - это объединение, каждое возможное значение объединения - это другой тип структуры); но здесь все это не имеет никакого значения.

Я благодарен за любую идею здесь. Я еще более благодарен, если это не просто теория, но если есть способ, я могу проверить, что эта теория действительно справедлива для клиента.

Решение

Я уже принял правильный ответ, но на всякий случай кто-то найдет этот вопрос в Google, вот что на самом деле произошло:

Указатели указывали на память, которая уже была освобождена. Освобождение памяти не приведет к нулю или приведет к возврату системы в систему сразу. Поэтому, хотя память была ошибочно освобождена, она содержала правильные значения. Указанный указатель не является NULL во время выполнения "if check".

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

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

Ответы

Ответ 1

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

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

Ответ 2

Во-первых, разыменование нулевого указателя - это поведение undefined. Это может привести к сбою, а не сбою, или установить обои на картинку SpongeBob Squarepants.

Тем не менее разыменование нулевого указателя обычно приводит к сбою. Таким образом, ваша проблема, вероятно, связана с повреждением памяти, например. от написания до конца одной из ваших строк. Это может привести к сбою с задержкой. Я особенно подозрительно, потому что очень маловероятно, что malloc(1) завершится неудачно, если ваша программа не будет закрыта до конца доступной виртуальной памяти, и вы, вероятно, заметили бы, если бы это было так.

Edit: OP указал, что это не результат, но null information->kind.name->data. Здесь возникает потенциальная проблема:

Нет проверки, имеет ли значение information->kind.name->data значение null. Единственная проверка на это

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

Предположим, что information->kind.name->data равно null, но information- > kind.name- > length - это, скажем, 100. Тогда это утверждение эквивалентно:

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

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

Ответ 3

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

В соответствии со стандартом C 6.5.3.2/4:

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

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

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

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

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

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

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

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

Ответ 8

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

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

Пример:

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

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

Ответ 9

Здесь один конкретный способ, которым вы можете пропустить указатель "data", который имеет NULL в

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

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

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

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

Ответ 10

Отсутствует '{' after last if statement означает, что что-то в разделе "//... code over skipped, not important..." контролирует доступ ко всему фрагменту кода. Из всего вложенного кода выполняется только strlcpy. Решение: никогда не использовать инструкции без фигурных скобок для уточнения контроля.

Рассмотрим это...

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

    .. snip ..

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

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

Ответ 11

Я буду запускать вашу программу под valgrind. Вы уже знаете, что есть проблема с указателями NULL, поэтому профиль этого кода.

Преимущество, заключающееся в том, что существа valgrind здесь, что он проверяет каждую ссылку на указатель и проверяет, была ли ранее объявлена ​​эта ячейка памяти, и она сообщит вам номер строки, структуру и все остальное, что вам нужно знать о памяти.

Как упоминалось выше, ссылка на ячейку памяти 0 является "любопытной вещью" SER SERA SERA ".

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

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

как

    if (information == NULL) {
      return -1; 
    }
    if (information->kind == NULL) {
      return -1; 
    }

и т.д.

Ответ 12

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

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

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

Ответ 13

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

Ответ 14

char * p = NULL;

p [i] похож на

p += i;

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

Ответ 15

Вы всегда должны проверить, равнозначно ли data- > 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.

Ответ 16

* result = malloc (realLength);//???

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

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

Ответ 17

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