Повреждение кучи под Win32; как найти?

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

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

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

Теперь вот загвоздка:
когда он запускается в легкой среде отладки (скажем Visual Studio 98 / AKA MSVC6 ), повреждение кучи достаточно легко воспроизвести - проходит десять или пятнадцать минут, прежде чем что-то ужасно выйдет из строя и возникнут исключения, например, alloc; при работе в сложной среде отладки (Rational Purify, VS2008/MSVC9 или даже Microsoft Application Verifier) ​​система становится ограниченной по скорости памяти и не дает сбоев (привязка к памяти: ЦП не работает выше 50%, индикатор диска не горит, программа работает так быстро, как только может, ящик потребляет 1.3G 2 ГБ ОЗУ) . Итак, у меня есть выбор между возможностью воспроизвести проблему (но не определить причину) или возможностью определить причину или проблему, которую я не могу воспроизвести.

Мои текущие предположения о том, что делать дальше:

  1. Получите безумно грубоватую коробку (чтобы заменить текущую коробку разработчика: 2 Гб ОЗУ E6550 Core2 Duo); это позволит воспроизвести сбой, вызывающий неправильное поведение при работе в мощной среде отладки; или
  2. Операторы перезаписи newи deleteиспользовать VirtualAllocи VirtualProtectпометить память как доступную только для чтения, как только это будет сделано. Запустите систему MSVC6и пусть ОС поймает злоумышленника, который пишет в освобожденную память. Да, это признак отчаяния: кто ад перезаписывает newи delete?! Интересно, сделает ли это такой же медленный процесс, как в Purify et al.

И нет: доставка со встроенными приборами Purify невозможна.

Коллега просто прошел мимо и спросил: «Переполнение стека? Получается ли сейчас переполнение стека?!?»

А теперь вопрос: как мне найти коррумпателя кучи?


Обновление: балансировка new[] и, delete[] похоже, долгий путь к решению проблемы. Вместо 15 минут приложение теперь доживает около двух часов до сбоя. Еще нет. Есть еще предложения? Повреждение кучи сохраняется.

Обновление: сборка релиза под Visual Studio 2008 кажется значительно лучше; текущие подозрения связаны с STL реализацией, поставляемой с VS98 .


  1. Воспроизведите проблему. Dr Watsonсоздаст дамп, который может быть полезен для дальнейшего анализа.

Я приму это к сведению, но меня беспокоит, что доктор Ватсон споткнется только постфактум, а не тогда, когда по куче начнут наступать.

Еще одна попытка может быть использована WinDebugв качестве инструмента отладки, который является довольно мощным и в то же время легким.

На данный момент все в порядке: не очень помогает, пока что-то не пойдет не так. Я хочу застать вандала с поличным.

Возможно, эти инструменты позволят вам хотя бы сузить проблему до определенного компонента.

Я не очень надеюсь, но отчаянные времена требуют ...

И вы уверены, что все компоненты проекта имеют правильные настройки библиотеки времени выполнения ( C/C++ tabкатегория «Генерация кода» в настройках проекта VS 6.0)?

Нет, я не знаю, и завтра я потрачу пару часов на просмотр рабочего пространства (в нем 58 проектов) и проверки того, что все они компилируются и связываются с соответствующими флагами.


Обновление: это заняло 30 секунд. Выберите все проекты в Settingsдиалоговом окне, снимайте выделение, пока не найдете проекты, у которых нет правильных настроек (все они имели правильные настройки).

Ответов (15)

Решение

Мой первый выбор - это специальный инструмент для работы с кучей, такой как pageheap.exe .

Перезапись new и delete может быть полезна, но это не улавливает выделения, зафиксированные кодом более низкого уровня. Если это то, что вы хотите, лучше обойтись low-level alloc API с помощью Microsoft Detours.

Также проверки работоспособности, такие как: проверка соответствия ваших библиотек времени выполнения (выпуск или отладка, многопоточный или однопоточный, dll и статическая библиотека), поиск плохих удалений (например, удаление там, где должен был быть delete [] used), убедитесь, что вы не смешиваете и не сопоставляете свои аллоки.

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

Как выглядит стек вызовов и т. Д. Во время первого исключения?

