Отладка повреждения памяти


23

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

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

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

Я пытался до сих пор:

Valgrinding черт из этого - нет недействительных записей, пока вещь не выходит из строя (что может занять 1+ дня в производстве ... или просто час), что действительно сбивает меня с толку, конечно, в какой-то момент он получит доступ к недействительной памяти и не перезапишет материал шанс? (Есть ли способ «разложить» диапазон адресов?)

Инструменты для анализа кода, а именно coverity и cppcheck. В то время как они указали на некоторые ... гадости и крайние случаи в коде, не было ничего серьезного.

Запись процесса, пока он не завершится с помощью gdb (через undodb), а затем работа в обратном направлении. Это / звучит / как будто это должно быть выполнимо, но я либо заканчиваю тем, что ломаю gdb, используя функцию автозаполнения, либо попадаю в какую-то внутреннюю структуру libevent, где я теряюсь, так как слишком много возможных ветвей (одно повреждение вызывает другое, и так на). Я думаю, было бы неплохо, если бы я мог видеть, к чему изначально принадлежит указатель / где он был размещен, что позволило бы устранить большинство проблем ветвления. Я не могу запустить valgrind с помощью undodb, и нормальная запись GDB слишком медленная (если это работает даже в сочетании с valgrind).

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

Я должен также отметить: я обычно получаю последовательные следы. Есть несколько мест, где происходит сбой, в основном связанный с тем, что класс сокета как-то повреждается. Будь то недопустимый указатель, указывающий на что-то, что не является сокетом, или сам класс сокета перезаписывается (частично?) Бредом. Хотя я подозреваю, что там больше всего происходит сбой, так как это одна из наиболее часто используемых частей, поэтому используется первая испорченная память.

В целом, этот вопрос занимал меня почти 2 месяца (время от времени, больше хобби), и это действительно расстраивает меня до такой степени, что я становлюсь раздражительным IRL и думаю о том, чтобы просто сдаться. Я просто не могу думать о том, что еще я должен сделать, чтобы найти проблему.

Есть ли полезные методы, которые я пропустил? Как ты с этим справляешься? (Это не может быть настолько распространенным, так как об этом не так много информации ... или я просто слепой?)

Редактировать:

Некоторые спецификации на случай, если это имеет значение:

Использование c ++ (11) через gcc 4.7 (версия предоставлена ​​debian wheezy)

Кодовая база составляет около 150 тыс. Строк

Редактировать в ответ на сообщение david.pfx: (извините за медленный ответ)

Ведете ли вы тщательный учет аварий, чтобы искать закономерности?

Да, у меня все еще есть свалки недавних аварий, лежащих вокруг

Несколько мест действительно похожи? В каком смысле?

Ну, в самой последней версии (кажется, они меняются всякий раз, когда я добавляю / удаляю код или изменяю связанные структуры), он всегда попадет в метод таймера предмета. В основном у элемента есть определенное время, после которого он истекает, и он отправляет обновленную информацию клиенту. Недопустимый указатель сокета будет в (все еще допустимом, насколько я могу судить) классе Player, в основном связанном с этим. Я также испытываю множество сбоев на этапе очистки после нормального завершения работы, когда он уничтожает все статические классы, которые не были явно уничтожены ( __run_exit_handlersв обратном следе). В основном это std::mapодин класс, предполагающий, что это только первое, что приходит на ум.

Как выглядят поврежденные данные? Нули? Ascii? Узоры?

Я не нашел никаких паттернов, мне кажется, что-то случайное. Трудно сказать, так как я не знаю, где началась коррупция.

Это связано с кучей?

Это полностью связано с кучей (я включил защиту стека gcc, и это ничего не перехватило).

Коррупция случается после free()?

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

Есть ли что-то особенное в сетевом трафике (размер буфера, цикл восстановления)?

Сетевой трафик состоит из необработанных данных. Таким образом, массивы char (u) intX_t или pack (для удаления заполнения) создают более сложные вещи, каждый пакет имеет заголовок, состоящий из идентификатора и самого размера пакета, который проверяется на соответствие ожидаемому размеру. Они имеют размер около 10-60 байт, при этом размер самого большого (внутреннего загрузочного пакета, запускаемого один раз при запуске) составляет несколько мегабайт.

