Как отлаживать двойные удаления в С++?
Я поддерживаю устаревшее приложение, написанное на С++. Время от времени он падает, и 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 в ссылку на нулевой указатель, что значительно облегчит ее обнаружение. Он не все поймает; у вас может быть копия устаревшего указателя, но это может помочь вырезать много общих сценариев использования-после-удаления.