У меня такие же проблемы в работе (мы тоже VC6 иногда используем ). И для этого нет простого решения. У меня есть только подсказки:

  • Попробуйте использовать автоматические аварийные дампы на производственной машине (см. Process Dumper ). Мой опыт показывает, что доктор Ватсон не идеален для сброса отходов.
  • Удалите все уловки (...) из вашего кода. Они часто скрывают серьезные исключения из памяти.
  • Проверьте расширенную отладку Windows - есть много отличных советов по устранению таких проблем, как ваша. Я всем сердцем рекомендую это.
  • Если вы используете STLпробные STLPortи проверенные сборки. Неверный итератор - черт возьми.

Удачи. На решение таких проблем, как ваша, у нас уходят месяцы. Будьте готовы к этому ...

Моим первым действием было бы следующее:

  1. Скомпилируйте двоичные файлы в версии «Release», но создав файл отладочной информации (эту возможность вы найдете в настройках проекта).
  2. Используйте Dr Watson в качестве отладчика по умолчанию (DrWtsn32 -I) на машине, на которой вы хотите воспроизвести проблему.
  3. Воспроизведите проблему. Доктор Ватсон создаст дамп, который может быть полезен для дальнейшего анализа.

Еще одна попытка может заключаться в использовании WinDebug в качестве инструмента отладки, который является довольно мощным и в то же время легким.

Возможно, эти инструменты позволят вам хотя бы сузить проблему до определенного компонента.

И вы уверены, что все компоненты проекта имеют правильные настройки библиотеки времени выполнения (вкладка C/C++, категория «Генерация кода» в настройках проекта VS 6.0)?

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

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

Если вы сможете узнать, что именно МОЖЕТ вызвать эту проблему, с помощью Google и документации по полученным вами исключениям, возможно, это даст дополнительное представление о том, что искать в коде.

Итак, исходя из имеющейся у вас ограниченной информации, это может быть комбинация одного или нескольких факторов:

  • Плохое использование кучи, т. Е. Двойное освобождение, чтение после освобождения, запись после освобождения, установка флага HEAP_NO_SERIALIZE с выделением и освобождение из нескольких потоков в одной и той же куче
  • Недостаточно памяти
  • Плохой код (например, переполнение буфера, опустошение буфера и т. Д.)
  • Проблемы со сроками

Если это вообще первые два, но не последний, вы уже должны были поймать его с помощью pageheap.exe.

Что, скорее всего, означает, что это связано с тем, как код обращается к общей памяти. К сожалению, отследить это будет довольно болезненно. Несинхронизированный доступ к общей памяти часто проявляется в виде странных "временных" проблем. Такие вещи, как отказ от семантики получения / выпуска для синхронизации доступа к общей памяти с помощью флага, неправильное использование блокировок и т. Д.

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

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

Когда вы тестировали VS2008, работали ли вы с HeapVerifier с параметром Conserve Memory, установленным на Yes? Это может снизить влияние распределителя кучи на производительность. (Кроме того, вам нужно запустить с ним Debug-> Start with Application Verifier, но вы, возможно, уже знаете об этом.)

Вы также можете попробовать отладку с помощью Windbg и различные варианты использования команды! Heap.

MSN

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

В книге Стива Магуайра Writing Solid Code (настоятельно рекомендуется) есть примеры отладки, которые вы можете выполнять с помощью этих подпрограмм, например:

  • Следите за распределением, чтобы найти утечки
  • Выделите больше памяти, чем необходимо, и поместите маркеры в начало и конец памяти - во время произвольной процедуры вы можете убедиться, что эти маркеры все еще там
  • memset память с маркером на выделение (чтобы найти использование неинициализированной памяти) и на свободном (чтобы найти использование свободной памяти)

Еще одна хорошая идея заключается в том , чтобы никогда не использовать вещи , как strcpy, strcat или sprintf - всегда использовать strncpy, strncat и snprintf . Мы также написали наши собственные версии для них, чтобы убедиться, что мы не списываем конец буфера, и они также выявили множество проблем.

Очевидная случайность повреждения памяти очень похожа на проблему синхронизации потоков - ошибка воспроизводится в зависимости от скорости машины. Если объекты (блоки памяти) разделяются между потоками, а примитивы синхронизации (критическая секция, мьютекс, семафор, другие) не относятся к каждому классу (объекту, классу), то можно прийти к ситуации. где класс (фрагмент памяти) удаляется / освобождается во время использования или используется после удаления / освобождения.

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

