it-swarm.com.ru

Нужно ли устанавливать указатели на `NULL` после их освобождения?

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

Избегайте сбоев при двойном освобождении указателей.

Short: вызов free() во второй раз, случайно, не дает сбоя, если для него установлено значение NULL.

  • Почти всегда это маскирует логическую ошибку, потому что нет причины вызывать free() во второй раз. Безопаснее позволить приложению аварийно завершить работу и исправить его.

  • Не гарантируется сбой, потому что иногда новая память выделяется по тому же адресу.

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

Логические ошибки также могут привести к повреждению данных.

Избегайте повторного использования освобожденных указателей

Short: доступ к освобожденным указателям может привести к повреждению данных, если malloc() выделяет память в том же месте, если для освобожденного указателя не установлено значение NULL

  • Нет гарантии, что программа выйдет из строя при доступе к указателю NULL, если смещение достаточно велико (someStruct->lastMember, theArray[someBigNumber]). Вместо сбоев произойдет повреждение данных.

  • Установка указателя на NULL не может решить проблему наличия другого указателя с тем же значением указателя.

Вопросы

Вот пост против слепой установки указателя на NULL после освобождения .

  • Какой из них сложнее отладить?
  • Есть ли возможность поймать оба?
  • Насколько вероятно, что такие ошибки приводят к повреждению данных, а не к сбою?

Не стесняйтесь расширять этот вопрос.

48
Georg Schölly

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

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

В моем собственном коде я вызываю только свою собственную функцию, которая является оберткой вокруг free (). Он принимает указатель на указатель и обнуляет указатель после освобождения памяти. И прежде чем он вызывает free, он вызывает Assert(p != NULL);, поэтому он все еще ловит попытки двойного освобождения одного и того же указателя.

Мой код также выполняет другие функции, такие как (только в сборке DEBUG) заполнение памяти очевидным значением сразу после его выделения, выполнение того же самого действия перед вызовом free() в случае наличия копии указателя и т.д. Подробности здесь ,

Правка: по запросу, вот пример кода.

void
FreeAnything(void **pp)
{
    void *p;

    AssertWithMessage(pp != NULL, "need pointer-to-pointer, got null value");
    if (!pp)
        return;

    p = *pp;
    AssertWithMessage(p != NULL, "attempt to free a null pointer");
    if (!p)
        return;

    free(p);
    *pp = NULL;
}


// FOO is a typedef for a struct type
void
FreeInstanceOfFoo(FOO **pp)
{
    FOO *p;

    AssertWithMessage(pp != NULL, "need pointer-to-pointer, got null value");
    if (!pp)
        return;

    p = *pp;
    AssertWithMessage(p != NULL, "attempt to free a null FOO pointer");
    if (!p)
        return;

    AssertWithMessage(p->signature == FOO_SIG, "bad signature... is this really a FOO instance?");

    // free resources held by FOO instance
    if (p->storage_buffer)
        FreeAnything(&p->storage_buffer);
    if (p->other_resource)
        FreeAnything(&p->other_resource);

    // free FOO instance itself
    free(p);
    *pp = NULL;
}

Комментарии:

Вы можете видеть во второй функции, что мне нужно проверить два указателя ресурса, чтобы увидеть, не являются ли они нулевыми, и затем вызвать FreeAnything(). Это из-за assert(), которая будет жаловаться на нулевой указатель. У меня есть это утверждение для того, чтобы обнаружить попытку двойного освобождения, но я не думаю, что на самом деле это принесло мне много ошибок; если вы хотите опустить подтверждения, вы можете пропустить проверку и просто всегда вызывать FreeAnything(). Кроме утверждения, ничего плохого не происходит, когда вы пытаетесь освободить нулевой указатель с помощью FreeAnything(), потому что он проверяет указатель и просто возвращает, если он уже был нулевым.

Мои действительные имена функций более кратки, но я попытался выбрать самодокументированные имена для этого примера. Кроме того, в моем реальном коде у меня есть код только для отладки, который заполняет буферы значением 0xDC перед вызовом free(), так что если у меня есть дополнительный указатель на ту же самую память (ту, которая не обнуляется), становится действительно очевидным, что данные, на которые он указывает, являются фиктивными. У меня есть макрос, DEBUG_ONLY(), который ничего не компилирует в не отладочной сборке; и макрос FILL(), который выполняет sizeof() над структурой. Эти два работают одинаково хорошо: sizeof(FOO) или sizeof(*pfoo). Итак, вот макрос FILL():

