#c #pointers #free
#c #память #Бесплатно
Вопрос:
Функция для освобождения экземпляра struct Foo
приведена ниже:
void DestroyFoo(Foo* foo)
{
if (foo) free(foo);
}
Мой коллега предложил вместо этого следующее:
void DestroyFoo(Foo** foo)
{
if (!(*foo)) return;
Foo *tmpFoo = *foo;
*foo = NULL; // prevents future concurrency problems
memset(tmpFoo, 0, sizeof(Foo)); // problems show up immediately if referred to free memory
free(tmpFoo);
}
Я вижу, что установка указателя на NULL
после освобождения лучше, но я не уверен в следующем:
-
Нам действительно нужно назначить указатель на временный? Помогает ли это с точки зрения параллелизма и разделяемой памяти?
-
Действительно ли хорошая идея установить для всего блока значение 0, чтобы вызвать сбой программы или, по крайней мере, вывести результаты со значительным расхождением?
Комментарии:
1. Все зависит от вариантов использования и того, чего вы пытаетесь достичь. Также обратите внимание, что вы можете вызывать
free
с нулевым указателем, тогда это просто ничего не сделает.2. Это неправильный вопрос для stackoverflow: ответы не ясны. Есть преимущества в выполнении действий во втором блоке кода, но есть и недостатки. Перевешивают ли выгоды затраты — это просто вопрос мнения.
3. Второй способ лучше и безопаснее. Вы также можете добавить
assert( foo )
в началеDestroyFoo
, чтобы перехватывать возможные неправильные вызовы в отладочной версии.4. Это не предотвратит никаких проблем с параллелизмом. Компилятор в принципе свободен оптимизировать код без ограничений абстрактной машины («наблюдаемое поведение»). Таким образом, temp — это просто бессмыслица и показывает базовое непонимание. C не поддерживает параллельное выполнение на этом уровне — по веским причинам.
5. Не устанавливайте для освобожденного блока значение 0x00. Установите для него повторяющиеся копии 0xDEADBEEF. Если вы отлаживаете сбой и обнаруживаете, что находитесь в середине блока мертвой говядины, вы знаете, что находитесь внутри освобожденной памяти.
Ответ №1:
Нам действительно нужно назначить указатель на временный? Помогает ли это с точки зрения параллелизма и разделяемой памяти?
Это не имеет никакого отношения к параллелизму или общей памяти. Это бессмысленно.
Действительно ли хорошая идея установить для всего блока значение 0, чтобы вызвать сбой программы или, по крайней мере, вывести результаты со значительным расхождением?
Нет. Вовсе нет.
Решение, предложенное вашим коллегой, ужасно. Вот почему:
-
Установка целого блока в 0 также ничего не дает. Поскольку кто-то случайно использует блок free()’ed, он не узнает об этом, основываясь на значениях в блоке. Именно такой блок
calloc()
возвращается. Таким образом, невозможно узнать, является ли это недавно выделенной памятью (calloc()
илиmalloc() memset()
) или той, которая была освобождена () вашим кодом ранее. Во всяком случае, для вашей программы требуется дополнительная работа по обнулению каждого освобождаемого блока памяти ()’ed. -
free(NULL);
четко определено и не выполняется, поэтомуif
условие вif(ptr) {free(ptr);}
ничего не дает. -
Поскольку
free(NULL);
не работает, установка указателя наNULL
фактически скрыла бы эту ошибку, потому что, если какая-то функция на самом деле вызываетсяfree()
по уже свободному () ‘ed указателю, то они бы этого не знали. -
большинство пользовательских функций будут иметь нулевую проверку при запуске и могут не рассматривать передачу
NULL
ей как условие ошибки:
void do_some_work(void *ptr) {
if (!ptr) {
return;
}
/*Do something with ptr here */
}
Таким образом, все эти дополнительные проверки и обнуление дают ложное ощущение «надежности», в то время как на самом деле это ничего не улучшило. Это просто заменило одну проблему другой — дополнительными затратами на производительность и раздуванием кода.
Таким образом, простой вызов free(ptr);
без какой-либо функции-оболочки является одновременно простым и надежным (большинство malloc()
реализаций немедленно завершились бы сбоем при double free, что хорошо).
Нет простого способа обойти «случайный» вызов free()
дважды или более. Ответственность программиста заключается в том, чтобы отслеживать всю выделенную память и free()
ее соответствие. Если кому-то трудно с этим справиться, то C, вероятно, не тот язык для них.
Комментарии:
1. Я согласен с этим ответом, за исключением части о том, чтобы не устанавливать указатель на NULL после. При назначении NULL double free, по крайней мере, согласован. Без этого двойное освобождение приводит к UB, и это может привести к молчаливому сбою . С присвоением NULL указателю освобождение может, по крайней мере, быть обнаружено позже. Не присвоение NULL только дает ложное чувство безопасности (вы ожидаете сбоя, который может и не произойти).
2. Некоторые из ваших комментариев являются аналогами. Поскольку рекомендуется проверять, не равен ли указатель нулю, прежде чем «что-то делать с ptr», вы должны установить ptr равным NULL после его освобождения. Это сэкономит много часов, потраченных на отладку ужасных ошибок, когда вы освобождаете память, а не устанавливаете ptr в NULL, и тогда кто-то другой (повторно) выделит ту же область памяти. Это также предотвратит двойное освобождение, поскольку вы не сможете освободить ptr, для которого установлено значение NULL.
3. Я считаю, что это (
free(ptr); ptr=NULL;
) не лучше, чем не устанавливать указатель на NULL. Если какой-то код использует указатель, который был отредактирован с помощью функции free(), это ошибка . Я согласен, что в некоторых ситуациях может быть полезно легко найти ошибку. Если ptr == NULL является условием ошибки, то да, это полезно. В противном случае, нет. Так что это полностью зависит от того, как остальная часть кода обрабатывает это. Я бы не сказал, что это прямо лучше (или хуже).4. Большинство пользовательских функций могут иметь нулевую проверку при запуске, но только для быстрого сбоя (и часто только в режиме отладки): Если вы не можете положиться на вызывающего абонента после выполнения контракта, вы уже проиграли, поскольку существует слишком много непроверяемых способов, которыми он может не выполнить свои обязанности. Смотрите Разумно ли обнулять каждый указатель с разыменованием? И да, принятие NULL или какого-либо другого подходящего маркера «не объект» явно является частью контракта большинства функций освобождения.
5. @EduardWirch Его нельзя использовать одновременно без какой-либо блокировки или механизма синхронизации (мьютексы C11, мьютексы pthread для потоков или разделяемой памяти и т.д.). Если указатель используется одновременно, в каждом месте, где он используется, должна быть блокировка / синхронизация. Кроме того, в функции DestroyFoo() необходима блокировка для безопасного изменения указателя. Поскольку он не используется внутри этой функции, если мы предположим, что при вызове этой функции получается блокировка, то: либо другие видят все изменения указателя, либо ничего, и опять же, все проблемы, о которых я упоминал, остаются в силе.
Ответ №2:
То, что предлагает ваш коллега, сделает код «более безопасным» в случае, если функция вызывается дважды (см. sleske comment…as «безопаснее» может означать не одно и то же для всех …;-).
С вашим кодом это, скорее всего, приведет к сбою:
Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
DestroyFoo(foo); // will call free on memory already freed
С версией кода ваших коллег это не приведет к сбою:
Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(amp;foo);
DestroyFoo(amp;foo); // will have no effect
Теперь, для этого конкретного сценария, достаточно выполнения tmpFoo = 0;
(внутри DestroyFoo
). memset(tmpFoo, 0, sizeof(Foo));
предотвратит сбой, если Foo имеет дополнительные атрибуты, к которым может быть получен неправильный доступ после освобождения памяти.
Итак, я бы сказал, да, это может быть хорошей практикой …. но это всего лишь своего рода защита от разработчиков, которые используют плохие методы (потому что определенно нет причин вызывать DestroyFoo
дважды без перераспределения) … в конце концов, вы делаете DestroyFoo
«безопаснее», но медленнее (это делает больше вещей, чтобы предотвратить его неправильное использование).
Комментарии:
1. На самом деле, я бы сказал, что это делает код менее безопасным, скрывая ошибку. Если вы случайно дважды освободите, вы хотите , чтобы это привело к сбою. Обратите внимание, что двойное освобождение не гарантирует сбоя — это лучший случай. Это также может повредить ваш распределитель памяти и перезаписать все ваши данные (неопределенное поведение).
2. @sleske Это правда (за исключением случаев, когда вы намеренно можете вызвать
DestroyFoo
дважды из двух разных фрагментов кода / инструкций). Но это определенно скрывает потенциальную ошибку из-за плохой практики программирования на верхних уровнях…. Я полностью согласен.3. Да, если вы решите, что хотите явно поддерживать двойное освобождение, то это не ошибка по определению; тогда вам понадобится оболочка, чтобы разрешить это. Но мне это кажется проблематичным дизайном; должна быть одна конкретная точка, в которой объект удаляется. Даже в этом случае общепринятым методом является обнуление указателя после его освобождения (потому что
free
для нулевого указателя ничего не будет сделано).4. @sleske, jpo38: Двойное освобождение выделенного объекта, скорее всего, означает, что вы все еще использовали его после первого
free
. Это неопределенное поведение. Как только выfree
создаете объект, вы больше не должны обращаться к нему. Все по-другому, если вы используете какую-то сборку мусора, где вы освобождаете только в том случае, если не существует другой ссылки. Но для этого вам нужны дополнительные меры, такие как счетчик ссылок (см. Python для примера). Тем не менее, вы вызываете только в том случае,free
если невозможно получить доступ к объекту.5. @sleske: Возможно, можно было бы сделать так, чтобы он ВСЕГДА зависал с помощью (по общему признанию, уродливого и потенциально опасного) взлома:
static char guard; assert(*foo != amp;guard); free(*foo); *foo = amp;guard;
. Но я думаю, что лучше просто использовать «традиционный»free_data(T*)
вместоfree_data(T**)
, поскольку двойное free обычно также подразумевает удаление после free в других местах в любом случае (и это не помогает с этим).
Ответ №3:
Второе решение, похоже, чрезмерно разработано. Конечно, в какой-то ситуации это могло бы быть безопаснее, но накладные расходы и сложность слишком велики.
Что вам следует сделать, если вы хотите быть в безопасности, так это присвоить указателю значение NULL после освобождения памяти. Это всегда хорошая практика.
Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
foo = NULL;
Более того, я не знаю, почему люди проверяют, равен ли указатель нулю, перед вызовом free(). Это не обязательно, так как free () сделает эту работу за вас.
Установка памяти в 0 (или что-то еще) является хорошей практикой только в некоторых случаях, поскольку free () не будет очищать память. Это просто отметит область памяти как свободную, чтобы ее можно было использовать повторно. Если вы хотите очистить память, чтобы никто не смог ее прочитать, вам нужно очистить ее вручную. Но это довольно тяжелая операция, и именно поэтому ее не следует использовать для освобождения всей памяти. В большинстве случаев достаточно освобождения без очистки, и вам не нужно жертвовать производительностью для выполнения ненужных операций.
Комментарии:
1. «Если вы хотите очистить память, чтобы никто не смог ее прочитать, вам нужно очистить ее вручную. » Это немного вводит в заблуждение. Во всех текущих многопользовательских операционных системах операционная система гарантирует, что ни один другой процесс не получит содержимое освобожденной памяти (обычно путем обнуления его перед передачей другому процессу). Таким образом, нет необходимости беспокоиться об утечке данных в другой процесс.
2. Я пытался сказать, что free () не очистит память, а скорее пометит ее как неиспользуемую. Итак, если вы хотите действительно очистить память, вам следует сделать это самостоятельно. Более того, существуют способы сброса памяти текущего запущенного процесса, поэтому, если ваша память не очищена, кто-нибудь сможет ее прочитать.
3. Верно, но в современных операционных системах для этого требуется запуск с теми же (или более высокими) привилегиями, что и у запущенного процесса, а затем выполняется очистка. Дело в том, что это проблема только при определенных моделях угроз, поэтому она вполне может быть бессмысленной, поэтому обнуление памяти не всегда является хорошей практикой.
4. Я согласен, что это не то, о чем вам следует беспокоиться при освобождении всех видов памяти. На самом деле я писал о чрезмерной разработке кода и выполнении множества ненужных вещей. Примечание об очистке памяти было просто ссылкой на исходный вопрос, и в одном из решений для памяти было установлено значение 0.
5. К вашему СВЕДЕНИЮ:
memset
beforefree
будет оптимизирован любым уважающим себя компилятором. Вам нужна функция типаSecureZeroMemory
, которая явно предназначена для того, чтобы не быть оптимизированной для этого.
Ответ №4:
void destroyFoo(Foo** foo)
{
if (!(*foo)) return;
Foo *tmpFoo = *foo;
*foo = NULL;
memset(tmpFoo, 0, sizeof(Foo));
free(tmpFoo);
}
Код вашего коллеги плох, потому что
- произойдет сбой, если
foo
будетNULL
- нет смысла создавать дополнительную переменную
- нет смысла устанавливать значения равными нулям
- освобождение структуры напрямую не работает, если она содержит вещи, которые необходимо освободить
Я думаю, что ваш коллега мог бы иметь в виду такой вариант использования
Foo* a = NULL;
Foo* b = createFoo();
destroyFoo(NULL);
destroyFoo(amp;a);
destroyFoo(amp;b);
В таком случае, это должно быть так. попробуйте здесь
void destroyFoo(Foo** foo)
{
if (!foo || !(*foo)) return;
free(*foo);
*foo = NULL;
}
Сначала нам нужно взглянуть на Foo
, давайте предположим, что это выглядит следующим образом
struct Foo
{
// variables
int number;
char character;
// array of float
int arrSize;
float* arr;
// pointer to another instance
Foo* myTwin;
};
Теперь, чтобы определить, как это должно быть уничтожено, давайте сначала определим, как это должно быть создано
Foo* createFoo (int arrSize, Foo* twin)
{
Foo* t = (Foo*) malloc(sizeof(Foo));
// initialize with default values
t->number = 1;
t->character = '1';
// initialize the array
t->arrSize = (arrSize>0?arrSize:10);
t->arr = (float*) malloc(sizeof(float) * t->arrSize);
// a Foo is a twin with only one other Foo
t->myTwin = twin;
if(twin) twin->myTwin = t;
return t;
}
Теперь мы можем написать функцию уничтожения, противоположную функции создания
Foo* destroyFoo (Foo* foo)
{
if (foo)
{
// we allocated the array, so we have to free it
free(t->arr);
// to avoid broken pointer, we need to nullify the twin pointer
if(t->myTwin) t->myTwin->myTwin = NULL;
}
free(foo);
return NULL;
}
Попробуйте здесь протестировать
int main ()
{
Foo* a = createFoo (2, NULL);
Foo* b = createFoo (4, a);
a = destroyFoo(a);
b = destroyFoo(b);
printf("success");
return 0;
}
Ответ №5:
К сожалению, эта идея просто не работает.
Если целью было перехватить double free, это не распространяется на случаи, подобные следующему.
Предположим, что этот код:
Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
free (ptr_1);
free (ptr_2); /* This is a bug */
Предложение состоит в том, чтобы написать вместо:
Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
DestroyFoo (amp;ptr_1);
DestroyFoo (amp;ptr_2); /* This is still a bug */
Проблема в том, что второй вызов DestroyFoo()
все равно завершится сбоем, потому что ptr_2
не сбрасывается в значение NULL и по-прежнему указывает на уже освобожденную память.