Много-много продукции утверждает. Сбой рано и предсказуемо, прежде чем ущерб распространяется.

Однажды у меня был сбой, связанный с std::mapкоррупцией, у каждой сущности есть карта своего «вида», каждая сущность, которая может ее видеть, и наоборот, находится в этом. Я добавил 200-байтовый буфер впереди и после, заполнил его 0x33 и проверял его перед каждым доступом. Коррупция волшебным образом исчезла, я должен был что-то переделать, что сделало ее чем-то другим.

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

Это работает .. до некоторой степени.

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

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

Параллельность: многопоточность, условия гонки и т. Д.

Существует поток mysql для выполнения «асинхронных» запросов, но все это остается нетронутым и передает информацию только классу базы данных через функции со всеми блокировками.

Прерывания

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

if (!tics) {
    abort();
} else
    tics = 0;

тики, volatile int tics = 0;которые увеличиваются каждый раз, когда цикл завершен. Старый код тоже.

события / обратные вызовы / исключения: состояние повреждения или стек непредсказуемо

Используется множество обратных вызовов (асинхронный сетевой ввод / вывод, таймеры), но они не должны делать ничего плохого.

Необычные данные: необычные входные данные / время / состояние

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

Зависимость от асинхронного внешнего процесса.

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


7
К сожалению, это распространено в нетривиальных приложениях C ++. Если вы используете систему контроля версий, тестирование различных наборов изменений, чтобы определить, какие изменения кода вызвали проблему, может помочь, но в этом случае может оказаться невозможным.
Теластин

Да, это действительно невозможно в моем случае. Я в основном перешел с работы на полностью и полностью сломанный в течение 2 месяцев и затем на стадию отладки, где у меня есть несколько работающий код. Старая система действительно не позволяла мне реализовывать мой новый гибкий код сети, не нарушая ничего.
Робин

2
На этом этапе вам, возможно, придется попытаться изолировать каждую часть. Возьмите каждый класс / подмножество решения, сделайте имитацию вокруг него, чтобы он мог функционировать, и проверяйте его на жизнь, пока не найдете раздел, который не работает.
День

начните с комментирования частей кода, пока у вас больше не будет сбоя.
cpp81

1
В дополнение к Valgrind, Coverity и cppcheck вы должны добавить Asan и UBsan в режим тестирования. Если ваш код - corss-platofrm, добавьте Microsoft Enterprise Analysis ( /analyze) и Apple Malloc и Scribble. Вам также следует использовать как можно больше компиляторов, используя как можно больше стандартов, потому что предупреждения компилятора являются диагностическими и со временем становятся лучше. Здесь нет серебряной пули, и один размер подходит не всем. Чем больше инструментов и компиляторов вы используете, тем более полный охват, потому что у каждого инструмента есть свои сильные и слабые стороны.

Ответы:


21

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

  • Ведете ли вы тщательный учет аварий, чтобы искать закономерности?
  • Несколько мест действительно похожи? В каком смысле?
  • Как выглядят поврежденные данные? Нули? Ascii? Узоры?
  • Есть ли какая-то многопоточность? Может ли это быть состояние гонки?
  • Это связано с кучей? Повреждение происходит после free ()?
  • Это связано со стеком? Стек поврежден?
  • Возможна ли свисающая ссылка? Значение данных, которое загадочным образом изменилось?
  • Есть ли что-то особенное в сетевом трафике (размер буфера, цикл восстановления)?

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

  • Много-много продукции утверждает. Сбой рано и предсказуемо, прежде чем ущерб распространяется.
  • Много и много охранников. Дополнительные элементы данных до и после локальных переменных, объектов и mallocs () устанавливаются в значения, а затем часто проверяются.
  • Стратегическое ведение журнала, чтобы вы точно знали, что происходило прямо перед этим. Добавьте в журнал, как вы получите ближе к ответу.

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

Не стесняйтесь добавлять детали, если мы можем помочь вообще.


Могу ли я просто добавить, что серьезные неопределенные ошибки, подобные этой, встречаются не так часто, и не так много вещей, которые могут (обычно) их вызывать. Они включают:

  • Параллельность: многопоточность, условия гонки и т. Д.
  • Прерывания / события / обратные вызовы / исключения: состояние повреждения или стек непредсказуемо
  • Необычные данные: необычные входные данные / время / состояние
  • Зависимость от асинхронного внешнего процесса.

