Повреждение кучи под 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. Reproduce the problem. Dr Watson will produce a dump that might be helpful in further analysis.

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

Another try might be using WinDebug as a debugging tool which is quite powerful being at the same time also lightweight.

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

Maybe these tools will allow you at least to narrow the problem to certain component.

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

And are you sure that all the components of the project have correct runtime library settings (C/C++ tab, Code Generation category in VS 6.0 project settings)?

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


Update: This took 30 seconds. Select all projects in the Settings dialog, unselect until you find the project(s) that don't have the right settings (they all had the right settings).

Было бы интересно узнать, есть ли у вас здесь реальное решение ...

Roddy 15.03.2010 18:49

Как именно выглядит сбой? Вы говорите «включая сбой выделения кучи» - может ли это означать, что вам просто не хватает памяти? (Я не разбираюсь в программировании Windows, но это может быть причиной в мире Linux.)

svec 06.08.2008 01:03

@svec C++ говорит, что из-за нехватки памяти выбрасывается std :: bad_alloc. Я вижу исключения памяти («эй, вы не можете там читать (или, может быть, писать)!»)

Josh 06.08.2008 16:36

> Сборка с 2008 года поймала бы такую ​​бредовую хрень ... может, даже MSVC6, но я не уверен. MSVC6 этого не поймает, но Линт поймет. Удаление текста из кода может быть хорошим началом. Это всего лишь 250 долларов (ноль по сравнению с временем, сэкономленным на отладке). Совет для тех, кто впервые использует линт: выключите все и медленно включайте. Я начал с ненужных заголовков и до сих пор доработал до 20 элементов. Когда я впервые запустил его на нашем продукте за ночь, в нем было больше ошибок, чем строк кода !!

graham.reeds 06.08.2008 16:49

Я не думаю, что у вас есть std :: bad_alloc в VC6, я думаю, он возвращает null?

paulm 02.01.2013 15:01
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
63
5
37 046
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

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

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

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

Ответ принят как подходящий

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Плохое использование кучи, то есть двойное освобождение, чтение после освобождения, запись после освобождения, установка флага 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!), Но в конечном итоге это очень полезно.

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

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

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

«всегда использовать strncpy вместо strcpy» - в Microsoft CRT есть еще лучшая альтернатива, strcpy_s.

Constantin 13.10.2008 00:37

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

alcor 10.04.2013 23:49

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

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

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

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

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

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

Это было очень полезно! Я не знал, почему мой новый внезапно возвращал NULL после включения Application Verifier!

Vinz 08.06.2015 23:36

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

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

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

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

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

P.S: Вы можете использовать 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.

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

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

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

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

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