Как отлаживать двойные удаления в С++?

Я поддерживаю устаревшее приложение, написанное на С++. Время от времени он падает, и Valgrind сообщает мне о двойном удалении какого-либо объекта.

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

Поделитесь своими лучшими советами и трюками!

Ответ 1

Вот какое общее предложение, которое помогло мне в этой ситуации:

  • Поверните уровень ведения журнала до полной отладки, если вы используете регистратор. Ищите подозрительные вещи в выходе. Если ваше приложение не регистрирует выделения указателей и удаляет подозрительный объект/класс, пришло время вставить в ваш код несколько операторов cout << "class Foo constructed, ptr= " << this << endl; (и соответствующие delete/destructor prints).
  • Запустите valgrind с -db-attach = yes. Я нашел это очень удобно, если немного утомительно. Valgrind покажет вам трассировку стека каждый раз, когда обнаруживает значительную ошибку памяти или событие, а затем спрашивает, хотите ли вы ее отладить. Вы можете многократно нажимать "n" много раз, если ваше приложение велико, но продолжайте искать строку кода, где первый объект (и, во-вторых,) удален.
  • Просто почистите код. Найдите конструкцию/удаление объекта. К сожалению, иногда это заканчивается в сторонней библиотеке: - (.
  • Обновление. Недавно выяснилось это: очевидно, gcc 4.8 и более поздние версии (если вы можете использовать GCC в вашей системе) имеют некоторые новые встроенные функции для обнаружения ошибок памяти, " адрес дезинфицирующего средства". Также доступна в системе компилятора LLVM.

Ответ 2

Угу. Что сказал @OliCharlesworth. Там нет верного способа проверки указателя, чтобы увидеть, указывает ли он на выделенную память, так как это действительно просто расположение памяти.

Самая большая проблема, которую ваш вопрос подразумевает, - это отсутствие воспроизводимости. Продолжая это, вы застряли в изменении простых конструкций "delete" до delete foo;foo = NULL;.

Даже тогда самый лучший сценарий - "кажется, что он меньше", пока вы не отменили его.

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

Это одна из самых простых по-настоящему неприятных проблем.

Ответ 3

Это может работать или не работать для вас.

Давным-давно я работал над программой 1M + lines, которой было тогда 15 лет. Столкнувшись с одной и той же проблемой - двойное удаление с огромным набором данных. С такими данными любой из "профайлеров памяти" не будет идти.

Вещи, которые были на моей стороне:

  • Это было очень воспроизводимо - у нас был макроязык и он выполнял те же script точно так же, как каждый раз воспроизводил его
  • Когда-то во время истории проекта кто-то решил, что "#define malloc my_malloc" и "#define free my_free" были использованы. Они не делали гораздо больше, чем вызов встроенных malloc() и free(), но проект уже скомпилирован и работал таким образом.

Теперь трюк/идея:

my_malloc(int size)
{
   static int allocation_num = 0;  // it was single threaded

   void* p = builtin_malloc(size+16);

   *(int*)p = ++allocation_num;
   *((char*)p+sizeof(int)) = 0; // not freed

   return (char*)p+16;  // check for NULL in order here
}

my_free(void* p)
{
    if (*((char*)p+sizeof(int)))
    {
        // this is double free, check allocation_number
        // then rerun app with this in my_alloc
        //    if (alloc_num == XXX) debug_break();
    }

    *((char*)p+sizeof(int)) = 1; // freed

    //built_in_free((char*)p-16);  // do not do this until problem is figured out
}

С новым/удалением это может быть сложнее, но с LD_PRELOAD вы можете заменить malloc/free, даже не перекомпилируя свое приложение.

Ответ 4

вы, вероятно, обновляете версию, которая обрабатывала удаление по-другому, а затем новую версию.

Вероятно, что сделала предыдущая версия, когда был вызван delete, он выполнил статическую проверку для if (X != NULL){ delete X; X = NULL;}, а затем в новой версии он просто выполнил действие delete.

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

Ответ 5

Я нашел это полезным: backtrace() в linux. (Вы должны скомпилировать с помощью -rdynamic.) Это позволяет вам узнать, откуда приходит эта двойная свобода, поставив блок try/catch вокруг всех операций с памятью (новый/удалить), а затем в блоке catch распечатайте трассировку стека.

Таким образом, вы можете сузить подозреваемых намного быстрее, чем запустить valgrind.

Я завернул backtrace в удобном маленьком классе, чтобы я мог просто сказать:

try {
  ...
} catch (...) {
  StackTrace trace;
  std::cerr << "Double free!!!\n" << trace << std::endl;
  throw;
} 

Ответ 6

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

Также в Windows вы можете использовать Application Verifier. Если я правильно помню, у него есть режим, он выделяет каждое выделение на отдельную страницу с защищенными страницами защиты между ними. Это очень эффективно при поиске переполнения буфера, но я подозреваю, что это также было бы полезно для ситуации с двойным свободным доступом.

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

delete foo;

заменяется на:

{ delete foo; foo = nullptr; }

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