Вопрос

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

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

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

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

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

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

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

Коллега просто проходил мимо и спросил: "Переполнение стека?У нас сейчас происходит переполнение стека ?!?"

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


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

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


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

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

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

Опять же, в данный момент это происходит:особой помощи не будет, пока что-то не пойдет не так.Я хочу поймать вандала с поличным.

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

Я не питаю особых надежд, но отчаянные времена требуют своего...

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

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


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

Это было полезно?

Решение

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

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

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

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

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Предварительная сборка доступна с Visual Studio Team System и, очевидно, как часть Windows SDK.

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

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

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

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

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

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

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

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

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

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

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

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

  • Плохое использование кучи, т. е. двойное освобождение, чтение после освобождения, запись после освобождения, установка флага HEAP_NO_SERIALIZE с помощью allocs и освобождение от нескольких потоков в одной куче
  • Из памяти
  • Плохой код (например, переполнение буфера, переполнение буфера и т.д.)
  • Проблемы со "сроками"

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

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

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

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

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

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

MSN

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

http://gandolf.homelinux.org /~smhanov/блог/?id=10

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top