#define FILL(p, b) \
    (memset((p), b, sizeof(*(p)))

Вот пример использования FILL() для помещения значений 0xDC перед вызовом:

if (p->storage_buffer)
{
    DEBUG_ONLY(FILL(pfoo->storage_buffer, 0xDC);)
    FreeAnything(&p->storage_buffer);
}

Пример использования этого:

PFOO pfoo = ConstructNewInstanceOfFoo(arg0, arg1, arg2);
DoSomethingWithFooInstance(pfoo);
FreeInstanceOfFoo(&pfoo);
assert(pfoo == NULL); // FreeInstanceOfFoo() nulled the pointer so this never fires
25
steveha

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

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

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

В первых двух случаях установка указателя на NULL кажется мне занятой работой без особой цели:

int doSomework() {
    char *working_space = malloc(400*1000);
    // lots of work
    free(working_space);
    working_space = NULL; // wtf? In case someone has a reference to my stack?
    return result;
}

int doSomework2() {
    char * const working_space = malloc(400*1000);
    // lots of work
    free(working_space);
    working_space = NULL; // doesn't even compile, bad luck
    return result;
}

void freeTree(node_type *node) {
    for (int i = 0; i < node->numchildren; ++i) {
        freeTree(node->children[i]);
        node->children[i] = NULL; // stop wasting my time with this rubbish
    }
    free(node->children);
    node->children = NULL; // who even still has a pointer to node?

    // Should we do node->numchildren = 0 too, to keep
    // our non-existent struct in a consistent state?
    // After all, numchildren could be big enough
    // to make NULL[numchildren-1] dereferencable,
    // in which case we won't get our vital crash.

    // But if we do set numchildren = 0, then we won't
    // catch people iterating over our children after we're freed,
    // because they won't ever dereference children.

    // Apparently we're doomed. Maybe we should just not use
    // objects after they're freed? Seems extreme!
    free(node);
}

int replace(type **thing, size_t size) {
    type *newthing = copyAndExpand(*thing, size);
    if (newthing == NULL) return -1;
    free(*thing);
    *thing = NULL; // seriously? Always NULL after freeing?
    *thing = newthing;
    return 0;
}

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

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

if (thing->cached != NULL) {
    assert(thing->cached != NULL);
    free(thing->cached);
    thing->cached = NULL;
}
free(thing);

Этот код говорит вам, что вы зашли слишком далеко. Так должно быть:

free(thing->cached);
free(thing);

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

free(thing->cached);
thing->cached = (void*)(0xFEFEFEFE);

Если вы не можете найти такую ​​константу в своей системе, вы можете выделить нечитаемую и/или нечитаемую страницу и использовать ее адрес.

7
Steve Jessop

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

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

Вывод: я бы пошел для установки указателя на NULL.

3
Kosi2801

Ответ зависит от (1) размера проекта, (2) ожидаемого времени жизни вашего кода, (3) размера команды . В небольшом проекте с коротким временем жизни вы можете пропустить установку указателей на NULL и просто выполнить отладку.

В большом долгоживущем проекте есть веские причины для установки указателей на NULL: (1) Защитное программирование всегда хорошо. Ваш код может быть в порядке, но новичок по-прежнему может бороться с указателями (2) Я лично считаю, что все переменные должны всегда содержать только допустимые значения. После удаления/освобождения указатель больше не является допустимым значением, поэтому его необходимо удалить из этой переменной. Замена его на NULL (единственное значение указателя, которое всегда допустимо) - хороший шаг ... (3). Код никогда не умирает. Он всегда используется повторно, и часто способами, которые вы не представляли в то время, когда писали это. Ваш сегмент кода может в конечном итоге быть скомпилирован в контексте C++ и, вероятно, перемещен в деструктор или метод, который вызывается деструктором. Взаимодействия виртуальных методов и объектов, которые находятся в процессе разрушения, являются тонкими ловушками даже для очень опытных программистов ... (4) Если ваш код заканчивается использованием в многопоточном контексте, какой-то другой поток может прочитать это переменная и попробуйте получить к нему доступ. Такие контексты часто возникают, когда унаследованный код переносится и повторно используется на веб-сервере. Таким образом, еще лучший способ освобождения памяти (с параноидальной точки зрения) - (1) скопировать указатель на локальную переменную, (2) установить исходную переменную в NULL, (3) удалить/освободить локальную переменную. 

3
Carsten Kuckuk

Если указатель будет использоваться повторно, после использования его следует установить обратно в 0(NULL), даже если объект, на который он указывал, не освобождается из кучи. Это позволяет проводить корректную проверку на NULL, например, если (p) {// сделать что-то}. Кроме того, только то, что вы освобождаете объект, на адрес которого указывает указатель, не означает, что указатель устанавливается в 0 после вызова ключевого слова delete или функции free вообще. 

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

Если указатель является членом (структура или класс), вы должны установить его в NULL после освобождения объекта или объектов по двойному указателю снова для правильной проверки против NULL. 

Это поможет вам избавиться от головной боли от неверных указателей, таких как «0xcdcd ...» и так далее. Таким образом, если указатель равен 0, то вы знаете, что он не указывает на адрес, и можете убедиться, что объект освобожден из кучи.

2
bvrwoo_3376

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

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

  • всегда инициализировать указатели - установите их в NULL или какой-либо действительный адрес
  • после вызова free () установите указатель на NULL
  • проверьте все указатели, которые могут быть NULL для фактического значения NULL, перед разыменованием их.
1
sharptooth

В C++ можно поймать как реализацию собственного умного указателя (или производную от существующих реализаций), так и реализацию чего-то вроде:

void release() {
    assert(m_pt!=NULL);
    T* pt = m_pt;
    m_pt = NULL;
    free(pt);
}

T* operator->() {
    assert(m_pt!=NULL);
    return m_pt;
}

В качестве альтернативы, в C вы могли бы по крайней мере предоставить два макроса с одинаковым эффектом:

#define SAFE_FREE(pt) \
    assert(pt!=NULL); \
    free(pt); \
    pt = NULL;

#define SAFE_PTR(pt) assert(pt!=NULL); pt
1
Sebastian

Эти проблемы чаще всего являются симптомами гораздо более глубокой проблемы. Это может происходить для всех ресурсов, которые требуют приобретения и более позднего выпуска, например память, файлы, базы данных, сетевые подключения и т. д. Основная проблема заключается в том, что вы потеряли отслеживание распределения ресурсов из-за отсутствующей структуры кода, выбрасывая случайные malloc-ы и освобождая всюду по базе кода.

Организуйте код вокруг DRY - не повторяйте себя. Держите связанные вещи вместе. Делай только одно, и делай это хорошо. «Модуль», который выделяет ресурс, отвечает за его освобождение и должен предоставлять функцию для этого, которая также заботится о указателях. Для любого конкретного ресурса у вас есть ровно одно место, где он выделен, и одно место, где он выпущен, оба близко друг к другу.

Скажем, вы хотите разбить строку на подстроки. Непосредственно с помощью malloc () ваша функция должна заботиться обо всем: анализировать строку, выделять необходимый объем памяти, копировать туда подстроки, а также и и. Сделайте функцию достаточно сложной, и вопрос не в том, потеряете ли вы ресурсы, а в том, когда.

Ваш первый модуль заботится о фактическом распределении памяти:


    void *MemoryAlloc (size_t size)
    void  MemoryFree (void *ptr)

Во всей вашей кодовой базе есть единственное место, где вызываются malloc () и free ().

Затем нам нужно выделить строки:


    StringAlloc (char **str, size_t len)
    StringFree (char **str)

Они позаботятся о том, чтобы len + 1 был необходим и чтобы указатель был установлен в NULL при освобождении. Предоставьте другую функцию для копирования подстроки:


    StringCopyPart (char **dst, const char *src, size_t index, size_t len)

Он позаботится о том, чтобы index и len находились внутри строки src, и при необходимости измените их. Он вызовет StringAlloc для dst и позаботится о том, чтобы dst был правильно завершен.

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

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

1
Secure

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

Есть еще один важный шаг, который нужно иметь в виду - установка указателя на NULL после освобождения - это только половина работы. В идеале, если вы используете эту идиому, вы должны также обернуть доступ к указателю примерно так:

if (ptr)
  memcpy(ptr->stuff, foo, 3);

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

0
Timo Geusch

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

Может быть, не по стандарту, но вам будет сложно найти реализацию, которая не определяет ее как недопустимую операцию, которая вызывает сбой или исключение (в зависимости от среды выполнения).

0
Anon.