2010-07-31 5 views
6

Пожалуйста, обратите внимание, что я не хочу, чтобы решить любую проблему с моим вопросом - я думал о вероятностях вещей, чтобы случиться и, таким образом, было интересно, о чем-то:Что именно происходит, если вы удаляете объект? (НКУ) (При двойном удаления аварии?)

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

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

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

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

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

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

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


Edit/Update:.

Я сделал тест, следующий код падает с Segfault (НКУ 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

in ??() (gdb)

# 0 0x0000000000000021 in ??()

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

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

+0

Похоже, вам нужно вкладывать деньги в какой-то мьютекса или критические секции слишком – 2010-08-01 01:25:49

+0

0A0D: Это мое решение prelimary (обходной путь). На самом деле был дефект дизайна, потому что не было намерения, что есть два потока, которые могут удалить объект. – IanH

ответ

6

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

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

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

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

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

+0

Что происходит с удаленным элементом, если после удаления нет malloc? – IanH

+0

Я провел несколько тестов с gcc 4.4: Кажется, что первое удаление перезаписывает таблицу виртуальных функций, поэтому авария происходит, когда виртуальный деструктор называется вторым. С не виртуальным деструктором glibc обнаруживает двойное освобождение и прерывает программу. – IanH

+0

Я добавил тест, который я сделал, и его результат на мой вопрос. – IanH

6

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

+2

Знание о том, что может произойти с удаленными объектами и что не помогает в анализе дампа ядра. – IanH

+0

@Ian Я предпочитаю, чтобы в первую очередь не было дампов. Кроме того, вы действительно не можете сказать, что произойдет, если у вас нет интимных знаний о системе распределения памяти, что мало кто делает, и которые могут измениться с момента выпуска до выпуска компилятора. – 2010-07-31 16:55:16

+0

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

1

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

Если у вас есть производный класс, не вызывайте delete в производном классе (child). Если он не объявлен виртуальным, то вызывается только деструктор ~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() 
+0

См. Мое обновление выше: По крайней мере, с новым gcc дважды удаляется сбой, даже если нет нового/malloc. И я знаю причину, по которой деструкторы almos всегда должны быть виртуальными. – IanH