Что именно происходит, если вы удаляете объект? (gcc) (Когда дважды удаляется сбой?) - PullRequest
6 голосов
/ 31 июля 2010

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

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

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

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

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

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

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

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


Редактировать / Update:

Я сделал тест, следующий код вылетает с ошибкой (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;
}

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

И на amd64, и на i686 указатель указывает на допустимую область памяти (кучу), но значение там недопустимо (счетчик? Это очень низкое значение, например, 0x11 или 0x21), поэтому 'call' (или 'jmp') когда компилятор выполнил возвратную оптимизацию) переходит в недопустимую область.

Программа принимает сигнал SIGSEGV,

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

в ?? () (ГБД)

# 0 0x0000000000000021 в ?? ()

# 1 0x000000000040083e в основном ()

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

Ответы [ 3 ]

6 голосов
/ 31 июля 2010

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

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

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

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

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

6 голосов
/ 31 июля 2010

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

1 голос
/ 01 августа 2010

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

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

т.е.

BaseClass* obj_ptr = new DerivedClass;  // Allowed due to polymorphism.
...
delete obj_ptr;  // this will call the destructor ~Parent() and NOT ~Child()
...