Предложение Грэма о пользовательском malloc / free - хорошая идея. Посмотрите, сможете ли вы охарактеризовать какую-то закономерность коррупции, чтобы дать вам рычаги воздействия.

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

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

Это в условиях нехватки памяти? Если это так, возможно, что новый возвращается, NULL а не бросает std::bad_alloc. Старые VC++ компиляторы не реализовали это должным образом. Есть статья о сбоях выделения памяти в устаревших версиях, приводящих к сбою STL приложений, созданных с помощью VC6 .

Запустите исходное приложение с. ADplus -crash -pn appnename.exe Когда всплывает проблема с памятью, вы получите хороший большой дамп.

Вы можете проанализировать дамп, чтобы выяснить, какая область памяти была повреждена. Если вам повезло, перезапись памяти - это уникальная строка, и вы можете понять, откуда она взялась. Если не повезло, нужно будет покопаться в win32 куче и выяснить, каковы были исходные характеристики памяти. (куча -x может помочь)

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

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

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

PS: Вы можете использовать DebugDiag для анализа дампов. Он может указать на DLL владение поврежденной кучей и предоставить другие полезные сведения.

Если вы решите переписать новое / удалить, я сделал это, и у меня есть простой исходный код по адресу:

http://gandolf.homelinux.org/~smhanov/blog/?id=10

Это улавливает утечки памяти, а также вставляет данные защиты до и после блока памяти, чтобы зафиксировать повреждение кучи. Вы можете просто интегрироваться с ним, поместив #include «debug.h» в начало каждого файла CPP и определив DEBUG и DEBUG_MEM.

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

Для статического анализа рассмотрите возможность компиляции с помощью PREfast ( cl.exe /analyze ). Он обнаруживает несовпадающий delete и delete[], переполнение буфера и множество других проблем. Однако будьте готовы преодолеть многие килобайты предупреждений L6, особенно если ваш проект все еще L4 не исправлен.

PREfast доступен в Visual Studio Team System и, по- видимому , как часть Windows SDK.

Вскоре мне пришлось решить аналогичную задачу. Если проблема все еще существует, я предлагаю вам сделать следующее: отслеживать все вызовы new / delete и malloc / calloc / realloc / free. Я делаю одну DLL экспортирующую функцию для регистрации всех вызовов. Эта функция получает параметр для идентификации источника вашего кода, указатель на выделенную область и тип вызова, сохраняя эту информацию в таблице. Вся выделенная / освобожденная пара удаляется. В конце или после того, как вам нужно, вы вызываете другую функцию для создания отчета по оставленным данным. С его помощью вы можете идентифицировать неправильные вызовы (новые / бесплатные или malloc / delete) или отсутствующие. Если в вашем коде есть перезапись буфера, сохраненная информация может быть неправильной, но каждый тест может обнаруживать / обнаруживать / включать решение выявленной ошибки. Многие пробеги помогают выявить ошибки. Удачи.

Как вы думаете, это состояние гонки? Несколько потоков совместно используют одну кучу? Можете ли вы предоставить каждому потоку частную кучу с помощью HeapCreate, тогда они могут работать быстро с HEAP_NO_SERIALIZE. В противном случае куча должна быть потокобезопасной, если вы используете многопоточную версию системных библиотек.

Пара предложений. Вы упомянули обильные предупреждения на W4 - я бы посоветовал потратить время на то, чтобы исправить ваш код, чтобы он компилировался чисто на уровне предупреждения 4 - это будет иметь большое значение для предотвращения трудноуловимых ошибок.

Во-вторых, для переключателя / analysis он действительно генерирует множество предупреждений. Чтобы использовать этот переключатель в моем собственном проекте, я создал новый файл заголовка, который использовал #pragma warning, чтобы отключить все дополнительные предупреждения, генерируемые / анализировать. Затем, ниже по файлу, я включаю только те предупреждения, которые мне небезразличны. Затем используйте переключатель компилятора / FI, чтобы этот файл заголовка был включен первым во все ваши единицы компиляции. Это должно позволить вам использовать переключатель / analysis при управлении выводом.