Я работаю над приложением многопоточный C++, которое разрушает кучу. Обычные инструменты для обнаружения этого повреждения кажутся неприменимыми. Старые сборки (18 месяцев назад) исходного кода демонстрируют то же поведение, что и самая последняя версия, так что это существует уже давно и просто не замечено; С другой стороны, дельты исходного кода не могут использоваться для определения времени появления ошибки - в репозитории есть много изменений кода.
Подсказка для аварийного поведения заключается в создании пропускной способности в этой системе - передача данных через сокет, которые преобразуются во внутреннее представление. У меня есть набор тестовых данных, которые будут периодически вызывать исключение приложения (в разных местах, по разным причинам, включая сбой выделения кучи, например: повреждение кучи).
Такое поведение кажется связанным с мощностью процессора или пропускной способностью памяти; чем больше каждого в машине, тем легче разбиться. Отключение ядра с гиперпоточностью или двухъядерного ядра снижает скорость (но не устраняет) повреждений. Это наводит на мысль о проблеме, связанной со временем.
Вот загвоздка:
Когда он запускается в облегченной среде отладки (скажем, Visual Studio 98 / AKA MSVC6
), повреждение кучи достаточно легко воспроизвести - проходит десять или пятнадцать минут, прежде чем что-то ужасно выйдет из строя и возникнут исключения, такие как alloc;
при работе в сложной среде отладки (Rational Purify, VS2008/MSVC9
или даже Microsoft Application Verifier) система становится ограниченной по скорости памяти и не дает сбоев (Ограничение памяти: ЦП не превышает 50%
, индикатор диска не горит, программа работает так быстро, как только может, коробка потребляет 1.3G
из 2 ГБ ОЗУ) . Итак, У меня есть выбор между возможностью воспроизвести проблему (но не определить причину) или возможностью определить причину или проблему, которую я не могу воспроизвести.
Мои текущие предположения о том, куда идти дальше:
E6550 Core2 Duo
); это позволит воспроизвести сбой, вызывающий неправильное поведение при работе в мощной среде отладки; или жеnew
и delete
для использования VirtualAlloc
и VirtualProtect
, чтобы пометить память как доступную только для чтения, как только это будет сделано. Запускайте под MSVC6
и пусть ОС поймает злоумышленника, который пишет в освобожденную память. Да, это знак отчаяния: кто, черт возьми, переписывает new
и delete
?! Интересно, сделает ли это такой же медленный процесс, как в Purify et al.И нет: доставка со встроенными приборами Purify невозможна.
Коллега просто прошел мимо и спросил: «Переполнение стека? Получается ли сейчас переполнение стека?!?»
А теперь вопрос: Как мне найти разрушителя кучи?
Обновление: балансировка new[]
и delete[]
, похоже, прошла долгий путь к решению проблемы. Вместо 15 минут приложение теперь доживает около двух часов до сбоя. Еще нет. Есть еще предложения? Повреждение кучи сохраняется.
Обновление: сборка релиза под Visual Studio 2008 кажется значительно лучше; текущие подозрения связаны с реализацией STL
, поставляемой с VS98
.
- 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 проектов) и проверки того, что все они компилируются и связываются с соответствующими флагами.
Settings
dialog, unselect until you find the project(s) that don't have the right settings (they all had the right settings).
Как именно выглядит сбой? Вы говорите «включая сбой выделения кучи» - может ли это означать, что вам просто не хватает памяти? (Я не разбираюсь в программировании Windows, но это может быть причиной в мире Linux.)
@svec C++ говорит, что из-за нехватки памяти выбрасывается std :: bad_alloc. Я вижу исключения памяти («эй, вы не можете там читать (или, может быть, писать)!»)
> Сборка с 2008 года поймала бы такую бредовую хрень ... может, даже MSVC6, но я не уверен. MSVC6 этого не поймает, но Линт поймет. Удаление текста из кода может быть хорошим началом. Это всего лишь 250 долларов (ноль по сравнению с временем, сэкономленным на отладке). Совет для тех, кто впервые использует линт: выключите все и медленно включайте. Я начал с ненужных заголовков и до сих пор доработал до 20 элементов. Когда я впервые запустил его на нашем продукте за ночь, в нем было больше ошибок, чем строк кода !!
Я не думаю, что у вас есть std :: bad_alloc в VC6, я думаю, он возвращает null?
Вы пробовали старые сборки, но есть ли причина, по которой вы не можете вернуться в историю репозитория и увидеть, когда именно была обнаружена ошибка?
В противном случае я бы предложил добавить какое-то простое ведение журнала, чтобы помочь отследить проблему, хотя я не понимаю, что конкретно вы можете захотеть регистрировать.
Если вы сможете узнать, что именно МОЖЕТ вызвать эту проблему, с помощью Google и документации по полученным вами исключениям, возможно, это даст дополнительное представление о том, что искать в коде.
Мой первый выбор - это специальный инструмент для работы с кучей, такой как pageheap.exe.
Перезапись new и delete может быть полезна, но это не улавливает выделения, зафиксированные кодом более низкого уровня. Если это то, что вы хотите, лучше обойти low-level alloc API
с помощью Microsoft Detours.
Также проверки работоспособности, такие как: убедитесь, что ваши библиотеки времени выполнения совпадают (выпуск или отладка, многопоточный или однопоточный, dll или статическая библиотека), поиск плохих удалений (например, удаление там, где должен был быть delete [] used), убедитесь, что вы не смешиваете и не сопоставляете свои аллоки.
Также попробуйте выборочно отключить потоки и посмотреть, когда / исчезнет ли проблема.
Как выглядит стек вызовов и т. д. Во время первого исключения?
Моим первым действием было бы следующее:
Еще одна попытка может заключаться в использовании WinDebug в качестве инструмента отладки, который является довольно мощным и в то же время легким.
Возможно, эти инструменты позволят вам хотя бы сузить проблему до определенного компонента.
И вы уверены, что все компоненты проекта имеют правильные настройки библиотеки времени выполнения (вкладка C / C++, категория «Генерация кода» в настройках проекта VS 6.0)?
У меня такие же проблемы в работе (мы тоже иногда используем VC6
). И для этого нет простого решения. У меня есть только подсказки:
STL
, попробуйте STLPort
и проверенные сборки. Неверный итератор - черт возьми.Удачи. На решение таких проблем, как ваша, у нас уходят месяцы. Будьте готовы к этому ...
Итак, исходя из имеющейся у вас ограниченной информации, это может быть комбинация одного или нескольких факторов:
Если это вообще первые два, но не последний, вы уже должны были поймать его с помощью 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!), Но в конечном итоге это очень полезно.
В книге Стива Магуайра Написание твердого кода (настоятельно рекомендуется) есть примеры отладки, которые вы можете выполнять с помощью этих подпрограмм, например:
Еще одна хорошая идея - использовать никогда такие вещи, как strcpy
, strcat
или sprintf
- всегда используйте strncpy
, strncat
и snprintf
. Мы также написали наши собственные версии для них, чтобы убедиться, что мы не списываем конец буфера, и они также выявили множество проблем.
«всегда использовать strncpy вместо strcpy» - в Microsoft CRT есть еще лучшая альтернатива, strcpy_s.
не забудьте прочитать полные спецификации msdn с такими функциями! могут случиться странные вещи, если вы не прочитаете их полностью!
Очевидная случайность повреждения памяти очень похожа на проблему синхронизации потоков - ошибка воспроизводится в зависимости от скорости машины. Если объекты (блоки памяти) разделяются между потоками, а примитивы синхронизации (критическая секция, мьютекс, семафор, другие) не относятся к каждому классу (объекту, классу), то можно прийти к ситуации. где класс (фрагмент памяти) удаляется / освобождается во время использования или используется после удаления / освобождения.
В качестве теста вы можете добавить примитивы синхронизации к каждому классу и методу. Это замедлит ваш код, потому что многие объекты будут ждать друг друга, но если это устранит повреждение кучи, ваша проблема с повреждением кучи станет проблемой оптимизации кода.
Предложение Грэма о пользовательском malloc / free - хорошая идея. Посмотрите, сможете ли вы охарактеризовать какую-то закономерность коррупции, чтобы дать вам рычаги воздействия.
Например, если он всегда находится в блоке одинакового размера (скажем, 64 байта), измените вашу пару malloc / free, чтобы всегда выделять 64-байтовые фрагменты на их собственной странице. Когда вы освобождаете 64-байтовый фрагмент, установите биты защиты памяти на этой странице, чтобы предотвратить чтение и удаление (с помощью VirtualQuery). Тогда любой, кто попытается получить доступ к этой памяти, сгенерирует исключение, а не повредит кучу.
Это предполагает, что количество ожидающих 64-байтовых фрагментов невелико или у вас есть много памяти, которую нужно записать в коробке!
Это в условиях нехватки памяти? Если это так, возможно, new возвращает NULL
, а не выбрасывает std :: bad_alloc. Старые компиляторы VC++
не реализовали это должным образом. Есть статья о том, что Сбои при выделении устаревшей памяти вызывает сбой приложений STL
, созданных с помощью VC6
.
Это было очень полезно! Я не знал, почему мой новый внезапно возвращал NULL после включения Application Verifier!
Запускаем оригинальное приложение с 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 при управлении выводом.
Было бы интересно узнать, есть ли у вас здесь реальное решение ...