Повреждение кучи под Win32; как найти?
Я работаю над многопоточным приложением C++, которое портит кучу. Обычные инструменты для обнаружения этой коррупции кажутся неприменимыми. Старые сборки (18 месяцев) исходного кода демонстрируют то же поведение, что и самый последний выпуск, так что это было давно и просто не было замечено; с другой стороны, исходные дельты не могут быть использованы для определения момента появления ошибки - в хранилище много изменений кода.
Подсказка для сбойного поведения заключается в создании пропускной способности в этой системе - передача данных через сокет, которая передается во внутреннее представление. У меня есть набор тестовых данных, которые периодически вызывают исключение приложения (в разных местах, по разным причинам, в том числе при сбое выделения кучи, например: повреждение кучи).
Поведение, похоже, связано с мощностью процессора или пропускной способностью памяти; чем больше у каждой машины, тем легче ее разбить. Отключение гиперпоточного ядра или двухъядерного ядра снижает вероятность (но не устраняет) повреждения. Это говорит о сроках, связанных с проблемой.
Теперь вот загвоздка:
Когда он запускается в облегченной среде отладки (скажем, Visual Studio 98 / AKA MSVC6
) повреждение кучи достаточно легко воспроизвести - проходит десять или пятнадцать минут, прежде чем что-то ужасно терпит неудачу и исключения, как alloc;
при работе в сложной среде отладки (Rational Purify, VS2008/MSVC9
или даже Microsoft Application Verifier) система становится связанной со скоростью памяти и не падает (связанная с памятью: ЦП не становится выше 50%
, индикатор диска не горит, программа работает так быстро, как может, коробка потребляет 1.3G
2G оперативной памяти). Итак, у меня есть выбор между способностью воспроизвести проблему (но не определить причину) или возможностью определить причину или проблему, которую я не могу воспроизвести.
Мои текущие лучшие предположения относительно того, куда идти дальше:
- Получите безумно щедрый ящик (чтобы заменить текущий ящик разработчика: 2 ГБ ОЗУ в
E6550 Core2 Duo
); это позволит воспроизвести сбой, вызвавший неправильное поведение при работе в мощной среде отладки; или же - Переписать операторы
new
а такжеdelete
использоватьVirtualAlloc
а такжеVirtualProtect
пометить память только для чтения, как только это будет сделано с. Бежать подMSVC6
и пусть ОС поймает плохого парня, который пишет в освобожденную память. Да, это признак отчаяния: кого, черт возьми, переписываетnew
а такжеdelete
?! Интересно, будет ли это так медленно, как в Purify et al.
И, нет, доставка со встроенным прибором Purify не возможна.
Коллега только что прошел мимо и спросил: "Переполнение стека? Получаем ли мы переполнение стека сейчас?!?"
А теперь вопрос: как мне найти кучного корруптора?
Обновление: балансировка new[]
а также delete[]
кажется, прошел долгий путь к решению проблемы. Вместо 15 минут приложение теперь работает примерно за два часа до сбоя. Еще нет. Есть еще предложения? Кучи коррупции сохраняется.
Обновление: сборка релиза под Visual Studio 2008 выглядит значительно лучше; текущее подозрение опирается на STL
реализация, которая поставляется с VS98
,
- Воспроизведите проблему.
Dr Watson
создаст дамп, который может быть полезен для дальнейшего анализа.
Я приму это к сведению, но я обеспокоен тем, что доктора Уотсона будут сбивать с толку только после факта, а не тогда, когда наваливают кучу.
Другая попытка может использовать
WinDebug
как инструмент отладки, который является довольно мощным, но в то же время легким.
Снова все пошло не так: мало помощи, пока что-то пойдет не так. Я хочу поймать вандала в действии.
Возможно, эти инструменты позволят вам хотя бы сузить проблему до определенного компонента.
Я не очень надеюсь, но отчаянные времена требуют...
И уверены ли вы, что все компоненты проекта имеют правильные настройки библиотеки времени выполнения (
C/C++ tab
, Категория генерации кода в настройках проекта VS 6.0)?
Нет, я не буду, и завтра я проведу пару часов, изучая рабочее пространство (в нем 58 проектов) и проверяя, все ли они компилируются и связываются с соответствующими флагами.
Обновление: это заняло 30 секунд. Выберите все проекты в
Settings
В диалоговом окне снимите флажок, пока не найдете проект (ы), которые не имеют правильных настроек (у всех были правильные настройки).
15 ответов
Мой первый выбор будет выделенным инструментом кучи, таким как pageheap.exe.
Переписывание new и delete может быть полезным, но это не позволяет отследить ассигнования, принятые кодом более низкого уровня. Если это то, что вы хотите, лучше обойти low-level alloc API
с помощью Microsoft Detours.
Также проверки работоспособности, такие как: проверьте соответствие ваших библиотек времени выполнения (выпуск против отладки, многопоточный против однопоточных, dll против статических lib), ищите плохие удаления (например, delete там, где должно было быть delete [] используется), убедитесь, что вы не смешиваете и не сопоставляете свои ресурсы.
Также попробуйте выборочно отключить потоки и посмотрите, когда проблема исчезнет.
Как выглядит стек вызовов и т. Д. Во время первого исключения?
У меня такие же проблемы в работе (мы также используем VC6
иногда). И нет простого решения для этого. У меня есть только несколько подсказок:
- Попробуйте с автоматическим аварийным сбросом на производственном компьютере (см. Process Dumper). Мой опыт говорит, что доктор Ватсон не идеален для свалки.
- Удалите все catch(...) из вашего кода. Они часто скрывают серьезные исключения из памяти.
- Проверьте Advanced Windows Debugging - есть много хороших советов для таких проблем, как ваша. Я рекомендую это всем своим сердцем.
- Если вы используете
STL
пытатьсяSTLPort
и проверил сборки. Неверный итератор - ад.
Удачи. Такие проблемы, как ваша, занимают у нас месяцы. Будьте готовы к этому...
Нам очень повезло, написав наши собственные malloc и бесплатные функции. В производственном процессе они просто называют стандартный malloc и free, но при отладке они могут делать все, что вы захотите. У нас также есть простой базовый класс, который ничего не делает, кроме переопределения операторов new и delete для использования этих функций, тогда любой написанный вами класс может просто наследовать от этого класса. Если у вас есть тонна кода, это может быть большая работа, чтобы заменить вызовы malloc и бесплатно на новые malloc и free (не забывайте realloc!), Но в долгосрочной перспективе это очень полезно.
В книге Стива Магуайра " Написание твердого кода" (настоятельно рекомендуется) есть примеры отладочной информации, которую вы можете выполнить в этих подпрограммах, например:
- Следите за распределением, чтобы найти утечки
- Выделите больше памяти, чем необходимо, и поместите маркеры в начало и конец памяти - во время свободной процедуры вы можете убедиться, что эти маркеры все еще есть
- memset память с маркером на выделение (чтобы найти использование неинициализированной памяти) и на свободном (чтобы найти использование свободной памяти)
Еще одна хорошая идея, чтобы никогда не использовать такие вещи, как strcpy
, strcat
, или же sprintf
- всегда используй strncpy
, strncat
, а также snprintf
, Мы также написали свои собственные версии, чтобы не допустить конца буфера, и они тоже столкнулись с множеством проблем.
Запустите оригинальное приложение с ADplus -crash -pn appnename.exe
Когда всплывет проблема с памятью, вы получите хороший большой дамп.
Вы можете проанализировать дамп, чтобы определить, какая ячейка памяти была повреждена. Если вам повезло, память для перезаписи - это уникальная строка, которую вы можете выяснить, откуда она взялась. Если вам не повезло, вам нужно будет покопаться в win32
куча и цифра, что было оригинальной характеристикой памяти. (куча -х может помочь)
После того, как вы узнаете, что произошло, вы можете сузить использование appverifier с помощью специальных настроек кучи. т.е. вы можете указать, что DLL
Вы контролируете, или какой размер выделения для мониторинга.
Надеюсь, это ускорит мониторинг настолько, чтобы поймать преступника.
По моему опыту, мне никогда не требовался режим полной проверки кучи, но я потратил много времени на анализ аварийных дампов и просмотр источников.
PS: Вы можете использовать DebugDiag для анализа дампов. Это может указать на DLL
владеть поврежденной кучей, и дать вам другие полезные детали.
Вы должны атаковать эту проблему с помощью как времени выполнения, так и статического анализа.
Для статического анализа рассмотрим компиляцию с помощью PREfast (cl.exe /analyze
). Обнаруживает несоответствие delete
а также delete[]
, переполнение буфера и множество других проблем. Будьте готовы, однако, пройти через многие килобайты предупреждения L6, особенно если ваш проект все еще имеет L4
не фиксируется.
PREfast доступен с Visual Studio Team System и, по- видимому, как часть Windows SDK.
Это в условиях низкой памяти? Если так, то может быть, что новое возвращается NULL
вместо того, чтобы бросать std::bad_alloc. Старшая VC++
компиляторы не реализовали это должным образом. Есть статья о сбоях выделения памяти Legacy STL
приложения, созданные с VC6
,
Кажущаяся случайность повреждения памяти очень напоминает проблему синхронизации потоков - ошибка воспроизводится в зависимости от скорости машины. Если объекты (куски памяти) совместно используются потоками, а примитивы синхронизации (критическая секция, мьютекс, семафор и т. Д.) Не относятся к отдельным классам (объектам, классам), то можно прийти к ситуации. где класс (кусок памяти) удаляется / освобождается во время использования или используется после удаления / освобождения.
В качестве теста для этого вы можете добавить примитивы синхронизации к каждому классу и методу. Это замедлит ваш код, потому что многим объектам придется ждать друг друга, но если это устранит повреждение кучи, ваша проблема с повреждением кучи станет проблемой оптимизации кода.
Вы пробовали старые сборки, но есть ли причина, по которой вы не можете продолжать идти дальше в истории репозитория и точно знать, когда появилась ошибка?
В противном случае, я бы посоветовал добавить какую-нибудь простую запись в журнал, чтобы помочь отследить проблему, хотя я не знаю, что конкретно вы захотите зарегистрировать.
Если вы можете узнать, что именно МОЖЕТ вызвать эту проблему, через Google и документацию об исключениях, которые вы получаете, возможно, это даст дополнительное понимание того, что искать в коде.
Мое первое действие будет следующим:
- Создайте двоичные файлы в версии "Release", но создайте файл с информацией об отладке (вы найдете эту возможность в настройках проекта).
- Используйте Dr Watson в качестве отладчика по умолчанию (DrWtsn32 -I) на машине, на которой вы хотите воспроизвести проблему.
- Воспроизведите проблему. Доктор Ватсон создаст дамп, который может быть полезен для дальнейшего анализа.
Другой попыткой может быть использование WinDebug в качестве инструмента отладки, который является достаточно мощным и в то же время легким.
Возможно, эти инструменты позволят вам хотя бы сузить проблему до определенного компонента.
И уверены ли вы, что все компоненты проекта имеют правильные настройки библиотеки времени выполнения (вкладка C/C++, категория "Генерация кода" в настройках проекта VS 6.0)?
Если вы решите переписать новый / удалить, я сделал это и имею простой исходный код по адресу:
http://gandolf.homelinux.org/~smhanov/blog/?id=10
Это отлавливает утечки памяти, а также вставляет защитные данные до и после блока памяти, чтобы зафиксировать повреждение кучи. Вы можете просто интегрироваться с ним, поместив #include "debug.h" в начале каждого файла CPP и определив DEBUG и DEBUG_MEM.
Таким образом, из имеющейся у вас ограниченной информации это может быть сочетание одной или нескольких вещей:
- Неправильное использование кучи, то есть двойное освобождение, чтение после освобождения, запись после освобождения, установка флага HEAP_NO_SERIALIZE с помощью allocs и освобождения из нескольких потоков в одной куче
- Недостаточно памяти
- Неверный код (т. Е. Переполнение буфера, переполнение буфера и т. Д.)
- "Сроки" проблемы
Если это вообще первые два, но не последние, вы должны были поймать его сейчас с помощью любого pageheap.exe.
Что, скорее всего, означает, что это связано с тем, как код обращается к общей памяти. К сожалению, отследить это будет довольно болезненно. Несинхронизированный доступ к разделяемой памяти часто проявляется как странные проблемы с синхронизацией. Например, не использовать семантику получения / освобождения для синхронизации доступа к общей памяти с флагом, неправильное использование блокировок и т. Д.
По крайней мере, это поможет как-то отслеживать распределение, как было предложено ранее. По крайней мере, тогда вы сможете увидеть, что на самом деле произошло до повреждения кучи, и попытаться диагностировать это.
Кроме того, если вы можете легко перенаправить выделения в несколько куч, вы можете попробовать это, чтобы увидеть, устраняет ли это проблему или приводит к более воспроизводимому поведению с ошибками.
Когда вы тестировали с VS2008, работали ли вы с HeapVerifier с параметром "Сохранить память", установленным в "Да"? Это может снизить влияние на производительность распределителя кучи. (Кроме того, вы должны запустить его с Debug->Start with Application Verifier, но вы, возможно, уже знаете это.)
Вы также можете попробовать отладку с помощью Windbg и различные варианты использования команды! Heap.
MSN
Как вы думаете, это состояние гонки? Несколько потоков разделяют одну кучу? Можете ли вы дать каждому потоку частную кучу с помощью HeapCreate, тогда они могут работать быстро с HEAP_NO_SERIALIZE. В противном случае, куча должна быть поточно-ориентированной, если вы используете многопоточную версию системных библиотек.
Пара предложений. Вы упомянули обильные предупреждения на W4 - я бы посоветовал потратить время на исправление кода для его аккуратной компиляции на уровне предупреждений 4 - это будет иметь большое значение для предотвращения трудно обнаруживаемых ошибок.
Второе - для параметра /analyse - оно действительно генерирует обильные предупреждения. Чтобы использовать этот параметр в моем собственном проекте, я создал новый файл заголовка, в котором использовалось предупреждение #pragma, чтобы отключить все дополнительные предупреждения, генерируемые / анализировать. Далее в файле я включаю только те предупреждения, которые меня волнуют. Затем используйте параметр компилятора /FI, чтобы этот файл заголовка был включен первым во все ваши модули компиляции. Это должно позволить вам использовать переключатель /analysis во время управления выводом
Предложение Грэма о кастомных malloc/free - хорошая идея. Посмотрите, можете ли вы охарактеризовать некоторую модель коррупции, чтобы дать вам возможность использовать ее.
Например, если он всегда находится в блоке одного и того же размера (скажем, 64 байта), измените вашу пару malloc/free, чтобы всегда выделять 64-байтовые чанки на их собственной странице. Когда вы освобождаете 64-байтовый блок, установите биты защиты памяти на этой странице, чтобы предотвратить чтение и чтение (используя VirtualQuery). Тогда любой, кто попытается получить доступ к этой памяти, сгенерирует исключение, а не повредит кучу.
Это предполагает, что количество выдающихся 64-байтовых блоков является лишь умеренным, или у вас есть много памяти для записи в коробке!
Немного времени мне пришлось решать аналогичную проблему. Если проблема все еще существует, я предлагаю вам сделать следующее: отслеживать все вызовы new/delete и malloc / calloc / realloc / free. Я делаю одну DLL, экспортирующую функцию для регистрации всех вызовов. Эта функция получает параметр для определения источника кода, указателя на выделенную область и типа вызова, сохраняя эту информацию в таблице. Все выделенные / освобожденные пары исключаются. В конце или после того, как вам нужно, вы вызываете другую функцию для создания отчета для оставленных данных. При этом вы можете определить неправильные вызовы (новые / бесплатные или malloc/delete) или пропущенные. Если в вашем коде есть какой-либо случай перезаписи буфера, сохраненная информация может быть неправильной, но каждый тест может обнаружить / обнаружить / включить решение выявленной ошибки. Много прогонов, чтобы помочь выявить ошибки. Удачи.