Это те части кода, на которых нужно сосредоточиться.


+1 Все хорошие предложения, особенно утверждения, охрана и логирование.
andy256

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

5

Используйте отладочную версию malloc / free. Оберните их и напишите свой, если это необходимо. Много веселья!

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

Один из самых коварных источников коррупции продолжает использовать кусок после его освобождения. Освобождение должно заполнять освобожденную память известным шаблоном (традиционно 0xDEADBEEF). Это помогает, если выделенные структуры включают элемент «магическое число», и свободно включают проверки на соответствующее магическое число перед использованием структуры.


1
Valgrind должен поймать двойное освобождение / использование free'd данных, не так ли?
Робин

Запись такого рода перегрузок для new / delete помогла мне обнаружить многочисленные проблемы с повреждением памяти. Особенно защитные байты, которые проверяются при удалении и вызывают программную точку останова, которая автоматически переводит меня в отладчик.
Эмили Л.

3

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

Некоторые предложения могут показаться наивными, другие могут показаться более подходящими, но мы надеемся, что одна из них заставит вас задуматься. Я должен сказать, что ответ от david.pfx имеет здравые советы и предложения.

Из симптомов

  • для меня это звучит как переполнение буфера.

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

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

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

  • У каждой переменной есть описание? И вы можете определить утверждение действительности?
    Если не добавить их, просмотрите код, чтобы увидеть, что каждая переменная используется правильно. Добавьте это утверждение везде, где это имеет смысл.

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

  • Я использую много журналов для отладки длинных / асинхронных / в реальном времени кодов.
    Снова вставьте запись в журнал при каждом вызове функции.
    Если файлы журнала становятся слишком большими, функции ведения журнала могут переносить / переключать файлы / и т. Д.
    Это наиболее полезно, если сообщения журнала имеют отступ от глубины вызова функции.
    Файл журнала может показать, как распространяется ошибка. Полезно, когда один фрагмент кода делает что-то не совсем правильное, что действует как бомба замедленного действия.

У многих людей есть свой собственный код для регистрации. У меня где-то есть старая система журналов макросов C, и, возможно, версия C ++ ...


3

Все, что было сказано в других ответах, очень актуально. Одной важной вещью, частично упоминаемой ddyer, является то, что упаковка malloc / free имеет свои преимущества. Он упоминает несколько, но я хотел бы добавить к этому очень важный инструмент отладки: вы можете записать каждый malloc / free во внешний файл вместе с несколькими строками callstack (или полным callstack, если вам это нужно). Если вы осторожны, вы можете легко сделать это довольно быстро и использовать его в производстве, если дело доходит до этого.

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

Важным примечанием является то, что, как вы упоминаете, изменение макета памяти может скрыть проблему. Таким образом, очень важно, чтобы при ведении журнала не было выделений (если вы можете!) Или как можно меньше. Это поможет воспроизводимости, если это связано с памятью. Это также поможет, если это будет как можно быстрее, если проблема связана с многопоточностью.

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

В качестве последней альтернативы вы также можете создать собственный распределитель, в котором для каждого выделения будет выделено не менее 2 страниц, и при их освобождении отменить отображение (выровнять выделение по границе страницы, выделить страницу до и пометить ее как недоступную или выровнять выделить в конце страницы и выделить страницу после и пометить как недоступные). Убедитесь, что вы не используете эти адреса виртуальной памяти для новых выделений хотя бы некоторое время. Это означает, что вам нужно самостоятельно управлять своей виртуальной памятью (зарезервируйте ее и используйте по своему усмотрению). Обратите внимание, что это ухудшит вашу производительность и может привести к значительному использованию виртуальной памяти в зависимости от того, сколько выделенных ресурсов вы используете. Чтобы смягчить это, будет полезно, если вы сможете работать в 64-битной среде и / или уменьшить диапазон выделений, которые в этом нуждаются (в зависимости от размера). Valgrind вполне может уже сделать это, но он может быть слишком медленным, чтобы вы могли решить проблему с ним. Выполнение этого только для нескольких размеров или объектов (если вы знаете какой, вы можете использовать специальный распределитель только для этих объектов) обеспечит минимальное влияние на производительность.


0

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.