Что именно произойдет, если вы удалите объект? (gcc) (При сбое двойного удаления?)

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

Что именно произойдет, если вы удалите объект и используете gcc в качестве компилятора?

На прошлой неделе я расследовал сбой, когда состояние гонки привело к двойному удалению объекта.

Сбой произошел при вызове виртуального деструктора объекта, так как указатель на таблицу виртуальных функций уже был перезаписан.

Переписывается ли указатель виртуальной функции при первом удалении?

Если нет, то безопасно ли второе удаление, если за это время не будет произведено новое выделение памяти?

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

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


Изменить/обновить:

Я сделал тест, следующий код вылетает с segfault (gcc 4.4, i686 и amd64):

class M
{
private:
  int* ptr;
public:
  M() {
  ptr = new int[1];
  }
  virtual ~M() {delete ptr;}
};

int main(int argc, char** argv)
{
  M* ptr = new M();
  delete ptr;
  delete ptr;
}

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

Как на amd64, так и на i686 указатель указывает на допустимую область памяти (кучу), но значение там недействительно (счетчик? Он очень низкий, например, 0x11 или 0x21), поэтому «вызов» (или «jmp», когда компилятор сделал возврат-оптимизацию) прыгает в недопустимый регион.

Программа получила сигнал SIGSEGV,

Ошибка сегментации. 0x0000000000000021

в ?? () (ГДБ)

#0 0x0000000000000021 in ?? ()

#1 0x000000000040083e в главном ()

Таким образом, при вышеупомянутых условиях указатель на таблицу виртуальных функций ВСЕГДА перезаписывается первым удалением, поэтому следующее удаление перейдет в нирвану, если в классе есть виртуальный деструктор.


person IanH    schedule 31.07.2010    source источник
comment
Похоже, вам нужно инвестировать в некоторые мьютексы или критические секции.   -  person    schedule 01.08.2010
comment
0A0D: Это мое предварительное решение (обходной путь). На самом деле был недостаток дизайна, потому что не предполагалось, что есть два потока, которые могут удалить объект.   -  person IanH    schedule 01.08.2010


Ответы (3)


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

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

1) указатель на следующий фрагмент был перезаписан, и второй free() вызывает segfault при попытке доступа к следующему фрагменту.

2) нижний колонтитул предыдущего чанка был изменен, и доступ к заголовку предыдущего чанка вызывает segfault.

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

person Community    schedule 31.07.2010
comment
Что происходит с удаленным элементом, если после удаления больше нет malloc? - person IanH; 31.07.2010
comment
Я провел несколько тестов с gcc 4.4: действительно кажется, что первое удаление перезаписывает таблицу виртуальных функций, поэтому сбой происходит, когда виртуальный деструктор вызывается во второй раз. С невиртуальным деструктором glibc обнаруживает двойное освобождение и прерывает программу. - person IanH; 31.07.2010
comment
Я добавил тест, который я сделал, и его результат к моему вопросу. - person IanH; 01.08.2010
comment
Что ж, это означает, что при пометке чанка свободным glibc использует часть освобожденной памяти для реализации какой-то структуры данных (например, для сортировки чанков по их размеру). - person ; 02.08.2010

Удаление чего-то дважды — это поведение undefined — вам не нужно никаких дополнительных объяснений, кроме этого, и, как правило, искать его бесполезно. Это может привести к сбою программы, а может и нет, но это всегда плохо, и программа всегда будет в неизвестном состоянии после того, как вы это сделаете.

person Community    schedule 31.07.2010
comment
Знание того, что может произойти с удаленными объектами, а что нет, помогает в анализе дампа памяти. - person IanH; 31.07.2010
comment
@Ian Я предпочитаю вообще не иметь дампов ядра. Кроме того, вы действительно не можете сказать, что произойдет, если у вас нет глубоких знаний о системе распределения памяти, которой мало кто занимается и которая вполне может меняться от версии к версии компилятора. - person ; 31.07.2010
comment
Я предпочитаю, чтобы багов вообще не было, но они случаются — и сбои тоже. Тогда очень полезно знать, что может случиться (и что может быть причиной того, что произошло), а что нет. В длительных проектах вы обычно не часто меняете компилятор. И, надеюсь, компилятор и его libc не изменят распределитель памяти, не заметив этого. - person IanH; 01.08.2010

При двукратном выполнении delete (или даже free) память может быть уже перераспределена, а повторное выполнение delete может привести к повреждению памяти. Размер выделенного блока памяти часто хранится непосредственно перед самим блоком памяти.

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

i.e.

BaseClass* obj_ptr = new DerivedClass;  // Allowed due to polymorphism.
...
delete obj_ptr;  // this will call the destructor ~Parent() and NOT ~Child()
person Community    schedule 01.08.2010
comment
См. мое обновление выше: по крайней мере, с новым gcc двойное удаление сразу же дает сбой, даже если в это время нет new/malloc. И я знаю причину, по которой деструкторы почти всегда должны быть виртуальными. - person IanH; 01.08.2010