среда, 17 декабря 2008 г.

Классификация проблем с памятью

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

До сих пор я не задумывался о том сколько проблем бывает с памятью. Если аккуратно программировать то о них и не приходится задумываться. Но если в наследство достается старый проект, скорее всего он везет с собою массу проблем разного рода. А еще если подумать о том, что архитектура системы представляется пока слабо, руки вообще опускаются.

Кто-то скажет - переписать... Нет, это путь тупиковый. Всмысле переписать - это непроизводительная деятельность, Надо учиться наводить порядок. :) Но, ближе к теме.

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

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

Нарушение границы блока мне попалось не далее как сегодня. Выделялась память для локального буфера (фу), 26 байт (два раза фу), потом она заполнялась данными и прогонялась через RC2_cbc_encode, который выравнивает данные в большую сторону на 8 байт. Итого 6 байт за границей блока. Но в данном случае это скорее всего не вызывало проблем. malloc вероятно до 16 байт выравнивает и сам, и реально в блоке 6 дополнительных байт точно было. Так никто бы и не узнал о проблеме.

Эта ошибка является типичной при работе с си-строками.

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

Использование блока после освобождения. Самая опасная форма - модификация блоков, последствия таких ошибок очень сложно предсказать. Но эту форму теоретически можно отслеживать. Если же доступ к блоку осуществляется на чтение, отследить это невозможно. Можно попытаться подобрать данные для заполнения блока при освобождении так, чтобы приложение на них сломалось.

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

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

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

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

Выделение блока нулевой длины. В стандарте си по этому поводу написано, что поведение может определяться реализацией. If the size of the space requested is zero, the behavior is implementation-defined: either a null pointer is returned, or the behavior is as if the size were some nonzero value, except that the returned pointer shall not be used to access an object.

Однако далеко не всем приложения может понравиться NULL в ответ на malloc. Хотя помоему это наиболее правильный вариант. Допустимым также можно считать указатель куда нибудь на ридонли память, чтобы приложение не пыталось туда что-нибудь писать. Кстати вот не знаю как должен вести себя в этом случае operator new?

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

Освобождение нулевого указателя. Это даже и не проблема а вполне допустимая операция. Но если приложение дергает ее слишком часто - повод задуматься.

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

Спонтанное замусоривание памяти. Скорее всего возникает в следствии других ошибок. Прежде чем бороться с духами в приложении, лучше разобраться с явными вещами. :)

PS: Еще немного поковыряюсь с ликтрейсером и наверное выложу его куда нибудь. Может быть будет полезен.

5 коммент.:

Анонимный комментирует...

>Использование блока после освобождения.
Здесь можно прибегнуть к страничной защите памяти, а именно - выделять блоки с размером, кратным странице. Если приложение не ест много памяти, то это вполне приемлемо. При освобождении блока надо добиться, чтобы страница стала недоступной. Тогда приложение свалится по SIGSEGV, если хочет получить доступ к блоку.
Единственное - выделение надо делать по часовому механизму, чтобы блок как можно дольше был недоступен.
Это, конечно, не 100%-ное решение, но может помочь.

Yuri Volkov комментирует...

ух ты! классное руководство получается =). В закладки обязательно =)

Unknown комментирует...

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

Андрей Валяев комментирует...

Страницами - это слишком круто... там памяти то выделяется десятками байт, даже не сотнями. :)

К тому же рулить страницами с юзерлевела - тяжеловато.

А еще вопрос информативности. Хотя падение в нужном месте - уже хорошо, но если действовать аккуратно, то можно узнать сколько байт приложение недопросило. :)

Правда мы узнаем это только после освобождения блока... :(

Анонимный комментирует...

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

Утечка...Самая вероятная и часто встречающаяся проблема.
Ну надо же, у нас это - одна из редких проблем... Может, мы просто всегда контролируем ситуацию через crt.

Я как-то выработал у себя привычку не пользоваться динамической памятью без лишней необходимости.
+1

Ну и что, что на это тратится стек, у меня его много, не вижу проблем.
С другой стороны, кучи еще больше... И malloc при нехватки памяти вернет NULL, а если в стеке нет места - ...

Да, пример с локальным буфером 26 байт через кучу - впечатляет. Это какой же оверхед должен быть на аллокациях.

Кстати вот не знаю как должен вести себя в этом случае operator new?
Динамическое создание объекта нулевого размера? Или ругаться, или предупреждать и выделять 1 байт? Чтобы operator delete не рушилась.

2 Chizh
Также и выход за границу буфера можно ловить с помощью страничного механизма, если конец буфера выровнять на конец страницы, а следующая страница пустая (или нормальная страница, тогда контролировать в ней атрибут Dirty).
Оччень неэкономично. Такие реализации уже есть, но не везде их можно использовать, особенно в C++ приложениях, где число аллокаций достаточно велико... к сожалению. Представь, после каждого объекта - страница 4К, соответственно на сам объект тратится тоже 4К. А памяти не так много... хотя в безвыходных ситуациях можно и так делать.

Буфера больших размеров надо выделять только через страницы. Не понимаю, как это - рулить страницами с юзерлевела - тяжеловато. Часто используемые буфера надо вообще блокировать, чтобы не было вытеснения.